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.
106 lines
5.0 KiB
C#
106 lines
5.0 KiB
C#
using System.IO;
|
|
|
|
namespace Futonizer.Services;
|
|
|
|
/// <summary>
|
|
/// Result of scanning an external subtitle's filename for language and
|
|
/// display hints.
|
|
/// </summary>
|
|
public readonly record struct SubtitleLanguageInfo(string Language, bool Forced, bool HearingImpaired, string TrackName);
|
|
|
|
/// <summary>
|
|
/// Best-effort language/flag detection for external subtitle files, based on
|
|
/// common filename tokens (e.g. "Show.S04E06.eng.srt", "Show.S04E06.forced.srt").
|
|
/// Never throws — falls back to an "und" (undetermined) language with no
|
|
/// track name when nothing recognisable is found.
|
|
/// </summary>
|
|
public static class SubtitleLanguageHelper
|
|
{
|
|
private static readonly Dictionary<string, string> TokenToIso6392 = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["en"] = "eng", ["eng"] = "eng", ["english"] = "eng",
|
|
["ja"] = "jpn", ["jp"] = "jpn", ["jpn"] = "jpn", ["japanese"] = "jpn",
|
|
["es"] = "spa", ["spa"] = "spa", ["spanish"] = "spa", ["esp"] = "spa",
|
|
["fr"] = "fre", ["fre"] = "fre", ["fra"] = "fre", ["french"] = "fre",
|
|
["de"] = "ger", ["ger"] = "ger", ["deu"] = "ger", ["german"] = "ger",
|
|
["it"] = "ita", ["ita"] = "ita", ["italian"] = "ita",
|
|
["pt"] = "por", ["por"] = "por", ["portuguese"] = "por", ["ptbr"] = "por",
|
|
["ru"] = "rus", ["rus"] = "rus", ["russian"] = "rus",
|
|
["zh"] = "chi", ["chi"] = "chi", ["zho"] = "chi", ["chinese"] = "chi", ["cmn"] = "chi", ["yue"] = "chi",
|
|
["ko"] = "kor", ["kor"] = "kor", ["korean"] = "kor",
|
|
["ar"] = "ara", ["ara"] = "ara", ["arabic"] = "ara",
|
|
["nl"] = "dut", ["dut"] = "dut", ["nld"] = "dut", ["dutch"] = "dut",
|
|
["sv"] = "swe", ["swe"] = "swe", ["swedish"] = "swe",
|
|
["no"] = "nor", ["nor"] = "nor", ["norwegian"] = "nor",
|
|
["da"] = "dan", ["dan"] = "dan", ["danish"] = "dan",
|
|
["fi"] = "fin", ["fin"] = "fin", ["finnish"] = "fin",
|
|
["pl"] = "pol", ["pol"] = "pol", ["polish"] = "pol",
|
|
["tr"] = "tur", ["tur"] = "tur", ["turkish"] = "tur",
|
|
["vi"] = "vie", ["vie"] = "vie", ["vietnamese"] = "vie",
|
|
["th"] = "tha", ["tha"] = "tha", ["thai"] = "tha",
|
|
["id"] = "ind", ["ind"] = "ind", ["indonesian"] = "ind",
|
|
["he"] = "heb", ["heb"] = "heb", ["hebrew"] = "heb",
|
|
["hin"] = "hin", ["hindi"] = "hin",
|
|
["cs"] = "cze", ["cze"] = "cze", ["ces"] = "cze", ["czech"] = "cze",
|
|
["el"] = "gre", ["gre"] = "gre", ["ell"] = "gre", ["greek"] = "gre",
|
|
["hu"] = "hun", ["hun"] = "hun", ["hungarian"] = "hun",
|
|
["ro"] = "rum", ["rum"] = "rum", ["ron"] = "rum", ["romanian"] = "rum",
|
|
["uk"] = "ukr", ["ukr"] = "ukr", ["ukrainian"] = "ukr",
|
|
};
|
|
|
|
private static readonly Dictionary<string, string> Iso6392ToDisplayName = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["eng"] = "English", ["jpn"] = "Japanese", ["spa"] = "Spanish", ["fre"] = "French",
|
|
["ger"] = "German", ["ita"] = "Italian", ["por"] = "Portuguese", ["rus"] = "Russian",
|
|
["chi"] = "Chinese", ["kor"] = "Korean", ["ara"] = "Arabic", ["dut"] = "Dutch",
|
|
["swe"] = "Swedish", ["nor"] = "Norwegian", ["dan"] = "Danish", ["fin"] = "Finnish",
|
|
["pol"] = "Polish", ["tur"] = "Turkish", ["vie"] = "Vietnamese", ["tha"] = "Thai",
|
|
["ind"] = "Indonesian", ["heb"] = "Hebrew", ["hin"] = "Hindi", ["cze"] = "Czech",
|
|
["gre"] = "Greek", ["hun"] = "Hungarian", ["rum"] = "Romanian", ["ukr"] = "Ukrainian",
|
|
};
|
|
|
|
private static readonly char[] TokenSeparators = { '.', '_', '-', ' ', '(', ')', '[', ']' };
|
|
|
|
public static SubtitleLanguageInfo Detect(string fileName)
|
|
{
|
|
string stem = Path.GetFileNameWithoutExtension(fileName);
|
|
var tokens = stem.Split(TokenSeparators, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
string language = "und";
|
|
bool forced = false;
|
|
bool hearingImpaired = false;
|
|
|
|
foreach (var token in tokens)
|
|
{
|
|
if (string.Equals(token, "forced", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
forced = true;
|
|
continue;
|
|
}
|
|
if (string.Equals(token, "sdh", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(token, "cc", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
hearingImpaired = true;
|
|
continue;
|
|
}
|
|
if (language == "und" && TokenToIso6392.TryGetValue(token, out var iso))
|
|
language = iso;
|
|
}
|
|
|
|
string trackName = string.Empty;
|
|
if (Iso6392ToDisplayName.TryGetValue(language, out var displayName))
|
|
trackName = displayName;
|
|
|
|
if (forced || hearingImpaired)
|
|
{
|
|
var suffixes = new List<string>();
|
|
if (forced) suffixes.Add("Forced");
|
|
if (hearingImpaired) suffixes.Add("SDH");
|
|
string suffix = $" ({string.Join(", ", suffixes)})";
|
|
trackName = string.IsNullOrEmpty(trackName) ? suffix.Trim(' ', '(', ')') : trackName + suffix;
|
|
}
|
|
|
|
return new SubtitleLanguageInfo(language, forced, hearingImpaired, trackName);
|
|
}
|
|
}
|