From 8b41c43937b7fab80e861d188939be988e520207 Mon Sep 17 00:00:00 2001 From: mithral Date: Mon, 12 Oct 2026 01:41:05 -0400 Subject: [PATCH] Fix the bare-number episode pattern, which never matched in production parseEpisode was handed the file's whole relative path, but the bare-number pattern is anchored at the start of a name. Handed "Show/Season 3/06 - Title.mkv" it could never match, so every file named that way was filed as a standalone film at the library root instead of an episode. Found by running a real scan against a real tree, not by reading the code: the unit test on parseEpisode passed a bare file name, which is exactly the input the call site was not providing. The unit test was right and the caller was wrong, which is a class of bug a helper test cannot see. parseEpisode now gets path.Base(rel), matching episodeTitle two lines below it, so the function really is a function of a file name and its directory as its signature and doc already claimed. Both scan-level cases are added to the tree test, so the call site is now covered and not just the helper. Verified red-green: reverting the call site fails "bare leading number under a Season directory". --- internal/scanner/scan.go | 6 +++++- internal/scanner/scan_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/scanner/scan.go b/internal/scanner/scan.go index a5a7e62..fe4fcfd 100644 --- a/internal/scanner/scan.go +++ b/internal/scanner/scan.go @@ -478,7 +478,11 @@ func buildTVGroups(lib library.Library, found map[string]candidate) map[string]g c := found[rel] dir := path.Dir(rel) - season, episode, isEpisode := parseEpisode(c.relPath, path.Base(dir)) + // The base name, not the relative path: parseEpisode is a function of a + // file name and its directory, and the leading-number pattern is anchored + // at the start. Handed "Show/Season 1/05 - Title.mkv" it would never + // match, which is how the bare-number case was silently missed. + season, episode, isEpisode := parseEpisode(path.Base(rel), path.Base(dir)) if !isEpisode { g := groups[rel] g.key, g.title, g.kind = rel, library.CleanTitle(rel), lib.Kind diff --git a/internal/scanner/scan_test.go b/internal/scanner/scan_test.go index bdbeeae..4110af0 100644 --- a/internal/scanner/scan_test.go +++ b/internal/scanner/scan_test.go @@ -519,6 +519,30 @@ func TestScanParsesEpisodes(t *testing.T) { {kind: "video", path: "Fargo/Season 4/Fargo.S01E01.mkv", title: "Fargo", parent: "Fargo/Season 4", season: 4, episode: 1}, }, }, + { + // A bare leading number, which older rips and anime use. This case + // exists because the pattern is anchored at the start of the name: + // handed the whole relative path it silently never matched, and only + // a scan-level test catches that - a unit test on parseEpisode passes + // a bare file name and cannot see it. + name: "bare leading number under a Season directory", + file: "The Wire/Season 3/06 - Title.mkv", + want: []item{ + {kind: "series", path: "The Wire", title: "The Wire"}, + {kind: "season", path: "The Wire/Season 3", title: "Season 3", parent: "The Wire", season: 3}, + {kind: "video", path: "The Wire/Season 3/06 - Title.mkv", title: "06 - Title", parent: "The Wire/Season 3", season: 3, episode: 6}, + }, + }, + { + // The anime shape: an episode with no season in the name at all. + name: "episode-only marker under a Season directory", + file: "Show/Season 2/E05.mkv", + want: []item{ + {kind: "series", path: "Show", title: "Show"}, + {kind: "season", path: "Show/Season 2", title: "Season 2", parent: "Show", season: 2}, + {kind: "video", path: "Show/Season 2/E05.mkv", title: "E05", parent: "Show/Season 2", season: 2, episode: 5}, + }, + }, } for _, tc := range tests {