316 lines
9.5 KiB
C#
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];
|
|
}
|
|
}
|