summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_tmdb_search_ladder.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_tmdb_search_ladder.py')
-rw-r--r--packages/meshbay-node/tests/test_tmdb_search_ladder.py193
1 files changed, 193 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_tmdb_search_ladder.py b/packages/meshbay-node/tests/test_tmdb_search_ladder.py
new file mode 100644
index 0000000..49b71bd
--- /dev/null
+++ b/packages/meshbay-node/tests/test_tmdb_search_ladder.py
@@ -0,0 +1,193 @@
+"""
+`WebRTCPeerSession._tmdb_search` — the movie retry ladder.
+
+Regression cover for a batch of wrong poster-grid matches found live on a
+real library (2026-08-29), all one mechanism: the ladder used to return the
+first candidate query whose title-similarity ratio merely cleared 0.6, so a
+wrong film that scored ~0.7 against guessit's weak bare title won before
+`alternative_title` or the Roman-numeral variant was ever tried.
+
+ * A two-volume film's second part matched the *first* — guessit peels
+ "Volume 2" into its own field, collapsing both parts onto one query,
+ and the more popular first part is TMDB's top result for both.
+ * A numbered sequel matched a same-year making-of documentary — TMDB's
+ real entry uses a Roman numeral, so the "3" query surfaces the doc.
+ * Two entries of one franchise matched a single early entry whose
+ *localized* TMDB title is itself the parsed franchise name.
+ * One entry matched nothing — every candidate query missed on text (the
+ filename's spelling of the subtitle differs from TMDB's by one letter).
+
+The canned `(result, ratio)` tuples stand in for what the live TMDB API +
+the real `_best_match` return for each query; the ratios are the ones those
+queries actually produced when the mechanism was traced against the API.
+"""
+
+import pytest
+from meshbay_common.protocol import IndexEntry
+from meshbay_node.indexer.title_parse import naive_title, parse_movie_filename
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+
+pytestmark = pytest.mark.asyncio
+
+
+# Stand-ins for real TMDB rows: a two-part film, a numbered sequel vs a
+# same-year documentary, and a franchise whose localized lead entry's title
+# is the franchise name.
+PART1 = {"id": "101", "title": "Some Saga: Volume 1", "release_date": "2003-10-10"}
+PART2 = {"id": "102", "title": "Some Saga: Volume 2", "release_date": "2004-04-16"}
+DOC = {"id": "201", "title": "Beyond Old Frontier", "release_date": "2001-11-01"}
+SEQUEL3 = {"id": "202", "title": "Old Frontier III", "release_date": "2001-07-18"}
+FRANCHISE_LEAD = {"id": "301", "title": "Some Agent 42 vs. Doctor X",
+ "release_date": "1962-10-05"}
+ENTRY_2002 = {"id": "302", "title": "Second Errand", "release_date": "2002-11-20"}
+ENTRY_1997 = {"id": "303", "title": "First Errand", "release_date": "1997-12-12"}
+STANDALONE = {"id": "401", "title": "A Quiet Film", "release_date": "2010-07-15"}
+
+
+class LadderTmdb:
+ """Canned `(result, ratio)` keyed by `(query, year)` — a hit for
+ `(query, None)` also answers a year-constrained lookup, mirroring the
+ real ladder's unconstrained retry."""
+
+ def __init__(self, table: dict):
+ self._table = table
+ self.calls: list[tuple] = []
+
+ async def search_movie(self, title, year=None):
+ self.calls.append((title, year))
+ if (title, year) in self._table:
+ return self._table[(title, year)]
+ if (title, None) in self._table:
+ return self._table[(title, None)]
+ return None, 0.0
+
+ async def search_tv(self, title):
+ self.calls.append(("tv", title))
+ return None, 0.0
+
+
+async def _run(name: str, table: dict):
+ """Drive the real `_tmdb_search` for a movie filename, deriving
+ `display_title` exactly as enrich.py would from the current parser."""
+ parsed = parse_movie_filename(name)
+ entry = IndexEntry(
+ id="x", name=name, path="movies", size=1, type="video", added_at=0,
+ display_title=parsed.display_title or parsed.naive_title,
+ )
+ client = LadderTmdb(table)
+ session = WebRTCPeerSession.__new__(WebRTCPeerSession)
+ result, ratio = await session._tmdb_search(client, entry, is_show=False)
+ return result, ratio, client
+
+
+# ── a two-part film: the parts must resolve to different entries ────────────
+
+async def test_second_volume_resolves_to_the_second_volume():
+ result, ratio, client = await _run(
+ "Some.Saga.Volume.2.2004.mkv",
+ {("Some Saga 2", 2004): (PART2, 0.92)},
+ )
+ assert result["id"] == "102"
+ # a strong first hit still costs exactly one request
+ assert client.calls == [("Some Saga 2", 2004)]
+
+
+async def test_first_volume_still_resolves_to_the_first_volume():
+ result, _, _ = await _run(
+ "Some.Saga.Volume.1.2003.mkv",
+ {("Some Saga 1", 2003): (PART1, 0.92)},
+ )
+ assert result["id"] == "101"
+
+
+# ── numbered sequel: the Roman-numeral variant beats a same-year documentary ─
+
+async def test_numbered_sequel_prefers_the_real_film_over_a_documentary():
+ naive = naive_title("Old.Frontier.3.2001.mkv")
+ result, _, _ = await _run(
+ "Old.Frontier.3.2001.mkv",
+ {
+ ("Old Frontier 3", 2001): (DOC, 0.80), # weak bare-title hit
+ ("Old Frontier", 2001): (DOC, 0.76),
+ ("Old Frontier III", 2001): (SEQUEL3, 1.0), # the sequel variant
+ (naive, 2001): (DOC, 0.50),
+ },
+ )
+ assert result["id"] == "202"
+
+
+# ── franchise: alternative_title beats the localized franchise name ────────
+
+async def test_franchise_entry_uses_its_subtitle_not_the_franchise_name():
+ result, _, _ = await _run(
+ "Some.Agent.42.-.2002.-.Second.Errand.mkv",
+ {
+ ("Some Agent 42", 2002): (FRANCHISE_LEAD, 0.70), # localized lead, popularity #1
+ ("Second Errand", 2002): (ENTRY_2002, 1.0),
+ },
+ )
+ assert result["id"] == "302"
+
+
+async def test_franchise_alternative_title_still_wins_over_the_localized_lead():
+ # PASS 1 lands on the localized lead at 0.70 (not strong enough to
+ # short-circuit); the ladder must go on to try the subtitle.
+ result, _, _ = await _run(
+ "Some.Agent.42.-.1995.-.Third.Errand.mkv",
+ {
+ ("Some Agent 42", 1995): (FRANCHISE_LEAD, 0.70),
+ ("Third Errand", 1995): ({"id": "305", "title": "Third Errand",
+ "release_date": "1995-11-16"}, 1.0),
+ },
+ )
+ assert result["id"] == "305"
+
+
+# ── the year-exact rescue: every candidate query misses on text ────────────
+
+async def test_rescued_by_exact_release_year_when_every_candidate_misses():
+ # PASS 1's own top hit IS the right film (TMDB year-filtered the search)
+ # but scores far below 0.6 against the bare franchise title; the
+ # alt-title and naive queries all miss because the filename spells the
+ # subtitle differently from TMDB.
+ result, ratio, _ = await _run(
+ "Some.Agent.42.1997.First.Errand.mkv",
+ {("Some Agent 42", 1997): (ENTRY_1997, 0.20)},
+ )
+ assert result["id"] == "303"
+ # rescued to exactly the confidence floor — _do_media_meta_request keeps
+ # a match at ratio >= 0.6, rejects one below it.
+ assert ratio >= 0.6
+
+
+async def test_year_rescue_does_not_fire_without_a_year_match():
+ # Same weak PASS 1, but the top hit's year does NOT match the filename's
+ # → no rescue, stays sub-0.6, and _do_media_meta_request reports
+ # confidence 0 rather than pinning a wrong film.
+ result, ratio, _ = await _run(
+ "Some.Agent.42.1997.First.Errand.mkv",
+ {("Some Agent 42", 1997): ({"id": "999", "title": "An Old Film",
+ "release_date": "1962-01-01"}, 0.20)},
+ )
+ assert ratio < 0.6
+
+
+# ── the ladder must not let a weaker later candidate override a good hit ────
+
+async def test_a_strong_first_hit_is_not_overridden_by_a_weaker_variant():
+ result, _, _ = await _run(
+ "The.Thing.-.2011.-.Wrong.Subtitle.mkv",
+ {
+ ("The Thing", 2011): ({"id": "1", "title": "The Thing",
+ "release_date": "2011-10-14"}, 0.80),
+ ("Wrong Subtitle", 2011): ({"id": "999", "title": "Wrong Subtitle",
+ "release_date": "2011-01-01"}, 0.75),
+ },
+ )
+ assert result["id"] == "1"
+
+
+async def test_strong_direct_match_costs_a_single_request():
+ _, _, client = await _run("A.Quiet.Film.2010.mkv",
+ {("A Quiet Film", 2010): (STANDALONE, 1.0)})
+ assert client.calls == [("A Quiet Film", 2010)]