From 71b7a310ce938f072fe20f27eeeadd40685f1ad1 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 29 Aug 2026 14:58:30 +0200 Subject: fix(node): correct TMDB movie matching, per-file overrides, rematch A batch of wrong poster-grid matches found live on a real library (2026-08-29): a two-volume film's second part matched the first; a numbered sequel matched a same-year making-of documentary; several entries of one franchise matched a single early entry whose localized TMDB title is the franchise name; one matched nothing. One mechanism: _tmdb_search returned the first candidate query whose title-similarity ratio merely cleared 0.6, before alternative_title / the Roman-numeral variant was ever tried. Matching: - title_parse: fold guessit's volume/part number back into display_title so the parts of a multi-part film stay distinct in the query, the card and the override. - _tmdb_search: keep a strong PASS 1 fast path (ratio >= 0.85, one request), otherwise score every candidate query and pick the best. A year-exact rescue lifts a sub-0.6 top hit to the confidence floor only when TMDB's own year-filtered result lands exactly on the filename's year. No local re-ranking of any single result list; no tmdb.py change. Fix match / rematch: - _admin_exec_tmdb_override: a movie override touches its own file only (guessit gives a whole franchise one display_title); a show override still fans out. Corrected files are marked in media_cache.tmdb_override. - media_cache: tmdb_override table; clear_file_tmdb / clear_tmdb_matches drop auto-resolved matches while sparing manual corrections. - ops.rematch_video + `meshbay-node video rematch` (loopback endpoint + CLI verb): re-resolve a group's video matches after a matcher fix. file_tmdb is keyed by content hash and otherwise only pruned on deletion, so nothing dislodged a cached match before. - a rename now drops the stale auto match too (daemon _reenrich_renamed_video_entries). UI: - VideoDetailModal shows the source filename and resolved TMDB id; an unmatched poster gets a badge (3 new video.* i18n keys x 10 locales). So a wrong match can actually be identified before hitting Fix match. docs/mediacenter.md 10.1 records this and the V8-V13 follow-up backlog (show-branch ladder, year-aware _best_match, wider sequel_variants, the 0.6-0.85 extra calls, movie grid merge, per-card rematch). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v --- packages/meshbay-node/tests/test_cli_dispatch.py | 1 + packages/meshbay-node/tests/test_media_cache.py | 49 ++++++ .../meshbay-node/tests/test_ops_rematch_video.py | 71 ++++++++ .../meshbay-node/tests/test_rename_reenrichment.py | 56 ++++++ packages/meshbay-node/tests/test_title_parse.py | 20 +++ .../tests/test_tmdb_override_policy.py | 68 ++++++++ .../meshbay-node/tests/test_tmdb_search_ladder.py | 193 +++++++++++++++++++++ 7 files changed, 458 insertions(+) create mode 100644 packages/meshbay-node/tests/test_ops_rematch_video.py create mode 100644 packages/meshbay-node/tests/test_tmdb_search_ladder.py (limited to 'packages/meshbay-node/tests') diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py index aff7330..aa966fd 100644 --- a/packages/meshbay-node/tests/test_cli_dispatch.py +++ b/packages/meshbay-node/tests/test_cli_dispatch.py @@ -37,6 +37,7 @@ VERBS = [ ["operator", "pair"], ["file", "list"], ["file", "rm", "abc", "--yes"], + ["video", "rematch", "--yes"], ["denylist", "show"], ["denylist", "clear", "--yes"], ["reload"], diff --git a/packages/meshbay-node/tests/test_media_cache.py b/packages/meshbay-node/tests/test_media_cache.py index e70ef43..4e65b24 100644 --- a/packages/meshbay-node/tests/test_media_cache.py +++ b/packages/meshbay-node/tests/test_media_cache.py @@ -71,6 +71,55 @@ async def test_prune_file_removes_thumb_and_mapping_but_not_shared_meta(cache): assert await cache.get_tmdb_meta("555", "tv") == {"name": "A Show"} +# ── auto-resolved matches vs manual "Fix match" corrections ───────────────── + +@pytest.mark.asyncio +async def test_clear_tmdb_matches_drops_auto_matches_but_keeps_overrides(cache): + await cache.set_file_tmdb("auto1", "10", "movie") + await cache.set_file_tmdb("auto2", "20", "movie") + await cache.set_file_tmdb("fixed", "30", "movie") + await cache.mark_tmdb_override("fixed") + + removed = await cache.clear_tmdb_matches(["auto1", "auto2", "fixed", "never-seen"]) + + assert removed == 2 + assert await cache.get_file_tmdb("auto1") is None + assert await cache.get_file_tmdb("auto2") is None + assert await cache.get_file_tmdb("fixed") == ("30", "movie"), ( + "a manual Fix match correction must survive ops.rematch_video") + + +@pytest.mark.asyncio +async def test_clear_tmdb_matches_empty_list_is_a_noop(cache): + assert await cache.clear_tmdb_matches([]) == 0 + + +@pytest.mark.asyncio +async def test_clear_file_tmdb_drops_one_auto_match_but_keeps_an_override(cache): + await cache.set_file_tmdb("renamed", "10", "movie") + await cache.clear_file_tmdb("renamed") + assert await cache.get_file_tmdb("renamed") is None + + await cache.set_file_tmdb("renamed-fixed", "11", "movie") + await cache.mark_tmdb_override("renamed-fixed") + await cache.clear_file_tmdb("renamed-fixed") + assert await cache.get_file_tmdb("renamed-fixed") == ("11", "movie") + + +@pytest.mark.asyncio +async def test_prune_file_also_clears_the_override_marker(cache): + await cache.set_file_tmdb("gone", "10", "movie") + await cache.mark_tmdb_override("gone") + + await cache.prune_file("gone") + + # the marker must not linger after the file leaves the index and then + # shield a later match on the same id from ops.rematch_video + assert await cache.get_file_tmdb("gone") is None + await cache.set_file_tmdb("gone", "99", "movie") + assert await cache.clear_tmdb_matches(["gone"]) == 1 + + # ── Music app (docs/musicbay.md §6) — file_mbid/mbid_meta ──────────────────── @pytest.mark.asyncio diff --git a/packages/meshbay-node/tests/test_ops_rematch_video.py b/packages/meshbay-node/tests/test_ops_rematch_video.py new file mode 100644 index 0000000..f63a1a9 --- /dev/null +++ b/packages/meshbay-node/tests/test_ops_rematch_video.py @@ -0,0 +1,71 @@ +""" +Tests for ops.rematch_video — drops a group's *auto-resolved* video->TMDB +matches so they re-resolve against the current matcher (run by the operator +after a matcher/parser fix; `media_cache.file_tmdb` is keyed by content +hash and is otherwise only pruned on deletion). Manual "Fix match" +corrections are kept. +""" + +import types + +import pytest +from meshbay_node import ops +from meshbay_node.media_cache import MediaCache + +pytestmark = pytest.mark.asyncio + + +def _indexer(*entries): + return types.SimpleNamespace(index=types.SimpleNamespace(entries=list(entries))) + + +def _entry(file_id, type_="video"): + return types.SimpleNamespace(id=file_id, type=type_) + + +@pytest.fixture +async def media_cache(tmp_path): + c = MediaCache(db_path=tmp_path / "media_cache.db") + await c.open() + yield c + await c.close() + + +async def test_no_media_cache_raises(): + with pytest.raises(ops.OpError): + await ops.rematch_video({}, "g" * 32) + + +async def test_unknown_group_raises(media_cache): + with pytest.raises(ops.OpError): + await ops.rematch_video({"media_cache": media_cache, "indexers": {}}, "nope") + + +async def test_clears_auto_matches_keeps_overrides_ignores_non_video(media_cache): + await media_cache.set_file_tmdb("v-auto-1", "10", "movie") + await media_cache.set_file_tmdb("v-auto-2", "20", "movie") + await media_cache.set_file_tmdb("v-fixed", "30", "movie") + await media_cache.mark_tmdb_override("v-fixed") + await media_cache.set_file_mbid("a-track", "some-mbid") # audio, untouched + + state = { + "media_cache": media_cache, + "indexers": {"g": _indexer( + _entry("v-auto-1"), _entry("v-auto-2"), _entry("v-fixed"), + _entry("a-track", "audio"), + )}, + } + + result = await ops.rematch_video(state, "g") + + assert result == {"status": "cleared", "removed": 2, "videos": 3, "group_id": "g"} + assert await media_cache.get_file_tmdb("v-auto-1") is None + assert await media_cache.get_file_tmdb("v-auto-2") is None + assert await media_cache.get_file_tmdb("v-fixed") == ("30", "movie") + assert await media_cache.get_file_mbid("a-track") == "some-mbid" + + +async def test_group_with_no_videos_is_a_clean_noop(media_cache): + state = {"media_cache": media_cache, "indexers": {"g": _indexer()}} + result = await ops.rematch_video(state, "g") + assert result == {"status": "cleared", "removed": 0, "videos": 0, "group_id": "g"} diff --git a/packages/meshbay-node/tests/test_rename_reenrichment.py b/packages/meshbay-node/tests/test_rename_reenrichment.py index 87e0d1a..7a77368 100644 --- a/packages/meshbay-node/tests/test_rename_reenrichment.py +++ b/packages/meshbay-node/tests/test_rename_reenrichment.py @@ -22,6 +22,7 @@ from meshbay_common.crypto import generate_gek from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer +from meshbay_node.media_cache import MediaCache from conftest import one_root @@ -115,6 +116,61 @@ async def test_a_renamed_file_gets_re_enriched(tmp_path): "from ever being title-parsed") +async def test_a_rename_drops_the_stale_cached_tmdb_match(tmp_path): + """ + A rename re-derives the title, which can change the correct TMDB match + — but media_cache.file_tmdb is keyed by content hash, unchanged by a + rename, so without an explicit clear the old name's match sticks + forever. (An explicit "Fix match" correction is kept — covered by + test_media_cache.py's clear_file_tmdb tests.) + """ + group_id = "a" * 32 + shared = tmp_path / "shared" + shared.mkdir() + old_path = shared / "old.frontier.3.2001.mkv" + old_path.write_bytes(b"not a real video, just needs to be indexed as one") + + config = Config( + hub=HubConfig(url="http://localhost:9999", username="testuser"), + node=NodeConfig(quic_port=_free_port(), ui_port=_free_port()), + groups=[GroupConfig( + id=group_id, name="test-group", shared_dir=str(shared), + visibility="private", quic_port=29018, + )], + keystore=KeystoreConfig(path=tmp_path / "keystore.enc"), + data_dir=tmp_path / "data", + ) + daemon = NodeDaemon(config) + daemon._broadcast_coalesce_secs = 0.01 + daemon._enricher = _SpyEnricher() + daemon._roster = _StubRoster() + media_cache = MediaCache(db_path=tmp_path / "media_cache.db") + await media_cache.open() + daemon._media_cache = media_cache + + indexer = DirectoryIndexer( + roots=one_root(shared), group_id=group_id, + sk_node=Ed25519PrivateKey.generate(), gek=generate_gek(), + on_change=daemon._on_index_change) + await indexer.initial_scan() + await daemon._on_index_change(indexer) + await asyncio.sleep(0.05) + + file_id = next(iter(indexer.index.entries)).id + await media_cache.set_file_tmdb(file_id, "201", "movie") # the wrong match + + old_path.rename(shared / "old.frontier.iii.2001.mkv") + assert await indexer.reconcile() + for _ in range(30): + if await media_cache.get_file_tmdb(file_id) is None: + break + await asyncio.sleep(0.02) + + assert await media_cache.get_file_tmdb(file_id) is None, ( + "a rename must drop the auto-resolved match so it re-resolves") + await media_cache.close() + + async def test_an_unrelated_update_does_not_re_trigger_enrichment(tmp_path): """ The other half of the same fix: an update whose name/path did *not* diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py index 5f0977c..03fb588 100644 --- a/packages/meshbay-node/tests/test_title_parse.py +++ b/packages/meshbay-node/tests/test_title_parse.py @@ -69,6 +69,26 @@ def test_sequel_variants_empty_when_no_trailing_digit(): assert sequel_variants("Some Movie") == [] +# ── bug 2026-08-29: guessit peels "Volume N" off the title ───────────────── + +def test_movie_volume_number_is_folded_back_into_the_title(): + # guessit parses both to title="Some Saga" + volume=1/2; leaving it that + # way collapsed the two parts onto one TMDB match and made "Fix match" + # (grouped by display_title) unable to separate them. + r1 = parse_movie_filename("Some.Saga.Volume.1.2003.mkv") + r2 = parse_movie_filename("Some.Saga.Volume.2.2004.mkv") + assert r1.display_title == "Some Saga 1" + assert r2.display_title == "Some Saga 2" + assert r1.display_title != r2.display_title + assert r1.year == 2003 and r2.year == 2004 + + +def test_movie_without_a_volume_token_is_unchanged(): + # the "3" is already part of guessit's title here, not a volume field + r = parse_movie_filename("Old.Frontier.3.2001.mkv") + assert r.display_title == "Old Frontier 3" + + # ── §3.3 row 6: no usable title at all ─────────────────────────────────────── def test_low_confidence_when_no_title_or_year(): diff --git a/packages/meshbay-node/tests/test_tmdb_override_policy.py b/packages/meshbay-node/tests/test_tmdb_override_policy.py index c2f28e6..0717a0c 100644 --- a/packages/meshbay-node/tests/test_tmdb_override_policy.py +++ b/packages/meshbay-node/tests/test_tmdb_override_policy.py @@ -195,6 +195,74 @@ async def test_override_updates_every_entry_sharing_the_display_title(tmp_path): await media_cache.close() +async def test_movie_override_touches_only_the_one_file(tmp_path): + """ + guessit gives a whole franchise the same display_title (every + ".-..-..mkv" parses to the franchise name), + so fanning a *movie* override out by display_title corrected the wrong + films (found live, 2026-08-29). A movie override applies to its own + file_id only; a show override still fans out (test above). + """ + session = _session(tmp_path, "op", operator="op") + index = session._ctx["index"] + m1 = IndexEntry(id="b" * 64, name="Some.Agent.42.-.2008.-.Second.Errand.mkv", + path="movies", size=1, type="video", added_at=0, + display_title="Some Agent 42") + m2 = IndexEntry(id="c" * 64, name="Some.Agent.42.-.2012.-.Third.Errand.mkv", + path="movies", size=1, type="video", added_at=0, + display_title="Some Agent 42") + index.add_entry(m1) + index.add_entry(m2) + + media_cache = MediaCache(db_path=tmp_path / "media_cache.db") + await media_cache.open() + try: + session._ctx["media_cache"] = media_cache + session._ctx["tmdb_client"] = None + session._verify_admin_sig = lambda transcript, sig: _true() + session._peer_registry = lambda: {} + + await session._admin_exec_tmdb_override( + {"subject": f"file_id={m1.id},tmdb_id=302,media_type=movie"}, + b"transcript", b"sig") + + assert await media_cache.get_file_tmdb(m1.id) == ("302", "movie") + assert await media_cache.get_file_tmdb(m2.id) is None, ( + "a movie override must not fan out to another film sharing the " + "parsed franchise display_title") + # the corrected file is also shielded from a later ops.rematch_video + assert await media_cache.clear_tmdb_matches([m1.id, m2.id]) == 0 + finally: + await media_cache.close() + + +async def test_show_override_fans_out_and_marks_every_corrected_file(tmp_path): + session = _session(tmp_path, "op", operator="op") + index = session._ctx["index"] + s1 = _entry("shared/S1", "s01e01.mkv", "Some Show") + s2 = _entry("shared/S2", "s02e01.mkv", "Some Show") + index.add_entry(s1) + index.add_entry(s2) + + media_cache = MediaCache(db_path=tmp_path / "media_cache.db") + await media_cache.open() + try: + session._ctx["media_cache"] = media_cache + session._ctx["tmdb_client"] = None + session._verify_admin_sig = lambda transcript, sig: _true() + session._peer_registry = lambda: {} + + await session._admin_exec_tmdb_override( + {"subject": f"file_id={s1.id},tmdb_id=2255,media_type=tv"}, + b"transcript", b"sig") + + assert await media_cache.get_file_tmdb(s2.id) == ("2255", "tv") # fan-out + # both episodes are marked, so ops.rematch_video spares the correction + assert await media_cache.clear_tmdb_matches([s1.id, s2.id]) == 0 + finally: + await media_cache.close() + + class _FakeTmdbClient: """Just enough for _tmdb_build_meta to run end to end — a fixed, deterministic response, not a search stub (the override already has a 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)] -- cgit v1.2.3