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:
@@ -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}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user