Allow track settings for completed tasks: re-probe file, keep Done until save; reset to queue on changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3459204e6f
commit
f5c6cc7438
@ -1,4 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EmbyToolbox.Models;
|
||||
|
||||
namespace EmbyToolbox.Services;
|
||||
@ -6,6 +9,9 @@ namespace EmbyToolbox.Services;
|
||||
/// <summary>Ход пакетного анализа очереди (ffprobe) для IProgress и UI.</summary>
|
||||
public readonly record struct QueueAnalysisProgress(int Processed, int Total, int ErrorCount);
|
||||
|
||||
/// <summary>Поля завершённой задачи, которые восстанавливаются после повторного ffprobe (форма дорожек).</summary>
|
||||
public readonly record struct DoneCompletionState(int Progress, string? LastRunId, bool IsProcessed, bool ProcessedInCurrentRun);
|
||||
|
||||
/// <summary>
|
||||
/// Асинхронный батч ffprobe: ограниченный параллелизм, отмена по <see cref="CancellationToken"/>,
|
||||
/// обновление строки через <paramref name="uiInvoke"/> (UI).
|
||||
@ -33,6 +39,112 @@ public sealed class QueueAnalysisService
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Повторный анализ текущего файла на диске (актуально после «Готово» или при отсутствии MediaAnalysis).
|
||||
/// Пересобирает дорожки и план; если передан <paramref name="preserveCompletion"/> — статус остаётся «Готово».
|
||||
/// </summary>
|
||||
public async Task<bool> RefreshItemMediaForTrackEditorAsync(
|
||||
ConversionQueueItem item,
|
||||
bool autoRemoveForeignTracks,
|
||||
bool disableSubtitleDefault,
|
||||
DoneCompletionState? preserveCompletion,
|
||||
Func<Action, Task> uiInvoke,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.FullPath) || !File.Exists(item.FullPath))
|
||||
{
|
||||
_logging.Warning($"редактор дорожек: файл не найден — {item.FullPath}", "conversion.queue");
|
||||
return false;
|
||||
}
|
||||
|
||||
FfprobeResult result;
|
||||
try
|
||||
{
|
||||
result = await _ffprobe.AnalyzeAsync(item.FullPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_logging.Error(
|
||||
$"редактор дорожек: ffprobe — {result.Error} — {item.FullPath}",
|
||||
"conversion.ffprobe",
|
||||
stderr: result.StdErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
var media = MediaAnalysisParser.TryParse(result.Json);
|
||||
if (media is null)
|
||||
{
|
||||
_logging.Error($"редактор дорожек: неверный JSON ffprobe — {item.FullPath}", "conversion.ffprobe");
|
||||
return false;
|
||||
}
|
||||
|
||||
SidecarDiscoveryResult discovery;
|
||||
try
|
||||
{
|
||||
discovery = await _sidecar.DiscoverAsync(item.FullPath, _ffprobe, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logging.Error($"редактор дорожек: sidecar — {ex.Message} — {item.FullPath}", "conversion.sidecar", ex);
|
||||
discovery = new SidecarDiscoveryResult(Array.Empty<SidecarFile>(), Array.Empty<ExternalAudioFile>());
|
||||
}
|
||||
|
||||
var audio = FfprobeAudioInfoParser.TryParse(result.Json) ?? new FfprobeAudioInfo(0, null, true);
|
||||
await uiInvoke(
|
||||
() =>
|
||||
{
|
||||
var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
item.TaskOverride.TrackOverrides.Clear();
|
||||
TrackOverrideSeeder.EnsureDefaults(
|
||||
item.TaskOverride,
|
||||
media,
|
||||
discovery.Sidecars,
|
||||
profile,
|
||||
autoRemoveForeignTracks,
|
||||
discovery.ExternalAudioFiles,
|
||||
item.FullPath,
|
||||
sidecarTitleResolver: null,
|
||||
logging: _logging,
|
||||
disableSubtitleDefault: disableSubtitleDefault);
|
||||
var plan = _planService.Build(media, discovery.Sidecars, profile, item.TaskOverride, discovery.ExternalAudioFiles);
|
||||
item.SetSuccessfulMediaAnalysis(
|
||||
media,
|
||||
discovery.Sidecars,
|
||||
discovery.ExternalAudioFiles,
|
||||
plan,
|
||||
audio.AudioStreamCount,
|
||||
audio.AudioSizeMbTotal,
|
||||
audio.IsPartial);
|
||||
if (preserveCompletion is { } d)
|
||||
{
|
||||
item.Status = ConversionQueueStatus.Done;
|
||||
item.Progress = d.Progress;
|
||||
item.LastRunId = d.LastRunId;
|
||||
item.IsProcessed = d.IsProcessed;
|
||||
item.ProcessedInCurrentRun = d.ProcessedInCurrentRun;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Status = ConversionQueueStatus.Pending;
|
||||
item.Progress = 0;
|
||||
item.ErrorMessage = null;
|
||||
item.ErrorDetails = null;
|
||||
}
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(
|
||||
IReadOnlyList<ConversionQueueItem> items,
|
||||
Func<ConversionQueueItem, bool> isStillInQueue,
|
||||
|
||||
@ -5,6 +5,8 @@ using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
@ -107,9 +109,13 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
PlayFileCommand = new RelayCommand(ExecutePlayFile, CanPlayFile);
|
||||
ClearQueueCommand = new RelayCommand(ExecuteClearQueue, () => QueueTasks.Count > 0 && !IsExecutionRunning);
|
||||
ClearCompletedFromQueueCommand = new RelayCommand(ExecuteClearCompletedFromQueue, CanClearCompletedFromQueue);
|
||||
OpenFileConversionSettingsCommand = new RelayCommand(ExecuteOpenFileSettings, p => !IsExecutionRunning && p is ConversionQueueItem i && i.MediaAnalysis is not null);
|
||||
OpenFileConversionSettingsCommand = new RelayCommand(
|
||||
p => { _ = ExecuteOpenFileSettingsAsync(p); },
|
||||
CanExecuteOpenFileSettings);
|
||||
OpenTrackSettingsCommand = new RelayCommand(ExecuteOpenTrackSettingsFromKeyboard, CanOpenTrackSettingsFromKeyboard);
|
||||
OpenBulkFileConversionSettingsCommand = new RelayCommand(ExecuteOpenBulkFileSettings, CanOpenBulkFileSettings);
|
||||
OpenBulkFileConversionSettingsCommand = new RelayCommand(
|
||||
p => { _ = ExecuteOpenBulkFileSettingsAsync(p); },
|
||||
CanOpenBulkFileSettings);
|
||||
StartProcessingCommand = new RelayCommand(ExecuteStartProcessing, () => !IsExecutionRunning);
|
||||
StopProcessingCommand = new RelayCommand(ExecuteStopProcessing, () => IsExecutionRunning);
|
||||
CopyQueueItemErrorCommand = new RelayCommand(ExecuteCopyQueueItemError, CanCopyQueueItemError);
|
||||
@ -603,14 +609,56 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteOpenFileSettings(object? parameter)
|
||||
private bool CanExecuteOpenFileSettings(object? p) =>
|
||||
!IsExecutionRunning
|
||||
&& p is ConversionQueueItem i
|
||||
&& i.Status is not (
|
||||
ConversionQueueStatus.Analyzing
|
||||
or ConversionQueueStatus.Running
|
||||
or ConversionQueueStatus.Copying
|
||||
or ConversionQueueStatus.Replacing)
|
||||
&& !string.IsNullOrWhiteSpace(i.FullPath)
|
||||
&& File.Exists(i.FullPath);
|
||||
|
||||
private async Task ExecuteOpenFileSettingsAsync(object? parameter)
|
||||
{
|
||||
if (IsExecutionRunning)
|
||||
try
|
||||
{
|
||||
if (IsExecutionRunning || parameter is not ConversionQueueItem item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameter is not ConversionQueueItem item || item.MediaAnalysis is null)
|
||||
var needsProbe = item.MediaAnalysis is null
|
||||
|| string.Equals(item.Status, ConversionQueueStatus.Done, StringComparison.Ordinal);
|
||||
if (needsProbe)
|
||||
{
|
||||
DoneCompletionState? preserve = null;
|
||||
if (string.Equals(item.Status, ConversionQueueStatus.Done, StringComparison.Ordinal))
|
||||
{
|
||||
preserve = new DoneCompletionState(
|
||||
item.Progress,
|
||||
item.LastRunId,
|
||||
item.IsProcessed,
|
||||
item.ProcessedInCurrentRun);
|
||||
}
|
||||
|
||||
var ok = await _queueAnalysis.RefreshItemMediaForTrackEditorAsync(
|
||||
item,
|
||||
autoRemoveForeignTracks: false,
|
||||
DisableSubtitleDefault,
|
||||
preserve,
|
||||
RunOnUiActionAsync,
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(true);
|
||||
if (!ok)
|
||||
{
|
||||
ShowToast("Не удалось прочитать дорожки файла (ffprobe).", ToastKind.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.MediaAnalysis is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -631,6 +679,59 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
() => w.Close());
|
||||
w.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logging.Error($"форма дорожек: {ex.Message}", "conversion.ui", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Перед формой дорожек: ffprobe для «Готово» и строк без MediaAnalysis.
|
||||
/// </summary>
|
||||
private async Task<bool> EnsureMediaFreshForTrackEditorsAsync(
|
||||
IReadOnlyList<ConversionQueueItem> items,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
var needsProbe = item.MediaAnalysis is null
|
||||
|| string.Equals(item.Status, ConversionQueueStatus.Done, StringComparison.Ordinal);
|
||||
if (!needsProbe)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.FullPath) || !File.Exists(item.FullPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DoneCompletionState? preserve = null;
|
||||
if (string.Equals(item.Status, ConversionQueueStatus.Done, StringComparison.Ordinal))
|
||||
{
|
||||
preserve = new DoneCompletionState(
|
||||
item.Progress,
|
||||
item.LastRunId,
|
||||
item.IsProcessed,
|
||||
item.ProcessedInCurrentRun);
|
||||
}
|
||||
|
||||
var ok = await _queueAnalysis.RefreshItemMediaForTrackEditorAsync(
|
||||
item,
|
||||
autoRemoveForeignTracks: false,
|
||||
DisableSubtitleDefault,
|
||||
preserve,
|
||||
RunOnUiActionAsync,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(true);
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CanOpenTrackSettingsFromKeyboard(object? parameter)
|
||||
{
|
||||
@ -641,8 +742,13 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
|
||||
var item = ResolveItemForTrackSettings(parameter);
|
||||
return item is not null
|
||||
&& item.MediaAnalysis is not null
|
||||
&& item.Status is not (ConversionQueueStatus.Running or ConversionQueueStatus.Copying or ConversionQueueStatus.Replacing);
|
||||
&& item.Status is not (
|
||||
ConversionQueueStatus.Analyzing
|
||||
or ConversionQueueStatus.Running
|
||||
or ConversionQueueStatus.Copying
|
||||
or ConversionQueueStatus.Replacing)
|
||||
&& !string.IsNullOrWhiteSpace(item.FullPath)
|
||||
&& File.Exists(item.FullPath);
|
||||
}
|
||||
|
||||
private void ExecuteOpenTrackSettingsFromKeyboard(object? parameter)
|
||||
@ -654,7 +760,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
_logging.Debug("Открыты настройки дорожек через F2", "conversion.keyboard");
|
||||
ExecuteOpenFileSettings(item);
|
||||
_ = ExecuteOpenFileSettingsAsync(item);
|
||||
}
|
||||
|
||||
private ConversionQueueItem? ResolveItemForTrackSettings(object? parameter)
|
||||
@ -689,7 +795,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
ConversionQueueStatus.Running or ConversionQueueStatus.Copying or ConversionQueueStatus.Replacing));
|
||||
}
|
||||
|
||||
private void ExecuteOpenBulkFileSettings(object? parameter)
|
||||
private async Task ExecuteOpenBulkFileSettingsAsync(object? parameter)
|
||||
{
|
||||
var selected = ResolveBulkSelection(parameter);
|
||||
if (selected.Count < 2)
|
||||
@ -697,6 +803,12 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await EnsureMediaFreshForTrackEditorsAsync(selected, CancellationToken.None).ConfigureAwait(true))
|
||||
{
|
||||
ShowToast("Не удалось обновить сведения о файлах (ffprobe).", ToastKind.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_logging.Info($"массовая настройка: выбрано строк {selected.Count}", "conversion.bulk");
|
||||
var analysis = _bulkTrackSettingsService.Analyze(selected);
|
||||
if (!analysis.HasMajority || analysis.MajorityItems.Count < 2)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user