diff --git a/EmbyToolbox/Services/QueueAnalysisService.cs b/EmbyToolbox/Services/QueueAnalysisService.cs
index b1401ea..1f866e4 100644
--- a/EmbyToolbox/Services/QueueAnalysisService.cs
+++ b/EmbyToolbox/Services/QueueAnalysisService.cs
@@ -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;
/// Ход пакетного анализа очереди (ffprobe) для IProgress и UI.
public readonly record struct QueueAnalysisProgress(int Processed, int Total, int ErrorCount);
+/// Поля завершённой задачи, которые восстанавливаются после повторного ffprobe (форма дорожек).
+public readonly record struct DoneCompletionState(int Progress, string? LastRunId, bool IsProcessed, bool ProcessedInCurrentRun);
+
///
/// Асинхронный батч ffprobe: ограниченный параллелизм, отмена по ,
/// обновление строки через (UI).
@@ -33,6 +39,112 @@ public sealed class QueueAnalysisService
_profile = profile;
}
+ ///
+ /// Повторный анализ текущего файла на диске (актуально после «Готово» или при отсутствии MediaAnalysis).
+ /// Пересобирает дорожки и план; если передан — статус остаётся «Готово».
+ ///
+ public async Task RefreshItemMediaForTrackEditorAsync(
+ ConversionQueueItem item,
+ bool autoRemoveForeignTracks,
+ bool disableSubtitleDefault,
+ DoneCompletionState? preserveCompletion,
+ Func 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(), Array.Empty());
+ }
+
+ 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 RunAsync(
IReadOnlyList items,
Func isStillInQueue,
diff --git a/EmbyToolbox/ViewModels/ConversionViewModel.cs b/EmbyToolbox/ViewModels/ConversionViewModel.cs
index 34b0252..72f3f09 100644
--- a/EmbyToolbox/ViewModels/ConversionViewModel.cs
+++ b/EmbyToolbox/ViewModels/ConversionViewModel.cs
@@ -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,33 +609,128 @@ 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
{
- return;
+ if (IsExecutionRunning || parameter is not ConversionQueueItem item)
+ {
+ return;
+ }
+
+ 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;
+ }
+
+ var w = new FileConversionSettingsWindow
+ {
+ Owner = Application.Current?.MainWindow
+ };
+ w.DataContext = new FileConversionSettingsViewModel(
+ item,
+ _planService,
+ _profile,
+ _trackSnapshotService,
+ _logging,
+ FormOptions,
+ CopyPreviousTrackSettings,
+ OnFileSettingsSaved,
+ () => w.Close());
+ w.ShowDialog();
+ }
+ catch (Exception ex)
+ {
+ _logging.Error($"форма дорожек: {ex.Message}", "conversion.ui", ex);
+ }
+ }
+
+ ///
+ /// Перед формой дорожек: ffprobe для «Готово» и строк без MediaAnalysis.
+ ///
+ private async Task EnsureMediaFreshForTrackEditorsAsync(
+ IReadOnlyList 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;
+ }
}
- if (parameter is not ConversionQueueItem item || item.MediaAnalysis is null)
- {
- return;
- }
-
- var w = new FileConversionSettingsWindow
- {
- Owner = Application.Current?.MainWindow
- };
- w.DataContext = new FileConversionSettingsViewModel(
- item,
- _planService,
- _profile,
- _trackSnapshotService,
- _logging,
- FormOptions,
- CopyPreviousTrackSettings,
- OnFileSettingsSaved,
- () => w.Close());
- w.ShowDialog();
+ 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)