diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-26 16:04:00 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-26 16:04:00 +0200 |
| commit | c81eaa0bd07e360ec03406dfc63267c3013a0319 (patch) | |
| tree | dbce9709954cd640a77356b3cd6b00b632b6023d /packages/meshbay-node/tests | |
| parent | b1543faad87fd865f8746711ab5f10ac4f331e8b (diff) | |
| download | meshbay-c81eaa0bd07e360ec03406dfc63267c3013a0319.tar.gz | |
fix(video): fix two remaining bugs in Specials numbering and long-season episodes
1. A season spanning more than one folder (a per-book Bonus folder nested
inside every numbered season) got the same synthetic episode numbers
handed out again in each folder independently — three unrelated
Specials all showing up as "S0E01". _synthetic_episode_number now ranks
across the whole show for the target season, not just one file's own
folder.
2. guessit reads a bare 3-digit leading episode number as a concatenated
season+episode guess rather than a plain episode number — "100" parses
as season=1, episode=0, not episode=100, with nothing in its output
distinguishing that from a real 2-digit episode. A season-like ancestor
already overrides guessit's season (previous commit); this applies the
same fix to episode via a direct regex on the leading number, capped at
3 digits so a leading year (4 digits) is never misread the same way.
Both confirmed against the real library this whole fix was found on:
per-book Bonus features across two books no longer collide, and a real
100th-episode file now resolves to episode 100 instead of 0.
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_enrich.py | 72 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_title_parse.py | 25 |
2 files changed, 86 insertions, 11 deletions
diff --git a/packages/meshbay-node/tests/test_enrich.py b/packages/meshbay-node/tests/test_enrich.py index 8c0c6b2..4b957cd 100644 --- a/packages/meshbay-node/tests/test_enrich.py +++ b/packages/meshbay-node/tests/test_enrich.py @@ -20,12 +20,13 @@ _HAVE_FFMPEG = shutil.which("ffmpeg") and shutil.which("ffprobe") # ── pure helpers, no ffmpeg needed ─────────────────────────────────────────── def test_season_and_show_from_ancestors_finds_season_folder(tmp_path): - folder = tmp_path / "Some Show" / "Season 2" + show = tmp_path / "Some Show" + folder = show / "Season 2" folder.mkdir(parents=True) ep = folder / "01 - Episode Title.mkv" ep.touch() - assert _season_and_show_from_ancestors(ep) == (2, "Some Show") + assert _season_and_show_from_ancestors(ep) == (2, show) def test_season_and_show_from_ancestors_none_when_no_season_folder(tmp_path): @@ -43,24 +44,26 @@ def test_season_and_show_from_ancestors_walks_past_a_per_season_bonus_folder(tmp # 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" + show = tmp_path / "Some Show" + folder = 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") + assert _season_and_show_from_ancestors(f) == (0, 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" + show = tmp_path / "Some Show" + folder = 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") + assert _season_and_show_from_ancestors(f) == (1, show) def test_title_from_siblings_borrows_from_a_titled_sibling(tmp_path): @@ -94,15 +97,36 @@ def test_title_from_siblings_ignores_a_titled_sibling_with_no_episode_number(tmp def test_synthetic_episode_number_is_stable_alphabetical_rank(tmp_path): - folder = tmp_path / "Specials" - folder.mkdir() + show = tmp_path / "Some Show" + folder = show / "Specials" + folder.mkdir(parents=True) (folder / "Bonus Feature One.mkv").touch() (folder / "Bonus Feature Three.mkv").touch() (folder / "Bonus Feature Two.mkv").touch() - assert _synthetic_episode_number(folder / "Bonus Feature One.mkv") == 1 - assert _synthetic_episode_number(folder / "Bonus Feature Three.mkv") == 2 - assert _synthetic_episode_number(folder / "Bonus Feature Two.mkv") == 3 + assert _synthetic_episode_number(folder / "Bonus Feature One.mkv", show, 0) == 1 + assert _synthetic_episode_number(folder / "Bonus Feature Three.mkv", show, 0) == 2 + assert _synthetic_episode_number(folder / "Bonus Feature Two.mkv", show, 0) == 3 + + +def test_synthetic_episode_number_does_not_collide_across_per_season_bonus_folders(tmp_path): + # The real bug this guards: season 0 spanning more than one folder + # (a Bonus folder nested inside each numbered season) — ranking within + # just one file's own folder hands out "episode 1" again in every + # other one, found live as three unrelated Specials all showing up as + # "S0E01". Ranked across the whole show instead, so each gets its own + # number. + show = tmp_path / "Some Show" + bonus1 = show / "Season 1" / "Bonus" + bonus1.mkdir(parents=True) + (bonus1 / "First Season's Bonus.mkv").touch() + bonus2 = show / "Season 2" / "Bonus" + bonus2.mkdir(parents=True) + (bonus2 / "Second Season's Bonus.mkv").touch() + + n1 = _synthetic_episode_number(bonus1 / "First Season's Bonus.mkv", show, 0) + n2 = _synthetic_episode_number(bonus2 / "Second Season's Bonus.mkv", show, 0) + assert n1 != n2 # ── end-to-end against a real (tiny, synthetic) video file ────────────────── @@ -323,3 +347,29 @@ async def test_enricher_groups_bare_numbered_episodes_under_the_show_folder(tmp_ assert fields["display_title"] == "Some Show" assert fields["season"] == 1 assert fields["episode"] == 2 + + +@pytestmark_ffmpeg +@pytest.mark.asyncio +async def test_enricher_reads_a_three_digit_episode_number_correctly(tmp_path, media_cache): + """ + guessit's own episode number is not trustworthy here: given a bare + leading "100", it reads that as a concatenated season+episode guess + (season=1, episode=0) rather than episode 100 — confirmed live, and + indistinguishable from a real 2-digit episode in its output. The + season-like ancestor already overrides guessit's season; this is the + same fix applied to episode. + """ + show = tmp_path / "Some Show" + season_eps = show / "Season 6" / "Episodes" + season_eps.mkdir(parents=True) + clip = season_eps / "100 A Long Season's Own Title.mkv" + _make_clip(clip) + entry = IndexEntry(id="fileid5", 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) + + assert fields["season"] == 6 + assert fields["episode"] == 100 diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py index 1881613..cf51646 100644 --- a/packages/meshbay-node/tests/test_title_parse.py +++ b/packages/meshbay-node/tests/test_title_parse.py @@ -6,6 +6,7 @@ is a manual acceptance step (§11), not something this repo's corpus holds. from meshbay_node.indexer.title_parse import ( ParsedName, + leading_episode_number, naive_title, parse_episode_filename, parse_movie_filename, @@ -128,6 +129,30 @@ def test_non_season_folder_name_returns_none(): assert season_from_folder_name("Some Show Name") is None +# ── §3.4c: a bare leading episode number, guessit's 3-digit blind spot ─────── + +def test_leading_episode_number_reads_the_whole_number(): + assert leading_episode_number("001 Episode's Own Title.mkv") == 1 + assert leading_episode_number("099 Episode's Own Title.mkv") == 99 + + +def test_leading_episode_number_not_split_by_guessit_at_three_digits(): + # The real bug this guards: guessit itself reads a bare "100" as season=1, + # episode=0 (a concatenated SxxE guess), not episode=100 — silently, with + # nothing in its output telling apart a real 2-digit episode from this. + assert leading_episode_number("100 Episode's Own Title.mkv") == 100 + + +def test_leading_episode_number_ignores_a_leading_year(): + # Four digits, not the 1-3 an episode-number prefix can be — this is a + # normal movie filename shape, not an episode number. + assert leading_episode_number("2010 - Some Movie.mkv") is None + + +def test_leading_episode_number_none_without_a_leading_number(): + assert leading_episode_number("Some Show.S01E01.mkv") is None + + def test_parsed_name_is_a_plain_dataclass(): # sanity: constructible with just the one required field, per the # "None => caller must supply from elsewhere" contract. |