emby-toolbox/EmbyToolbox/ViewModels/TrackSettingsRowViewModel.cs
Emby Toolbox 3459204e6f Track settings: show stream kind emojis in Type column (match queue summary).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:40:52 +05:00

316 lines
9.5 KiB
C#

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 = "Встроенная";
}
// Эмодзи как в ячейке «Дорожки» очереди (ConversionQueueItem.TrackSummaryDisplay).
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<TrackActionKind>(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;
/// <summary>Обновить подсказку Details при смене целевого контейнера (teletext + MKV).</summary>
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));
}
/// <summary>Встроенная teletext-субдорожка для выделения строки (MKV copy не поддерживается).</summary>
public bool IsTeletextSubtitle =>
_entry.Source == SourceKind.Embedded
&& _entry.StreamKind == MediaStreamKind.Subtitle
&& SubtitleCodecRules.IsTeletext(_embeddedStream?.CodecName);
public IList<TrackActionKind> 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<TrackActionKind> 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];
}
}