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; /// True после успешного ffprobe (аудио-поля валидны для отображения). private bool _ffprobeAnalyzed; private int _ffprobeAudioCount; private int? _ffprobeAudioSizeMb; private bool _ffprobeAudioSizePartial; private MediaAnalysisResult? _mediaAnalysis; private IReadOnlyList _sidecars = System.Array.Empty(); private IReadOnlyList _externalAudioFiles = System.Array.Empty(); 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(); } } /// Параметры из ffprobe + списки потоков. public MediaAnalysisResult? MediaAnalysis { get => _mediaAnalysis; private set { if (ReferenceEquals(_mediaAnalysis, value)) { return; } _mediaAnalysis = value; OnPropertyChanged(); OnPropertyChanged(nameof(TrackSummaryDisplay)); } } public IReadOnlyList Sidecars { get => _sidecars; private set { if (Equals(_sidecars, value)) { return; } _sidecars = value; OnPropertyChanged(); } } /// Общий корень батча (добавление каталогом или вычисленный LCA файлов при перетаскивании/мультовыборе). Для области snapshot между эпизодами одного добавления. public string? SnapshotScopeBatchRoot { get; set; } /// Разбор внешних аудиофайлов (мультипотоковые контейнеры и т.д.) для пере-seed дорожек. public IReadOnlyList ExternalAudioFiles { get => _externalAudioFiles; private set { if (Equals(_externalAudioFiles, value)) { return; } _externalAudioFiles = value; OnPropertyChanged(); } } public ConversionTaskOverride TaskOverride { get; } = new(); 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(); } /// Размер файла, МБ (целое, округление). 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(); /// Восстановление медиаданных из снимка (загрузка очереди) без затрагивания . public void RestorePersistedMediaSnapshot( MediaAnalysisResult media, IReadOnlyList sidecars, IReadOnlyList 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)); } } /// Есть ли сохранённые краткие данные ffprobe по аудио (для .conv_setup). public bool HasFfprobeAudioSummary => _ffprobeAnalyzed; /// Число аудиопотоков из последнего анализа (0, если не анализировалось). 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; /// Подсказка для «размер аудио» с пометкой * о частичном расчёте. 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 sidecars, IReadOnlyList 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 }; /// Сырое значение 0–100 для пайплайна (ffmpeg/копирование); в UI использовать . public int Progress { get => _progress; set { if (_progress == value) { return; } _progress = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayProgressPercent)); } } /// Прогресс для интерфейса: 100% только при статусе «Готово»; иначе не выше 99. 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; 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(); } } /// Подробный текст ошибки (stderr ffmpeg/ffprobe и т.д.); для буфера приоритетнее краткого . 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)); } } }