diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-26 15:51:10 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-26 15:51:10 +0200 |
| commit | b1543faad87fd865f8746711ab5f10ac4f331e8b (patch) | |
| tree | 37bca053cc875973c27dcdbbfed9bfb65f50c2f4 /packages/meshbay-node/tests | |
| parent | 84aa73e89cfe486c62317f761dcf87d7845eb4f7 (diff) | |
| download | meshbay-b1543faad87fd865f8746711ab5f10ac4f331e8b.tar.gz | |
fix(video): group every episode under the show's own folder, not a per-file guessit title
The mechanism itself was wrong, not just the TMDB matching: a bare episode
numbering convention with no show name in the filename at all
(`001 Episode's Own Title.ext`, no SxxExx, no show prefix — entirely
ordinary on its own) makes guessit invent a "title" from whatever text
follows the number. That text is the individual episode's own name, and
differs for every episode in the folder — trusting it, as the code did,
groups nothing together at all: every episode became its own single-
episode "show", searched against TMDB by that one-off title alone.
Whenever a season-like ancestor folder exists (a numbered season, or
Specials/Bonus/Extras -> season 0), its own root folder now names the show
unconditionally — never a per-file guessit title, which cannot tell a
show's real name from an individual episode's one-off name when the
filename carries no reliable ShowName/SxxExx structure. The walk continues
past *every* consecutive season-like ancestor, not just the first: a
per-season Bonus folder (Show/Season N/Bonus/file.ext) is nested two
levels inside the show, both "Bonus" and "Season N" season-like on their
own, and stopping at the first would hand back "Season N" as the show's
name instead of "Show".
Also adds "livre" ("book") to the season-word vocabulary (§3.4) — some
shows number their seasons that way (Roman numerals) rather than
"season"/"saison".
Supersedes _title_from_show_siblings from the previous commit (removed):
that fallback assumed a per-file title could still be trusted often
enough to be worth borrowing from a sibling season folder — this fix
means it never needs trusting in the first place once a season-like
ancestor exists.
Verified directly against the real library this was found on, not just
synthetic tests: every sample file (a numbered episode, a Book/Bonus
feature, a Book-root "making of") now resolves to the show's real name.
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_enrich.py | 113 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_title_parse.py | 7 |
2 files changed, 76 insertions, 44 deletions
diff --git a/packages/meshbay-node/tests/test_enrich.py b/packages/meshbay-node/tests/test_enrich.py index 5497812..8c0c6b2 100644 --- a/packages/meshbay-node/tests/test_enrich.py +++ b/packages/meshbay-node/tests/test_enrich.py @@ -9,8 +9,8 @@ import pytest from meshbay_common.protocol import IndexEntry from meshbay_node.indexer.enrich import ( - Enricher, _season_from_ancestors, _synthetic_episode_number, - _title_from_show_siblings, _title_from_siblings, + Enricher, _season_and_show_from_ancestors, _synthetic_episode_number, + _title_from_siblings, ) from meshbay_node.media_cache import MediaCache @@ -19,22 +19,48 @@ _HAVE_FFMPEG = shutil.which("ffmpeg") and shutil.which("ffprobe") # ── pure helpers, no ffmpeg needed ─────────────────────────────────────────── -def test_season_from_ancestors_finds_season_folder(tmp_path): +def test_season_and_show_from_ancestors_finds_season_folder(tmp_path): folder = tmp_path / "Some Show" / "Season 2" folder.mkdir(parents=True) ep = folder / "01 - Episode Title.mkv" ep.touch() - assert _season_from_ancestors(ep) == 2 + assert _season_and_show_from_ancestors(ep) == (2, "Some Show") -def test_season_from_ancestors_none_when_no_season_folder(tmp_path): +def test_season_and_show_from_ancestors_none_when_no_season_folder(tmp_path): folder = tmp_path / "Movies" folder.mkdir() f = folder / "Some Movie 2015.mkv" f.touch() - assert _season_from_ancestors(f) is None + assert _season_and_show_from_ancestors(f) is None + + +def test_season_and_show_from_ancestors_walks_past_a_per_season_bonus_folder(tmp_path): + # The real shape this guards: a Specials/Bonus folder nested *inside* + # each numbered season folder (Show/Season N/Bonus/file.ext) rather + # than once at the show's top level — both "Bonus" and "Season N" are + # season-like on their own, and stopping at the first (innermost) + # would hand back "Season N" as the show's name instead of "Show". + folder = tmp_path / "Some Show" / "Season 1" / "Bonus" + folder.mkdir(parents=True) + f = folder / "One-Off Bonus Title.mkv" + f.touch() + + assert _season_and_show_from_ancestors(f) == (0, "Some Show") + + +def test_season_and_show_from_ancestors_book_word_season_folder(tmp_path): + # Same shape as the real bug this whole fix guards, with the "book" + # season vocabulary (§3.4) instead of "season"/"saison": a plain + # numbered-episode folder nested under it. + folder = tmp_path / "Some Show" / "Livre I" / "Episodes" + folder.mkdir(parents=True) + f = folder / "001 Episode's Own Title.mkv" + f.touch() + + assert _season_and_show_from_ancestors(f) == (1, "Some Show") def test_title_from_siblings_borrows_from_a_titled_sibling(tmp_path): @@ -67,44 +93,6 @@ def test_title_from_siblings_ignores_a_titled_sibling_with_no_episode_number(tmp assert _title_from_siblings(folder / "Bonus Feature Two.mkv") is None -def test_title_from_show_siblings_borrows_from_a_sibling_season_folder(tmp_path): - show = tmp_path / "Some Show" - specials = show / "Specials" - specials.mkdir(parents=True) - (specials / "Bonus Feature One.mkv").touch() - season1 = show / "Season 1" - season1.mkdir() - (season1 / "Some Show.S01E01.mkv").touch() - - assert _title_from_show_siblings(specials / "Bonus Feature One.mkv") == "Some Show" - - -def test_title_from_show_siblings_none_when_no_show_folder_has_one(tmp_path): - show = tmp_path / "Some Show" - specials = show / "Specials" - specials.mkdir(parents=True) - (specials / "Bonus Feature One.mkv").touch() - (specials / "Bonus Feature Two.mkv").touch() - - assert _title_from_show_siblings(specials / "Bonus Feature One.mkv") is None - - -def test_title_from_show_siblings_not_specific_to_the_word_specials(tmp_path): - # Genericity check: the same gap (a season-like folder whose files carry - # no episode grammar of their own) can show up under a plain numbered - # season folder too, not only one named "Specials" — nothing in - # _title_from_show_siblings should assume otherwise. - show = tmp_path / "Some Show" - season2 = show / "Season 2" - season2.mkdir(parents=True) - (season2 / "One-Off Episode Title.mkv").touch() - season1 = show / "Season 1" - season1.mkdir() - (season1 / "Some Show.S01E01.mkv").touch() - - assert _title_from_show_siblings(season2 / "One-Off Episode Title.mkv") == "Some Show" - - def test_synthetic_episode_number_is_stable_alphabetical_rank(tmp_path): folder = tmp_path / "Specials" folder.mkdir() @@ -298,3 +286,40 @@ async def test_enricher_handles_specials_folder_with_no_episode_grammar(tmp_path assert fields["display_title"] == "Some Show" assert fields["season"] == 0 assert fields["episode"] is not None + + +@pytestmark_ffmpeg +@pytest.mark.asyncio +async def test_enricher_groups_bare_numbered_episodes_under_the_show_folder(tmp_path, media_cache): + """ + The real-world shape this whole fix is for: every episode (and every + Bonus feature) named "<number> <its own one-off title>", with the + show's name appearing nowhere in any filename at all — only in the + show's own root folder. guessit still finds an episode number here + (unlike the plain-Specials case above), and invents a "title" from + whatever text follows it — a different one for every file. Trusting + that per-file title, as the code used to, groups nothing together at + all: every episode becomes its own single-episode "show", searched + against TMDB by its own one-off title, and mismatched to whichever + unrelated real film or show happens to share it. + """ + show = tmp_path / "Some Show" + season1_eps = show / "Livre I" / "Episodes" + season1_eps.mkdir(parents=True) + (season1_eps / "001 First Episode's Own Title.mkv").touch() + clip = season1_eps / "002 Second Episode's Own Title.mkv" + _make_clip(clip) + season2_eps = show / "Livre II" / "Episodes" + season2_eps.mkdir(parents=True) + (season2_eps / "001 A Season 2 Episode's Own Title.mkv").touch() + entry = IndexEntry(id="fileid4", name=clip.name, path=str(clip.relative_to(tmp_path)), + size=clip.stat().st_size, type="video", added_at=0) + + enricher = Enricher(media_cache) + _, fields = await _run(enricher, entry, clip) + + # Not "Second Episode's Own Title" — every episode has a different + # one, and none of them is the show. + assert fields["display_title"] == "Some Show" + assert fields["season"] == 1 + assert fields["episode"] == 2 diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py index 1a277f2..1881613 100644 --- a/packages/meshbay-node/tests/test_title_parse.py +++ b/packages/meshbay-node/tests/test_title_parse.py @@ -111,6 +111,13 @@ def test_season_folder_roman_numeral(): assert season_from_folder_name("Saison IV") == 4 +def test_season_folder_book_word_roman_numeral(): + # Some shows number their seasons by "book" (Roman numerals) rather + # than "season"/"saison" — real, observed vocabulary (§3.4). + assert season_from_folder_name("Livre I") == 1 + assert season_from_folder_name("Livre VI") == 6 + + def test_specials_folder_maps_to_season_zero(): assert season_from_folder_name("Specials") == 0 assert season_from_folder_name("Bonus") == 0 |