using System.Collections.Generic; using System.Linq; using System.ComponentModel; using System.Runtime.CompilerServices; using EmbyToolbox.Models; using EmbyToolbox.Services; namespace EmbyToolbox.ViewModels; public interface ITrackPlanPreviewHost { void RecalculatePlanPreview(); void OnTrackDefaultEnabled(TrackSettingsRowViewModel row); void ValidateDefaultConflicts(); } public sealed class TrackSettingsRowViewModel : INotifyPropertyChanged { private readonly ITrackPlanPreviewHost _parent; private readonly TrackOverrideEntry _entry; private readonly MediaStreamInfo? _embeddedStream; private string _details; private TrackActionKind _action; private bool _hasDefaultConflict; public TrackSettingsRowViewModel( ITrackPlanPreviewHost parent, TrackOverrideEntry entry, int displayIndex, MediaStreamInfo? embedded, string? targetContainer) { _parent = parent; _entry = entry; _embeddedStream = embedded; _action = entry.Action; IndexDisplay = displayIndex; if (entry.Source == SourceKind.External) { SourceDisplay = "Внешняя"; if (entry.StreamKind == MediaStreamKind.Subtitle) { entry.Language = "rus"; } else if (string.IsNullOrEmpty(entry.Language)) { entry.Language = "rus"; } } else { SourceDisplay = "Встроенная"; } TypeDisplay = entry.StreamKind switch { MediaStreamKind.Video => "Video", MediaStreamKind.Audio => "Audio", MediaStreamKind.Subtitle => "Subtitle", MediaStreamKind.Attachment => "Attachment", MediaStreamKind.Data => "Data", _ => "?" }; Codec = entry.StreamKind == MediaStreamKind.Subtitle && embedded is not null && SubtitleCodecRules.IsTeletext(embedded.CodecName) ? "dvb_teletext" : embedded?.CodecName ?? (entry.Source == SourceKind.External ? (entry.StreamKind == MediaStreamKind.Attachment ? "font" : entry.StreamKind == MediaStreamKind.Audio && !string.IsNullOrWhiteSpace(entry.ExternalStreamCodec) ? entry.ExternalStreamCodec : (System.IO.Path.GetExtension(entry.ExternalPath ?? string.Empty) is { Length: > 0 } e ? e : "?")) : "?"); _details = BuildDetails(entry, embedded, targetContainer); ValidActions = new List(GetValidActions(entry)); if (!ValidActions.Contains(_action) && ValidActions.Count > 0) { _action = ValidActions[0]; _entry.Action = _action; } } public TrackOverrideEntry DataModel => _entry; public int IndexDisplay { get; } public string SourceDisplay { get; } public string TypeDisplay { get; } public string Codec { get; } public string Details => _details; /// Обновить подсказку Details при смене целевого контейнера (teletext + MKV). public void RefreshSubtitleDetails(string? targetContainer) { if (_entry is not { Source: SourceKind.Embedded, StreamKind: MediaStreamKind.Subtitle }) { return; } var next = BuildDetails(_entry, _embeddedStream, targetContainer); if (_details == next) { return; } _details = next; OnPropertyChanged(nameof(Details)); } /// Встроенная teletext-субдорожка для выделения строки (MKV copy не поддерживается). public bool IsTeletextSubtitle => _entry.Source == SourceKind.Embedded && _entry.StreamKind == MediaStreamKind.Subtitle && SubtitleCodecRules.IsTeletext(_embeddedStream?.CodecName); public IList ValidActions { get; } public TrackActionKind Action { get => _action; set { if (_action == value) { return; } _action = value; _entry.Action = value; if (value == TrackActionKind.Remove) { _entry.Default = false; OnPropertyChanged(nameof(Default)); } OnPropertyChanged(); OnPropertyChanged(nameof(IsAudioBitrateVisible)); OnPropertyChanged(nameof(IsDefaultEnabled)); _parent.ValidateDefaultConflicts(); _parent.RecalculatePlanPreview(); } } public string? Language { get => _entry.Language; set { if (_entry.Language == value) { return; } _entry.Language = value; OnPropertyChanged(); _parent.RecalculatePlanPreview(); } } public string? Title { get => _entry.Title; set { if (_entry.Title == value) { return; } _entry.Title = value; OnPropertyChanged(); _parent.RecalculatePlanPreview(); } } public bool? Default { get => _entry.Default; set { if (_entry.Default == value) { return; } _entry.Default = value; OnPropertyChanged(); if (value is true) { _parent.OnTrackDefaultEnabled(this); } _parent.ValidateDefaultConflicts(); _parent.RecalculatePlanPreview(); } } public bool IsDefaultEnabled => (_entry.StreamKind is MediaStreamKind.Audio or MediaStreamKind.Subtitle) && _action != TrackActionKind.Remove; public bool HasDefaultConflict { get => _hasDefaultConflict; set { if (_hasDefaultConflict == value) { return; } _hasDefaultConflict = value; OnPropertyChanged(); } } public string? AudioBitrateKbps { get => _entry.AudioBitrateKbps; set { if (_entry.AudioBitrateKbps == value) { return; } _entry.AudioBitrateKbps = value; OnPropertyChanged(); _parent.RecalculatePlanPreview(); } } public bool IsAudioBitrateVisible => _entry.StreamKind == MediaStreamKind.Audio && ((_entry.Source == SourceKind.Embedded && _action == TrackActionKind.Convert) || (_entry.Source == SourceKind.External && _action == TrackActionKind.Add && !string.IsNullOrWhiteSpace(_entry.AudioBitrateKbps))); public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private static string FormatBps(long? bps) => bps is { } b ? $"{(b + 500) / 1000} kbps" : "?"; private static string BuildDetails(TrackOverrideEntry entry, MediaStreamInfo? embedded, string? targetContainer) { if (entry.Source == SourceKind.External) { if (!string.IsNullOrWhiteSpace(entry.ExternalStreamDetails)) { return entry.ExternalStreamDetails; } return entry.ExternalPath ?? string.Empty; } if (embedded is { Kind: MediaStreamKind.Subtitle } sub) { if (SubtitleCodecRules.IsTeletext(sub.CodecName) && SubtitleCodecRules.TargetsMkv(targetContainer)) { return "unsupported for MKV copy"; } return sub.SubtitleFormat ?? sub.CodecName; } return embedded switch { { Kind: MediaStreamKind.Video } v => $"{v.Width?.ToString() ?? "?"}x{v.Height?.ToString() ?? "?"}; {(v.FrameRate is { } fr ? fr.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture) : "?")} fps; {v.PixelFormat ?? "?"}", { Kind: MediaStreamKind.Audio } a => $"{a.Channels} ch; {a.SampleRateHz} Hz; {FormatBps(a.BitRateBps)}", _ => string.Empty }; } private static IReadOnlyList GetValidActions(TrackOverrideEntry e) { if (e.Source == SourceKind.External) { return [TrackActionKind.Add, TrackActionKind.Remove]; } if (e.StreamKind is MediaStreamKind.Data) { return [TrackActionKind.Remove, TrackActionKind.Keep]; } if (e.StreamKind is MediaStreamKind.Attachment) { return [TrackActionKind.Remove, TrackActionKind.Keep]; } if (e.StreamKind is MediaStreamKind.Video) { return [TrackActionKind.Keep, TrackActionKind.Convert, TrackActionKind.Remove]; } if (e.StreamKind is MediaStreamKind.Subtitle) { return [TrackActionKind.Keep, TrackActionKind.Convert, TrackActionKind.Remove]; } if (e.StreamKind is MediaStreamKind.Audio) { return [TrackActionKind.Keep, TrackActionKind.Convert, TrackActionKind.Remove]; } return [TrackActionKind.Keep, TrackActionKind.Remove]; } }