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));
}
}
}