From 5d28d0c96cc8489645178b483835489779cb3887 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 29 Aug 2026 15:43:38 +0200 Subject: feat(node): V8–V11 — show-branch ladder, year-aware _best_match, wider sequel_variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V8: the TV/show branch of _tmdb_search used the old "first candidate over 0.6 wins" shape. It now shares one _tmdb_ladder helper with the movie branch — score every candidate query, keep the best, fast-path a confident primary hit. A year lifted off the show's folder name (title_parse.year_in, e.g. "Some.Show.2022.S01") rescues a sub-0.6 hit that lands on the exact year. title_parse.clean_query de-dots a folder-derived title without naive_title's extension-stripping trap. V9: _best_match gains an optional `year`. When the top result is not a confident textual hit (ratio < 0.6) and a year was requested, a different result of that exact release year is preferred — TMDB already year-filtered the search, so this is a hard corroboration, not the fuzzy re-rank §3.3 warns against. A confident top hit is never overridden. search_movie/search_tv forward the year. V10: sequel_variants widened — trailing Roman→digit as well as digit→Roman, spelled-out indices (one..twelve / un..douze / ordinals), and a "Part N" / "Chapitre N" wrapper. Still empty for a trailing word that is not an index or a 4-digit year. V11: when the primary hit is already decent (>= 0.6) and there is nothing more specific to try (no alternative_title, no sequel variant — only a punctuation restatement left), the ladder returns without the extra requests. The clean-title common case is back to one call. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v --- .../meshbay-node/tests/test_tmdb_search_ladder.py | 58 +++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) (limited to 'packages/meshbay-node/tests/test_tmdb_search_ladder.py') diff --git a/packages/meshbay-node/tests/test_tmdb_search_ladder.py b/packages/meshbay-node/tests/test_tmdb_search_ladder.py index 49b71bd..9865c77 100644 --- a/packages/meshbay-node/tests/test_tmdb_search_ladder.py +++ b/packages/meshbay-node/tests/test_tmdb_search_ladder.py @@ -61,8 +61,12 @@ class LadderTmdb: return self._table[(title, None)] return None, 0.0 - async def search_tv(self, title): - self.calls.append(("tv", title)) + async def search_tv(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 @@ -80,6 +84,17 @@ async def _run(name: str, table: dict): return result, ratio, client +async def _run_show(display_title: str, name: str, table: dict): + entry = IndexEntry( + id="s", name=name, path="Show/S1", size=1, type="video", added_at=0, + display_title=display_title, season=1, episode=1, + ) + client = LadderTmdb(table) + session = WebRTCPeerSession.__new__(WebRTCPeerSession) + result, ratio = await session._tmdb_search(client, entry, is_show=True) + 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(): @@ -191,3 +206,42 @@ 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)] + + +# ── §10.1/V11: a decent primary hit with nothing more specific to try ────── + +async def test_decent_primary_with_no_stronger_candidate_costs_one_call(): + result, _, client = await _run( + "A.Quiet.Film.2010.mkv", + {("A Quiet Film", 2010): ({"id": "77", "title": "A Quiet Movie", + "release_date": "2010-01-01"}, 0.70)}, + ) + assert result["id"] == "77" + assert client.calls == [("A Quiet Film", 2010)], ( + "no alt title, no sequel index → the punctuation-restatement fallback " + "is not worth a second request once the primary hit is decent") + + +# ── §10.1/V8: the show branch uses the same scored ladder ───────────────── + +async def test_show_scored_ladder_beats_a_weak_primary_hit(): + result, _, _ = await _run_show( + "Some.Show.Name.S01", "s01e01.mkv", + { + ("Some.Show.Name.S01", None): ({"id": "10", "name": "Promo Special", + "first_air_date": "2019-01-01"}, 0.55), + ("Some Show Name S01", None): ({"id": "20", "name": "Some Show Name", + "first_air_date": "2017-01-01"}, 0.95), + }, + ) + assert result["id"] == "20" + + +async def test_show_year_in_folder_name_rescues_a_weak_hit(): + result, ratio, _ = await _run_show( + "Some.Show.2019.S01", "s01e01.mkv", + {("Some.Show.2019.S01", 2019): ({"id": "30", "name": "Some Show", + "first_air_date": "2019-05-01"}, 0.30)}, + ) + assert result["id"] == "30" + assert ratio >= 0.6 -- cgit v1.2.3