summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-29 15:43:38 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-29 15:43:38 +0200
commit5d28d0c96cc8489645178b483835489779cb3887 (patch)
tree90248f1c3211ca37495a774d0bc140109c299abc /packages/meshbay-node/tests
parent236e5e811355212945b89c4f7a99df5c837e97c7 (diff)
downloadmeshbay-5d28d0c96cc8489645178b483835489779cb3887.tar.gz
feat(node): V8–V11 — show-branch ladder, year-aware _best_match, wider sequel_variants
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_title_parse.py44
-rw-r--r--packages/meshbay-node/tests/test_tmdb.py53
-rw-r--r--packages/meshbay-node/tests/test_tmdb_search_ladder.py58
3 files changed, 153 insertions, 2 deletions
diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py
index 03fb588..ac4d434 100644
--- a/packages/meshbay-node/tests/test_title_parse.py
+++ b/packages/meshbay-node/tests/test_title_parse.py
@@ -6,12 +6,14 @@ is a manual acceptance step (§11), not something this repo's corpus holds.
from meshbay_node.indexer.title_parse import (
ParsedName,
+ clean_query,
leading_episode_number,
naive_title,
parse_episode_filename,
parse_movie_filename,
season_from_folder_name,
sequel_variants,
+ year_in,
)
@@ -69,6 +71,48 @@ def test_sequel_variants_empty_when_no_trailing_digit():
assert sequel_variants("Some Movie") == []
+# ── §10.1/V10: wider sequel-index handling ──────────────────────────────────
+
+def test_sequel_variants_roman_numeral_offers_the_digit_form():
+ v = sequel_variants("Old Frontier III")
+ assert "Old Frontier" in v
+ assert "Old Frontier 3" in v
+
+
+def test_sequel_variants_strips_a_part_keyword_wrapper():
+ v = sequel_variants("Some Saga Part 2")
+ assert "Some Saga" in v
+ assert "Some Saga II" 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
+
+
+def test_sequel_variants_ignores_a_trailing_word_that_is_not_an_index():
+ assert sequel_variants("The Dark Knight") == []
+ assert sequel_variants("In Bruges") == []
+
+
+def test_sequel_variants_ignores_a_four_digit_year_suffix():
+ assert sequel_variants("Blade Runner 2049") == []
+
+
+def test_year_in_lifts_a_year_from_a_show_folder_name():
+ assert year_in("Some.Show.2022.S01") == 2022
+ assert year_in("Some Show") is None
+ assert year_in("Episode 100 of 2010") == 2010
+
+
+def test_clean_query_despaces_a_folder_name_without_eating_the_last_word():
+ # naive_title would rsplit on the last dot and drop ".Name"
+ assert clean_query("Some.Show.Name") == "Some Show Name"
+ assert clean_query("Some.Show.Name.S01") == "Some Show Name S01"
+ assert naive_title("Some.Show.Name") != "Some Show Name" # the trap it avoids
+
+
# ── bug 2026-08-29: guessit peels "Volume N" off the title ─────────────────
def test_movie_volume_number_is_folded_back_into_the_title():
diff --git a/packages/meshbay-node/tests/test_tmdb.py b/packages/meshbay-node/tests/test_tmdb.py
index fcbc2a2..ab452ce 100644
--- a/packages/meshbay-node/tests/test_tmdb.py
+++ b/packages/meshbay-node/tests/test_tmdb.py
@@ -40,6 +40,59 @@ async def test_search_movie_returns_top_result_and_confidence():
await client.close()
+# ── §10.1/V9: year-exact preference, only when the top hit is weak ──────────
+
+@pytest.mark.asyncio
+async def test_year_exact_result_wins_when_the_top_hit_is_low_confidence():
+ # results[0] is TMDB's popularity #1 but a poor textual match for the
+ # query; results[2] is the exact requested year.
+ body = {"results": [
+ {"id": 1, "title": "Franchise vs. The Doctor", "release_date": "1962-10-05"},
+ {"id": 2, "title": "Franchise: Goldfinger", "release_date": "1964-09-17"},
+ {"id": 3, "title": "Second Errand", "release_date": "2002-11-20"},
+ ]}
+ client = TmdbClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(_handler({"search/movie": body})),
+ )
+ result, _ = await client.search_movie("The Franchise", 2002)
+
+ assert result["id"] == 3
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_year_is_ignored_when_the_top_hit_is_already_confident():
+ body = {"results": [
+ {"id": 1, "title": "The Franchise", "release_date": "1999-01-01"},
+ {"id": 2, "title": "Unrelated", "release_date": "2002-01-01"},
+ ]}
+ client = TmdbClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(_handler({"search/movie": body})),
+ )
+ result, ratio = await client.search_movie("The Franchise", 2002)
+
+ assert result["id"] == 1 and ratio > 0.9
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_no_year_match_leaves_the_top_result_in_place():
+ body = {"results": [
+ {"id": 1, "title": "Something Else Entirely", "release_date": "1990-01-01"},
+ {"id": 2, "title": "Also Not It", "release_date": "1991-01-01"},
+ ]}
+ client = TmdbClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(_handler({"search/movie": body})),
+ )
+ result, _ = await client.search_movie("The Franchise", 2002)
+
+ assert result["id"] == 1
+ await client.close()
+
+
@pytest.mark.asyncio
async def test_search_tv_returns_top_result():
body = {"results": [{"id": 7, "name": "Some Show"}]}
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