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".
This commit is contained in:
mithral 2026-10-12 01:41:05 -04:00
parent 6545240430
commit 8b41c43937
2 changed files with 29 additions and 1 deletions

View file

@ -478,7 +478,11 @@ func buildTVGroups(lib library.Library, found map[string]candidate) map[string]g
c := found[rel] c := found[rel]
dir := path.Dir(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 { if !isEpisode {
g := groups[rel] g := groups[rel]
g.key, g.title, g.kind = rel, library.CleanTitle(rel), lib.Kind g.key, g.title, g.kind = rel, library.CleanTitle(rel), lib.Kind

View file

@ -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}, {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 { for _, tc := range tests {