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.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using EmbyToolbox.Models;
|
using EmbyToolbox.Models;
|
||||||
|
|
||||||
namespace EmbyToolbox.Services;
|
namespace EmbyToolbox.Services;
|
||||||
@ -6,6 +9,9 @@ namespace EmbyToolbox.Services;
|
|||||||
/// <summary>Ход пакетного анализа очереди (ffprobe) для IProgress и UI.</summary>
|
/// <summary>Ход пакетного анализа очереди (ffprobe) для IProgress и UI.</summary>
|
||||||
public readonly record struct QueueAnalysisProgress(int Processed, int Total, int ErrorCount);
|
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>
|
/// <summary>
|
||||||
/// Асинхронный батч ffprobe: ограниченный параллелизм, отмена по <see cref="CancellationToken"/>,
|
/// Асинхронный батч ffprobe: ограниченный параллелизм, отмена по <see cref="CancellationToken"/>,
|
||||||
/// обновление строки через <paramref name="uiInvoke"/> (UI).
|
/// обновление строки через <paramref name="uiInvoke"/> (UI).
|
||||||
@ -33,6 +39,112 @@ public sealed class QueueAnalysisService
|
|||||||
_profile = profile;
|
_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(
|
public async Task<int> RunAsync(
|
||||||
IReadOnlyList<ConversionQueueItem> items,
|
IReadOnlyList<ConversionQueueItem> items,
|
||||||
Func<ConversionQueueItem, bool> isStillInQueue,
|
Func<ConversionQueueItem, bool> isStillInQueue,
|
||||||
|
|||||||
@ -5,6 +5,8 @@ using System.ComponentModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@ -107,9 +109,13 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
|||||||
PlayFileCommand = new RelayCommand(ExecutePlayFile, CanPlayFile);
|
PlayFileCommand = new RelayCommand(ExecutePlayFile, CanPlayFile);
|
||||||
ClearQueueCommand = new RelayCommand(ExecuteClearQueue, () => QueueTasks.Count > 0 && !IsExecutionRunning);
|
ClearQueueCommand = new RelayCommand(ExecuteClearQueue, () => QueueTasks.Count > 0 && !IsExecutionRunning);
|
||||||
ClearCompletedFromQueueCommand = new RelayCommand(ExecuteClearCompletedFromQueue, CanClearCompletedFromQueue);
|
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);
|
OpenTrackSettingsCommand = new RelayCommand(ExecuteOpenTrackSettingsFromKeyboard, CanOpenTrackSettingsFromKeyboard);
|
||||||
OpenBulkFileConversionSettingsCommand = new RelayCommand(ExecuteOpenBulkFileSettings, CanOpenBulkFileSettings);
|
OpenBulkFileConversionSettingsCommand = new RelayCommand(
|
||||||
|
p => { _ = ExecuteOpenBulkFileSettingsAsync(p); },
|
||||||
|
CanOpenBulkFileSettings);
|
||||||
StartProcessingCommand = new RelayCommand(ExecuteStartProcessing, () => !IsExecutionRunning);
|
StartProcessingCommand = new RelayCommand(ExecuteStartProcessing, () => !IsExecutionRunning);
|
||||||
StopProcessingCommand = new RelayCommand(ExecuteStopProcessing, () => IsExecutionRunning);
|
StopProcessingCommand = new RelayCommand(ExecuteStopProcessing, () => IsExecutionRunning);
|
||||||
CopyQueueItemErrorCommand = new RelayCommand(ExecuteCopyQueueItemError, CanCopyQueueItemError);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parameter is not ConversionQueueItem item || item.MediaAnalysis is null)
|
return true;
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanOpenTrackSettingsFromKeyboard(object? parameter)
|
private bool CanOpenTrackSettingsFromKeyboard(object? parameter)
|
||||||
@ -641,8 +742,13 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
|||||||
|
|
||||||
var item = ResolveItemForTrackSettings(parameter);
|
var item = ResolveItemForTrackSettings(parameter);
|
||||||
return item is not null
|
return item is not null
|
||||||
&& item.MediaAnalysis is not null
|
&& item.Status is not (
|
||||||
&& item.Status is not (ConversionQueueStatus.Running or ConversionQueueStatus.Copying or ConversionQueueStatus.Replacing);
|
ConversionQueueStatus.Analyzing
|
||||||
|
or ConversionQueueStatus.Running
|
||||||
|
or ConversionQueueStatus.Copying
|
||||||
|
or ConversionQueueStatus.Replacing)
|
||||||
|
&& !string.IsNullOrWhiteSpace(item.FullPath)
|
||||||
|
&& File.Exists(item.FullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOpenTrackSettingsFromKeyboard(object? parameter)
|
private void ExecuteOpenTrackSettingsFromKeyboard(object? parameter)
|
||||||
@ -654,7 +760,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logging.Debug("Открыты настройки дорожек через F2", "conversion.keyboard");
|
_logging.Debug("Открыты настройки дорожек через F2", "conversion.keyboard");
|
||||||
ExecuteOpenFileSettings(item);
|
_ = ExecuteOpenFileSettingsAsync(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConversionQueueItem? ResolveItemForTrackSettings(object? parameter)
|
private ConversionQueueItem? ResolveItemForTrackSettings(object? parameter)
|
||||||
@ -689,7 +795,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
|||||||
ConversionQueueStatus.Running or ConversionQueueStatus.Copying or ConversionQueueStatus.Replacing));
|
ConversionQueueStatus.Running or ConversionQueueStatus.Copying or ConversionQueueStatus.Replacing));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOpenBulkFileSettings(object? parameter)
|
private async Task ExecuteOpenBulkFileSettingsAsync(object? parameter)
|
||||||
{
|
{
|
||||||
var selected = ResolveBulkSelection(parameter);
|
var selected = ResolveBulkSelection(parameter);
|
||||||
if (selected.Count < 2)
|
if (selected.Count < 2)
|
||||||
@ -697,6 +803,12 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await EnsureMediaFreshForTrackEditorsAsync(selected, CancellationToken.None).ConfigureAwait(true))
|
||||||
|
{
|
||||||
|
ShowToast("Не удалось обновить сведения о файлах (ffprobe).", ToastKind.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_logging.Info($"массовая настройка: выбрано строк {selected.Count}", "conversion.bulk");
|
_logging.Info($"массовая настройка: выбрано строк {selected.Count}", "conversion.bulk");
|
||||||
var analysis = _bulkTrackSettingsService.Analyze(selected);
|
var analysis = _bulkTrackSettingsService.Analyze(selected);
|
||||||
if (!analysis.HasMajority || analysis.MajorityItems.Count < 2)
|
if (!analysis.HasMajority || analysis.MajorityItems.Count < 2)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user