diff --git a/core/text/title_match.py b/core/text/title_match.py index 36818fcd..8d1d1080 100644 --- a/core/text/title_match.py +++ b/core/text/title_match.py @@ -193,9 +193,18 @@ def numeric_tokens_differ(title_a: str, title_b: str) -> bool: string similarity ('Vol.4' vs 'Vol.4.5' = 0.97) and token-subset checks both wave these through, which hung volume 4.5's cover art on volume 4 (Sokhi). Shared digits on both sides ('1989' vs '1989 (Deluxe)') are - fine.""" + fine. + + Tokenises on non-word runs but KEEPS word characters of every script, so a + digit glued to a non-latin word stays its own digit-bearing token. Stripping + to [a-z0-9] turned CJK into spaces, collapsing 'サウンドトラック2' to a bare + '2' that a shared number elsewhere ('第2期' = season 2) already covered — so + 'Soundtrack' and 'Soundtrack2' both reduced to {'2'} and matched, hanging the + wrong cover (Sokhi again).""" def _digit_tokens(text: str) -> frozenset: - tokens = re.sub(r"[^a-z0-9]+", " ", (text or "").casefold()).split() + # \W is Unicode-aware for str: CJK/kana count as word chars, so a digit + # stays attached to its word instead of collapsing to a bare '2'. + tokens = re.sub(r"\W+", " ", (text or "").casefold()).split() return frozenset(t for t in tokens if any(c.isdigit() for c in t)) return _digit_tokens(title_a) != _digit_tokens(title_b) diff --git a/tests/matching/test_numeric_release_guard.py b/tests/matching/test_numeric_release_guard.py index b2ba79c8..23639b29 100644 --- a/tests/matching/test_numeric_release_guard.py +++ b/tests/matching/test_numeric_release_guard.py @@ -19,6 +19,12 @@ from core.musicbrainz_service import MusicBrainzService VOL4 = "B小町 - TVアニメ「【推しの子】」キャラクターソングCD Vol.4" VOL45 = "B小町 - TVアニメ「【推しの子】」キャラクターソングCD Vol.4.5" +# Sokhi #2: the sequel number is glued straight onto a CJK word ('…トラック2'), +# with the SAME digit already present elsewhere ('第2期' = season 2). Stripping +# to [a-z0-9] collapsed both titles to {'2'} and the wrong (cour-2) cover won. +OST = "『無職転生 〜異世界行ったら本気だす〜』 第2期 オリジナル・サウンドトラック" +OST2 = "『無職転生 〜異世界行ったら本気だす〜』 第2期 オリジナル・サウンドトラック2" + def test_helper_volume_and_sequel_differ(): assert numeric_tokens_differ(VOL4, VOL45) @@ -26,11 +32,20 @@ def test_helper_volume_and_sequel_differ(): assert numeric_tokens_differ("Now 99", "Now 100") +def test_helper_cjk_trailing_sequel_digit_differs(): + # The trailing '2' must register as a difference even though '第2期' already + # puts a '2' on both sides. + assert numeric_tokens_differ(OST, OST2) + assert numeric_tokens_differ(OST2, OST) + + def test_helper_shared_or_no_digits_match(): assert not numeric_tokens_differ("1989", "1989 (Deluxe)") assert not numeric_tokens_differ(VOL4, VOL4) assert not numeric_tokens_differ("IGOR", "IGOR (Deluxe)") assert not numeric_tokens_differ("", "") + # Same CJK album on both sides (incl. the shared 第2期) still matches. + assert not numeric_tokens_differ(OST, OST) def _service_with_results(results): diff --git a/tests/metadata/test_art_lookup.py b/tests/metadata/test_art_lookup.py index 92a77a82..335974ee 100644 --- a/tests/metadata/test_art_lookup.py +++ b/tests/metadata/test_art_lookup.py @@ -202,6 +202,19 @@ def test_album_matches_rejects_numeric_difference(): assert art_lookup._album_matches("Taylor Swift", "1989", "Taylor Swift", "1989 (Deluxe)") +def test_album_matches_rejects_cjk_trailing_sequel_digit(): + """Sokhi #2: the sequel '2' is glued straight onto a CJK word + ('…サウンドトラック2'), and '第2期' (season 2) already puts a '2' on both + sides — so the digit-strip collapsed both to {'2'} and the cour-2 + soundtrack's cover hung on the base soundtrack.""" + ART = "藤澤慶昌" + OST = "『無職転生 〜異世界行ったら本気だす〜』 第2期 オリジナル・サウンドトラック" + assert not art_lookup._album_matches(ART, OST, ART, OST + "2") + assert not art_lookup._album_matches(ART, OST + "2", ART, OST) + # The genuine base-album hit still matches (incl. its shared 第2期). + assert art_lookup._album_matches(ART, OST, ART, OST) + + # --------------------------------------------------------------------------- # build_art_lookup — caching + guarding # ---------------------------------------------------------------------------