emby-toolbox/EmbyToolbox/Models/ConversionQueueItem.cs
2026-05-16 20:28:49 +05:00

674 lines
20 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
namespace EmbyToolbox.Models;
public sealed class ConversionQueueItem : INotifyPropertyChanged
{
private string _fullPath;
private string _fileName;
private string _directoryPath;
private int _orderNumber;
private int _progress;
private string _status = ConversionQueueStatus.Pending;
private string _profile = "Emby";
private string _planSummary = string.Empty;
private int _fileSizeMb;
/// <summary>True после успешного ffprobe (аудио-поля валидны для отображения).</summary>
private bool _ffprobeAnalyzed;
private int _ffprobeAudioCount;
private int? _ffprobeAudioSizeMb;
private bool _ffprobeAudioSizePartial;
private MediaAnalysisResult? _mediaAnalysis;
private IReadOnlyList<SidecarFile> _sidecars = System.Array.Empty<SidecarFile>();
private IReadOnlyList<ExternalAudioFile> _externalAudioFiles = System.Array.Empty<ExternalAudioFile>();
private EffectiveProfileSettings? _effectiveProfileSettings;
private ConversionPlan? _lastPlan;
private bool _isProcessed;
private bool _processedInCurrentRun;
private string? _lastRunId;
private bool _isSkipPlan = true;
private bool _isManuallyEdited;
private string? _errorMessage;
private string? _errorDetails;
public string FullPath
{
get => _fullPath;
private set
{
if (string.Equals(_fullPath, value, StringComparison.OrdinalIgnoreCase))
{
return;
}
_fullPath = value;
_directoryPath = Path.GetDirectoryName(value) ?? string.Empty;
OnPropertyChanged();
OnPropertyChanged(nameof(DirectoryPath));
}
}
public string FileName
{
get => _fileName;
private set
{
if (string.Equals(_fileName, value, StringComparison.Ordinal))
{
return;
}
_fileName = value;
OnPropertyChanged();
}
}
public string DirectoryPath
{
get => _directoryPath;
private set
{
if (string.Equals(_directoryPath, value, StringComparison.Ordinal))
{
return;
}
_directoryPath = value;
OnPropertyChanged();
}
}
/// <summary>Параметры из ffprobe + списки потоков.</summary>
public MediaAnalysisResult? MediaAnalysis
{
get => _mediaAnalysis;
private set
{
if (ReferenceEquals(_mediaAnalysis, value))
{
return;
}
_mediaAnalysis = value;
OnPropertyChanged();
OnPropertyChanged(nameof(TrackSummaryDisplay));
}
}
public IReadOnlyList<SidecarFile> Sidecars
{
get => _sidecars;
private set
{
if (Equals(_sidecars, value))
{
return;
}
_sidecars = value;
OnPropertyChanged();
}
}
/// <summary>Общий корень батча (добавление каталогом или вычисленный LCA файлов при перетаскивании/мультовыборе). Для области snapshot между эпизодами одного добавления.</summary>
public string? SnapshotScopeBatchRoot { get; set; }
/// <summary>Разбор внешних аудиофайлов (мультипотоковые контейнеры и т.д.) для пере-seed дорожек.</summary>
public IReadOnlyList<ExternalAudioFile> ExternalAudioFiles
{
get => _externalAudioFiles;
private set
{
if (Equals(_externalAudioFiles, value))
{
return;
}
_externalAudioFiles = value;
OnPropertyChanged();
}
}
public ConversionTaskOverride TaskOverride { get; } = new();
public EffectiveProfileSettings? EffectiveProfileSettings
{
get => _effectiveProfileSettings;
set
{
if (ReferenceEquals(_effectiveProfileSettings, value))
{
return;
}
_effectiveProfileSettings = value;
OnPropertyChanged();
}
}
public ConversionPlan? LastPlan
{
get => _lastPlan;
private set
{
if (Equals(_lastPlan, value))
{
return;
}
_lastPlan = value;
OnPropertyChanged();
}
}
public ConversionQueueItem(string fullPath)
{
var normalized = Path.GetFullPath(fullPath);
_fullPath = normalized;
_fileName = Path.GetFileName(normalized);
_directoryPath = Path.GetDirectoryName(normalized) ?? string.Empty;
SetInitialFileSizeBytes();
}
/// <summary>Размер файла, МБ (целое, округление).</summary>
public int FileSizeMb
{
get => _fileSizeMb;
private set
{
if (_fileSizeMb == value)
{
return;
}
_fileSizeMb = value;
OnPropertyChanged();
}
}
private void SetInitialFileSizeBytes()
{
try
{
if (File.Exists(FullPath))
{
var len = new FileInfo(FullPath).Length;
FileSizeMb = (int)Math.Round(len / 1024.0 / 1024.0, MidpointRounding.AwayFromZero);
}
}
catch
{
FileSizeMb = 0;
}
}
public void RefreshFileSizeFromDisk() => SetInitialFileSizeBytes();
/// <summary>Восстановление медиаданных из снимка (загрузка очереди) без затрагивания <see cref="IsManuallyEdited"/>.</summary>
public void RestorePersistedMediaSnapshot(
MediaAnalysisResult media,
IReadOnlyList<SidecarFile> sidecars,
IReadOnlyList<ExternalAudioFile> externalAudioFiles,
bool hasFfprobeAudioSummary,
int ffprobeAudioCount,
int? ffprobeAudioSizeMb,
bool ffprobeAudioSizePartial)
{
MediaAnalysis = media;
Sidecars = sidecars;
ExternalAudioFiles = externalAudioFiles;
if (hasFfprobeAudioSummary)
{
SetFfprobeAudioData(ffprobeAudioCount, ffprobeAudioSizeMb, ffprobeAudioSizePartial);
}
else
{
_ffprobeAnalyzed = false;
_ffprobeAudioCount = 0;
_ffprobeAudioSizeMb = null;
_ffprobeAudioSizePartial = false;
OnPropertyChanged(nameof(AudioTracksDisplay));
OnPropertyChanged(nameof(AudioTracksSortValue));
OnPropertyChanged(nameof(AudioSizeMbDisplay));
OnPropertyChanged(nameof(AudioSizeSortValue));
OnPropertyChanged(nameof(AudioSizeMbToolTip));
OnPropertyChanged(nameof(HasFfprobeAudioSummary));
}
}
/// <summary>Есть ли сохранённые краткие данные ffprobe по аудио (для .conv_setup).</summary>
public bool HasFfprobeAudioSummary => _ffprobeAnalyzed;
/// <summary>Число аудиопотоков из последнего анализа (0, если не анализировалось).</summary>
public int FfprobeEmbeddedAudioStreamCount => _ffprobeAnalyzed ? _ffprobeAudioCount : 0;
public int? FfprobeAudioSizeEstimateMb => _ffprobeAudioSizeMb;
public bool FfprobeAudioSizeEstimatePartial => _ffprobeAudioSizePartial;
public string AudioTracksDisplay => !_ffprobeAnalyzed ? "-" : _ffprobeAudioCount.ToString();
public int AudioTracksSortValue => _ffprobeAnalyzed ? _ffprobeAudioCount : -1;
public string AudioSizeMbDisplay
{
get
{
if (!_ffprobeAnalyzed)
{
return "-";
}
if (_ffprobeAudioSizeMb is not int audioMb)
{
return "-";
}
return _ffprobeAudioSizePartial ? $"{audioMb}*" : audioMb.ToString();
}
}
public string TrackSummaryDisplay
{
get
{
if (MediaAnalysis is null)
{
return "-";
}
var videoTotal = MediaAnalysis.VideoStreams.Count;
var audioTotal = MediaAnalysis.AudioStreams.Count;
var subtitleTotal = MediaAnalysis.SubtitleStreams.Count;
var attachmentTotal = MediaAnalysis.AllStreams.Count(s => s.Kind == MediaStreamKind.Attachment);
var overrides = TaskOverride.TrackOverrides;
if (overrides.Count > 0)
{
foreach (var track in overrides)
{
if (track.StreamKind == MediaStreamKind.Video)
{
if (track.Source == SourceKind.Embedded && track.Action == TrackActionKind.Remove)
{
videoTotal--;
}
else if (track.Source == SourceKind.External && track.Action == TrackActionKind.Add)
{
videoTotal++;
}
}
else if (track.StreamKind == MediaStreamKind.Audio)
{
if (track.Source == SourceKind.Embedded && track.Action == TrackActionKind.Remove)
{
audioTotal--;
}
else if (track.Source == SourceKind.External && track.Action == TrackActionKind.Add)
{
audioTotal++;
}
}
else if (track.StreamKind == MediaStreamKind.Subtitle)
{
if (track.Source == SourceKind.Embedded && track.Action == TrackActionKind.Remove)
{
subtitleTotal--;
}
else if (track.Source == SourceKind.External && track.Action == TrackActionKind.Add)
{
subtitleTotal++;
}
}
else if (track.StreamKind == MediaStreamKind.Attachment)
{
if (track.Source == SourceKind.Embedded && track.Action == TrackActionKind.Remove)
{
attachmentTotal--;
}
else if (track.Source == SourceKind.External && track.Action == TrackActionKind.Add)
{
attachmentTotal++;
}
}
}
}
if (videoTotal < 0)
{
videoTotal = 0;
}
if (audioTotal < 0)
{
audioTotal = 0;
}
if (subtitleTotal < 0)
{
subtitleTotal = 0;
}
if (attachmentTotal < 0)
{
attachmentTotal = 0;
}
return $"🎬 {videoTotal} 🔊 {audioTotal} 💬 {subtitleTotal} 📎 {attachmentTotal}";
}
}
public int AudioSizeSortValue => _ffprobeAnalyzed && _ffprobeAudioSizeMb is { } mb ? mb : -1;
/// <summary>Подсказка для «размер аудио» с пометкой * о частичном расчёте.</summary>
public string? AudioSizeMbToolTip
{
get
{
if (_ffprobeAnalyzed && _ffprobeAudioSizePartial)
{
return "* расчет частичный, у части дорожек неизвестен bitrate";
}
if (_ffprobeAnalyzed && _ffprobeAudioSizeMb is not null && _ffprobeAudioSizeMb >= 0)
{
return "Суммарная оценка: длительность × битрейт по дорожкам, МБ (целое).";
}
return null;
}
}
public void SetFfprobeAudioData(int trackCount, int? audioSizeMb, bool isPartial)
{
_ffprobeAnalyzed = true;
_ffprobeAudioCount = trackCount;
_ffprobeAudioSizeMb = audioSizeMb;
_ffprobeAudioSizePartial = isPartial;
OnPropertyChanged(nameof(AudioTracksDisplay));
OnPropertyChanged(nameof(AudioTracksSortValue));
OnPropertyChanged(nameof(AudioSizeMbDisplay));
OnPropertyChanged(nameof(AudioSizeSortValue));
OnPropertyChanged(nameof(AudioSizeMbToolTip));
OnPropertyChanged(nameof(HasFfprobeAudioSummary));
}
public void SetSuccessfulMediaAnalysis(
MediaAnalysisResult media,
IReadOnlyList<SidecarFile> sidecars,
IReadOnlyList<ExternalAudioFile> externalAudioFiles,
ConversionPlan plan,
int audioCount,
int? audioSizeMb,
bool audioSizePartial)
{
MediaAnalysis = media;
Sidecars = sidecars;
ExternalAudioFiles = externalAudioFiles;
LastPlan = plan;
PlanSummary = string.IsNullOrWhiteSpace(plan.ShortSummary) ? "Skip — обработка не требуется" : plan.ShortSummary;
IsManuallyEdited = false;
RecomputeSkipFlag();
SetFfprobeAudioData(audioCount, audioSizeMb, audioSizePartial);
}
public void SetPlan(ConversionPlan plan)
{
LastPlan = plan;
PlanSummary = string.IsNullOrWhiteSpace(plan.ShortSummary) ? "Skip — обработка не требуется" : plan.ShortSummary;
RecomputeSkipFlag();
OnPropertyChanged(nameof(TrackSummaryDisplay));
}
public int OrderNumber
{
get => _orderNumber;
set
{
if (_orderNumber == value)
{
return;
}
_orderNumber = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayIndexText));
}
}
public string Status
{
get => _status;
set
{
if (_status == value)
{
return;
}
_status = value;
OnPropertyChanged();
OnPropertyChanged(nameof(StatusSortOrder));
OnPropertyChanged(nameof(DisplayProgressPercent));
}
}
public int StatusSortOrder => _status switch
{
ConversionQueueStatus.Pending => 0,
ConversionQueueStatus.Running => 1,
ConversionQueueStatus.Copying => 2,
ConversionQueueStatus.Replacing => 3,
ConversionQueueStatus.Done => 4,
ConversionQueueStatus.Error => 5,
ConversionQueueStatus.Cancelled => 6,
_ => 99
};
/// <summary>Сырое значение 0100 для пайплайна (ffmpeg/копирование); в UI использовать <see cref="DisplayProgressPercent"/>.</summary>
public int Progress
{
get => _progress;
set
{
if (_progress == value)
{
return;
}
_progress = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayProgressPercent));
}
}
/// <summary>Прогресс для интерфейса: 100% только при статусе «Готово»; иначе не выше 99.</summary>
public int DisplayProgressPercent =>
string.Equals(_status, ConversionQueueStatus.Done, StringComparison.Ordinal)
? 100
: Math.Min(99, Math.Max(0, _progress));
public string Profile
{
get => _profile;
set
{
if (_profile == value)
{
return;
}
_profile = value;
EffectiveProfileSettings = null;
OnPropertyChanged();
}
}
public string PlanSummary
{
get => _planSummary;
set
{
if (_planSummary == value)
{
return;
}
_planSummary = value;
OnPropertyChanged();
}
}
public bool IsSkipPlan
{
get => _isSkipPlan;
set
{
if (_isSkipPlan == value)
{
return;
}
_isSkipPlan = value;
OnPropertyChanged();
}
}
public bool IsManuallyEdited
{
get => _isManuallyEdited;
set
{
if (_isManuallyEdited == value)
{
return;
}
_isManuallyEdited = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayIndexText));
OnPropertyChanged(nameof(ManualEditToolTip));
RecomputeSkipFlag();
}
}
public string DisplayIndexText => IsManuallyEdited ? $"✎ {OrderNumber}" : OrderNumber.ToString();
public string? ManualEditToolTip => IsManuallyEdited ? "Настройки изменены вручную" : null;
public bool IsProcessed
{
get => _isProcessed;
set
{
if (_isProcessed == value)
{
return;
}
_isProcessed = value;
OnPropertyChanged();
}
}
public bool ProcessedInCurrentRun
{
get => _processedInCurrentRun;
set
{
if (_processedInCurrentRun == value)
{
return;
}
_processedInCurrentRun = value;
OnPropertyChanged();
}
}
public string? LastRunId
{
get => _lastRunId;
set
{
if (_lastRunId == value)
{
return;
}
_lastRunId = value;
OnPropertyChanged();
}
}
public string? ErrorMessage
{
get => _errorMessage;
set
{
if (_errorMessage == value)
{
return;
}
_errorMessage = value;
OnPropertyChanged();
}
}
/// <summary>Подробный текст ошибки (stderr ffmpeg/ffprobe и т.д.); для буфера приоритетнее краткого <see cref="ErrorMessage"/>.</summary>
public string? ErrorDetails
{
get => _errorDetails;
set
{
if (_errorDetails == value)
{
return;
}
_errorDetails = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void RecomputeSkipFlag()
{
IsSkipPlan = LastPlan?.HasRealActions is false;
}
public void UpdateOutputPath(string newPath, string? newContainerFormat)
{
var normalized = Path.GetFullPath(newPath);
FullPath = normalized;
FileName = Path.GetFileName(normalized);
DirectoryPath = Path.GetDirectoryName(normalized) ?? string.Empty;
SetInitialFileSizeBytes();
if (MediaAnalysis is { } m)
{
MediaAnalysis = new MediaAnalysisResult
{
ContainerFormat = newContainerFormat ?? m.ContainerFormat,
FormatName = m.FormatName,
FormatBitRateBps = m.FormatBitRateBps,
DurationSeconds = m.DurationSeconds,
VideoStreams = m.VideoStreams,
AudioStreams = m.AudioStreams,
SubtitleStreams = m.SubtitleStreams,
DataStreams = m.DataStreams,
AllStreams = m.AllStreams,
SourceVideoBitrateBps = m.SourceVideoBitrateBps
};
OnPropertyChanged(nameof(TrackSummaryDisplay));
}
}
}