aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-30 16:31:44 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-30 16:31:44 +0200
commitb206dc86221d453c699074749f59f1d7cb3d47dc (patch)
tree6db723f22ba49b892f07942ea93d04076d0f72fe
parent4b64ba2513d3985c843167153311436a80474149 (diff)
parent854b76338072979526ce36fbc176c82f6fc5d0bd (diff)
downloadmeshbay-b206dc86221d453c699074749f59f1d7cb3d47dc.tar.gz
Merge branch 'fix/saga-match'
-rw-r--r--docs/mediacenter.md13
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/title_parse.py30
-rw-r--r--packages/meshbay-node/tests/test_title_parse.py17
3 files changed, 49 insertions, 11 deletions
diff --git a/docs/mediacenter.md b/docs/mediacenter.md
index aa1bd03..3a6b733 100644
--- a/docs/mediacenter.md
+++ b/docs/mediacenter.md
@@ -786,6 +786,19 @@ flat-dumped episode always carries a marker, so nothing regresses.
(`Show.2019.308.mkv`) that guessit slots wrongly has no in-UI remedy short of renaming the
file. A "this is a movie / a show" toggle would close that.
+### 10.3 A whole saga matched to its first film (2026-08-30)
+
+Every `Star Wars Episode <N> - <subtitle>` file resolved to the 1977 original (TMDB id 11).
+Cause: `sequel_variants` stripped `Episode <N>` and offered the bare `Star Wars` as a
+candidate query — which matches the 1977 film's `original_title` at ratio 1.0, beating
+PASS 1's correct (but lower-ratio) hit. A franchise's bare name is very often a real,
+different film. **Fixed** (`fix/starwars-saga-match`): when a `Part`/`Episode`/… keyword is
+what carried the index, `sequel_variants` no longer emits the bare base — only
+`<base> <digit>` and `<base> <roman>`. Without a keyword (`Jurassic Park 3`) the bare base
+is still offered. Verified live: Star Wars I–VI now each resolve to their own episode;
+Jurassic Park / Kill Bill / Bond regressions all hold. V12's movie merge, reverted the
+same day, would have compounded this into one card for the saga.
+
## 11. Acceptance before shipping
1. Re-run the §3 validation (real TMDB calls, same corpus, same script
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<base>.+?)(?:\s+(?:" + _PART_KEYWORDS + r"))?"
+ r"^(?P<base>.+?)(?:\s+(?P<kw>" + _PART_KEYWORDS + r"))?"
r"\s+(?P<num>\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 "<Franchise> Episode <N>" 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():