From 854b76338072979526ce36fbc176c82f6fc5d0bd Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 30 Aug 2026 16:31:34 +0200 Subject: fix(node): stop a numbered saga all matching its first film MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every " Episode - " file in a numbered franchise resolved to the series' first entry (a real, older film). `sequel_variants` stripped "Episode " and offered the bare "" as a candidate query; that matches the first film's original_title at ratio 1.0 and beat PASS 1's correct-but-lower hit. A franchise's bare name is very often a real, different film. When a Part/Episode/Chapitre/… keyword carried the index, sequel_variants no longer emits the bare base — only " " and " ". Without a keyword (" 3") the bare base is still offered, so that fix is untouched. Verified live against TMDB: the franchise's episodes each resolve to their own entry; the earlier numbered-sequel, two-part-film and franchise-subtitle regressions all hold. docs/mediacenter.md §10.3. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v --- .../src/meshbay_node/indexer/title_parse.py | 30 ++++++++++++++++------ packages/meshbay-node/tests/test_title_parse.py | 17 +++++++++--- 2 files changed, 36 insertions(+), 11 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py index def947f..032dc58 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py @@ -129,7 +129,7 @@ def naive_title(filename: str) -> str: _PART_KEYWORDS = r"part|chapter|volume|vol|partie|chapitre|volet|livre|book|episode" _TRAILING_INDEX_RE = re.compile( - r"^(?P.+?)(?:\s+(?:" + _PART_KEYWORDS + r"))?" + r"^(?P.+?)(?:\s+(?P" + _PART_KEYWORDS + r"))?" r"\s+(?P\d{1,2}|[ivxlcdm]{1,6}|" + "|".join(_NUMBER_WORDS) + r")$", re.IGNORECASE, ) @@ -151,8 +151,14 @@ def sequel_variants(title: str) -> list[str]: title: the file has a digit where TMDB uses a Roman numeral (or the reverse), spells the number out, or wraps it as "Part N" / "Chapitre N" (§3.3 row 4, §10.1/V10). Returns extra candidate titles to try — the - base alone, and the index re-rendered as digit and as Roman numeral. - Empty when `title` carries no recognisable trailing index. + index re-rendered as digit and as Roman numeral, plus (only when there + is no "Part"/"Episode"/… keyword) the bare base. + + The bare base is withheld for a keyword'd index — "Star Wars Episode + III" → "Star Wars" — because a franchise's bare name is very often a + real, *different* film (the 1977 original), and that variant matched + every episode to it (§10.1/V14). Without the keyword ("Jurassic Park 3") + the number is decoration and "Jurassic Park" is the right base to try. """ m = _TRAILING_INDEX_RE.match(title.strip()) if not m: @@ -164,12 +170,20 @@ def sequel_variants(title: str) -> list[str]: if n is None: return [] tok = m.group("num").lower() - out = [base] roman = _int_to_roman(n) - if roman and roman.lower() != tok: - out.append(f"{base} {roman}") - if str(n) != tok: - out.append(f"{base} {n}") + if m.group("kw"): + # keyword stripped ("… Part 2" -> base has neither the word nor the + # number), so both renderings are new; the bare base is withheld. + out = [f"{base} {n}"] + if roman: + out.append(f"{base} {roman}") + else: + # "Movie 2" — base already carries `tok`; only offer what differs. + out = [base] + if roman and roman.lower() != tok: + out.append(f"{base} {roman}") + if str(n) != tok: + out.append(f"{base} {n}") return [v for v in dict.fromkeys(out) if v != title] diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py index 045635f..56f4e20 100644 --- a/packages/meshbay-node/tests/test_title_parse.py +++ b/packages/meshbay-node/tests/test_title_parse.py @@ -80,16 +80,27 @@ def test_sequel_variants_roman_numeral_offers_the_digit_form(): assert "Old Frontier 3" in v -def test_sequel_variants_strips_a_part_keyword_wrapper(): +def test_sequel_variants_rewrites_a_part_keyword_index_but_keeps_the_name(): + # §10.1/V14: with a "Part"/"Episode"/… keyword the bare base is + # withheld — "Some Saga" alone collides with a franchise-origin film. v = sequel_variants("Some Saga Part 2") - assert "Some Saga" in v assert "Some Saga II" in v + assert "Some Saga 2" in v + assert "Some Saga" not in v def test_sequel_variants_reads_a_spelled_out_index(): v = sequel_variants("Story Chapter Three") - assert "Story" in v assert "Story 3" in v and "Story III" in v + assert "Story" not in v # keyword present -> no bare base + + +def test_sequel_variants_star_wars_shape_does_not_offer_the_bare_franchise(): + # Every " Episode " was matching the 1977 original + # because the "Franchise" variant hit it at ratio 1.0 (§10.1/V14). + v = sequel_variants("Star Wars Episode III") + assert "Star Wars" not in v + assert "Star Wars 3" in v def test_sequel_variants_ignores_a_trailing_word_that_is_not_an_index(): -- cgit v1.2.3