Match subtitles to episodes by plain episode number, not just S04E06

Adds a season-agnostic fallback to subtitle-episode matching so dropped
subtitle files no longer need a full SxxEyy code to be attached to the
right video. Recognises bare numbers (06, 6, 16), explicit markers
without a season (E06, EP06, Episode 06), and numbers cleanly delimited
by separators, including the common anime release style 'Show - 06 -
Title'. Guards against false positives from resolutions (1080p), years,
and decimal-looking audio tags (DD5.1) by requiring clean separator
boundaries and capping bare numbers at a few digits.

Full season+episode codes still take priority and match exactly as
before; the new bare-number matching only kicks in when one side of the
match lacks season info.

Note: dotnet is not reachable in this environment, so the required
Release build validation could not be run before this commit.
This commit is contained in:
2026-07-24 12:10:06 +00:00
parent e24a401ffb
commit 05fbe34e20
5 changed files with 128 additions and 26 deletions
+36 -16
View File
@@ -266,7 +266,7 @@ public partial class MainWindow : FluentWindow
if (matchedSubtitles > 0 || unmatchedSubtitles > 0)
{
StatusText.Text = unmatchedSubtitles > 0
? $"Matched {matchedSubtitles} subtitle(s); {unmatchedSubtitles} couldn't be matched to an episode code (S04E06 style)."
? $"Matched {matchedSubtitles} subtitle(s); {unmatchedSubtitles} couldn't be matched to an episode (S04E06, E06, or plain episode number like 06/6/16)."
: $"Matched {matchedSubtitles} subtitle(s) to their episode.";
}
}
@@ -275,22 +275,32 @@ public partial class MainWindow : FluentWindow
/// <summary>
/// Attempts to attach a dropped subtitle file to whichever queued video(s)
/// share the same episode code (e.g. S04E06). Falls back to the single
/// queued file when there's exactly one and no episode code could be
/// extracted from the subtitle's filename. Returns false if no target
/// could be determined, in which case the file is kept pending until a
/// matching video is dropped later.
/// share the same episode. Prefers a full season+episode code (e.g.
/// S04E06) when the subtitle's filename has one; otherwise falls back to
/// matching by bare episode number alone (e.g. "06", "6", "16", "E06"),
/// season-agnostic. Falls back further to the single queued file when
/// there's exactly one and no episode info at all could be extracted
/// from the subtitle's filename. Returns false if no target could be
/// determined, in which case the file is kept pending until a matching
/// video is dropped later.
/// </summary>
private bool MatchSubtitleToQueue(string subtitlePath)
{
string subFileName = Path.GetFileName(subtitlePath);
string? code = SubtitleMatcher.ExtractEpisodeCode(subFileName);
string? subCode = SubtitleMatcher.ExtractEpisodeCode(subFileName);
int? subEpisodeNumber = subCode == null ? SubtitleMatcher.ExtractEpisodeNumber(subFileName) : null;
List<QueuedFileItem> targets;
if (code != null)
if (subCode != null)
{
targets = _fileQueue
.Where(q => string.Equals(SubtitleMatcher.ExtractEpisodeCode(q.FileName), code, StringComparison.OrdinalIgnoreCase))
.Where(q => string.Equals(SubtitleMatcher.ExtractEpisodeCode(q.FileName), subCode, StringComparison.OrdinalIgnoreCase))
.ToList();
}
else if (subEpisodeNumber != null)
{
targets = _fileQueue
.Where(q => SubtitleMatcher.ExtractEpisodeNumber(q.FileName) == subEpisodeNumber)
.ToList();
}
else if (_fileQueue.Count == 1)
@@ -312,19 +322,29 @@ public partial class MainWindow : FluentWindow
/// <summary>
/// Called whenever a new video is added to the queue: checks the pending
/// (previously unmatched) subtitle files for one sharing this video's
/// episode code, and attaches + removes any matches from the pending list.
/// (previously unmatched) subtitle files for one matching this video's
/// episode — by full season+episode code when both have one, or by bare
/// episode number otherwise — and attaches + removes any matches from
/// the pending list.
/// </summary>
private void MatchPendingSubtitlesToFile(QueuedFileItem item)
{
if (_pendingSubtitles.Count == 0) return;
string? code = SubtitleMatcher.ExtractEpisodeCode(item.FileName);
if (code == null) return;
string? videoCode = SubtitleMatcher.ExtractEpisodeCode(item.FileName);
int? videoEpisodeNumber = SubtitleMatcher.ExtractEpisodeNumber(item.FileName);
if (videoCode == null && videoEpisodeNumber == null) return;
var matches = _pendingSubtitles
.Where(p => string.Equals(SubtitleMatcher.ExtractEpisodeCode(Path.GetFileName(p)), code, StringComparison.OrdinalIgnoreCase))
.ToList();
var matches = _pendingSubtitles.Where(p =>
{
string subName = Path.GetFileName(p);
string? subCode = SubtitleMatcher.ExtractEpisodeCode(subName);
if (subCode != null)
return videoCode != null && string.Equals(subCode, videoCode, StringComparison.OrdinalIgnoreCase);
int? subEpisodeNumber = SubtitleMatcher.ExtractEpisodeNumber(subName);
return subEpisodeNumber != null && subEpisodeNumber == videoEpisodeNumber;
}).ToList();
foreach (var match in matches)
{