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
+61
View File
@@ -0,0 +1,61 @@
using System.IO;
using System.Text.RegularExpressions;
namespace Futonizer.Services;
/// <summary>
/// Recognises subtitle files by extension and extracts a normalized episode
/// code (e.g. "S04E06") from a filename, used to match dropped subtitle
/// files to the video file with the same episode code.
/// </summary>
public static class SubtitleMatcher
{
/// <summary>
/// Every subtitle container/format extension mkvmerge (and therefore
/// Futonizer) can mux in: SubRip, (Advanced) SubStation Alpha, VobSub,
/// WebVTT, HDMV/PGS, Universal Subtitle Format, SAMI, TTML/DFXP, EBU-STL,
/// MicroDVD/SubViewer (both commonly saved with a plain ".sub" extension),
/// and Kate.
/// </summary>
public static readonly string[] SupportedExtensions =
{
".srt", ".ass", ".ssa", ".sub", ".idx", ".vtt", ".webvtt",
".sup", ".pgs", ".usf", ".smi", ".sami", ".ttml", ".dfxp",
".stl", ".kate",
};
public static bool IsSubtitleFile(string path)
{
string ext = Path.GetExtension(path);
return !string.IsNullOrEmpty(ext)
&& SupportedExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
}
// "S04E06", "S4E6", "S04 E06", "S04-E06", "S04.E06" — the standard scene/anime tagging scheme.
private static readonly Regex SeasonEpisodeRegex =
new(@"S(\d{1,4})[\s._-]*E(\d{1,4})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Fallback: "4x06" style season x episode separator.
private static readonly Regex SeasonXEpisodeRegex =
new(@"(?<![\d.])(\d{1,2})x(\d{2,3})(?!\d)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Extracts and normalizes the episode code from a filename (e.g.
/// "Show.S4E6.1080p.mkv" and "Show - 04x06 - Title.srt" both return
/// "S04E06"), so files can be matched regardless of zero-padding or
/// separator style. Returns null if no episode code is found.
/// </summary>
public static string? ExtractEpisodeCode(string fileName)
{
var match = SeasonEpisodeRegex.Match(fileName);
if (!match.Success)
match = SeasonXEpisodeRegex.Match(fileName);
if (!match.Success) return null;
if (!int.TryParse(match.Groups[1].Value, out int season)) return null;
if (!int.TryParse(match.Groups[2].Value, out int episode)) return null;
return $"S{season:D2}E{episode:D2}";
}
}