Match and mux in dropped external subtitles

Drag in subtitle files alongside .mkv files and they're matched to
the right video by episode code (S04E06, zero-padding and separator
insensitive, plus 4x06 as a fallback) and muxed in as extra subtitle
tracks. Any subtitle format mkvmerge accepts is recognized: SRT,
(S)SA, VobSub, WebVTT, PGS/SUP, USF, SAMI, TTML/DFXP, STL, Kate.

Matching works regardless of drop order — subtitles dropped before
their video are held pending until a matching video shows up, and
vice versa. Language, forced, and SDH flags are guessed from common
filename tokens (e.g. "eng", "forced", "sdh") to set the muxed
track's language/name/flags; unmatched subtitles are reported in the
status bar.
This commit is contained in:
2026-07-08 10:37:56 +02:00
parent 304d9ad047
commit c7c125d40b
9 changed files with 511 additions and 21 deletions
+27 -1
View File
@@ -43,6 +43,19 @@ public class QueuedFileItem : INotifyPropertyChanged
public string FileSizeDisplay { get; }
public ObservableCollection<TrackItem> Tracks { get; } = new();
/// <summary>
/// External subtitle files (dropped separately and matched to this file by
/// episode code, e.g. S04E06) that will be muxed in as additional
/// subtitle tracks when this file is processed.
/// </summary>
public ObservableCollection<ExternalSubtitleTrack> ExternalSubtitles { get; } = new();
public bool HasExternalSubtitles => ExternalSubtitles.Count > 0;
public string ExternalSubtitlesTooltip => ExternalSubtitles.Count == 0
? string.Empty
: "External subtitles to mux in:\n" + string.Join("\n", ExternalSubtitles.Select(s => $"- {s.FileName} ({s.LanguageDisplay})"));
public string Status
{
get => _status;
@@ -71,7 +84,12 @@ public class QueuedFileItem : INotifyPropertyChanged
int audioSelected = Tracks.Count(t => t.Type == "audio" && t.Copy);
int subtitleSelected = Tracks.Count(t => t.Type == "subtitles" && t.Copy);
return audioSelected == 1 && subtitleSelected == 1
// A subtitle requirement is met either by picking exactly one
// internal track, or by having at least one matched external
// subtitle file (which can also be combined with one internal track).
bool subtitleOk = subtitleSelected == 1 || (subtitleSelected == 0 && ExternalSubtitles.Count > 0);
return audioSelected == 1 && subtitleOk
? FileLoadState.Correct
: FileLoadState.Incorrect;
}
@@ -87,6 +105,7 @@ public class QueuedFileItem : INotifyPropertyChanged
FilePath = filePath;
FileSizeDisplay = FormatFileSize(filePath);
Tracks.CollectionChanged += OnTracksChanged;
ExternalSubtitles.CollectionChanged += OnExternalSubtitlesChanged;
}
private static string FormatFileSize(string filePath)
@@ -112,6 +131,13 @@ public class QueuedFileItem : INotifyPropertyChanged
}
}
private void OnExternalSubtitlesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasExternalSubtitles));
OnPropertyChanged(nameof(ExternalSubtitlesTooltip));
NotifyLoadState();
}
private void OnTracksChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)