diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index e2a345aa..239cfba0 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -354,8 +354,15 @@ def _normalize_album_for_match(name: str) -> str: return cleaned +# Capture the FULL trailing number, including multi-part / decimal volumes. +# Normalization strips the dot in "Vol.5.5" to "vol 5 5", so without the +# `(?:\s+\d+)*` the extractor grabbed only the last "5" — making "Vol.5", +# "Vol.5.5" and "Vol.4.5" all look like volume "5" and collapse together. That +# made the watchlist treat tracks from different character-song CD volumes as +# duplicates and skip them (Sokhi: partially-filled discography never completed). _VOLUME_MARKER_RE = re.compile( - r'\b(?:vol(?:ume)?|pt|part|disc|book|chapter|episode)\.?\s*(\d+)\b|\b(\d+)\s*$', + r'\b(?:vol(?:ume)?|pt|part|disc|book|chapter|episode)\.?\s*(\d+(?:\s+\d+)*)\b' + r'|\b(\d+(?:\s+\d+)*)\s*$', re.IGNORECASE, ) diff --git a/tests/test_watchlist_album_match.py b/tests/test_watchlist_album_match.py index 7cb17a7d..cbe9fbf2 100644 --- a/tests/test_watchlist_album_match.py +++ b/tests/test_watchlist_album_match.py @@ -239,3 +239,32 @@ def test_agreeing_volume_markers_still_match(spotify_name, lib_name) -> None: """Same volume marker should NOT block a match that other rules accept.""" assert _albums_likely_match(spotify_name, lib_name) + + +@pytest.mark.parametrize( + "spotify_name,lib_name", + [ + # Decimal / multi-part volume numbers must be distinguished — the dot is + # stripped to a space in normalization, and grabbing only the last digit + # made these collapse to the same marker (Sokhi: character-song CDs). + ("Character CD Vol.5", "Character CD Vol.5.5"), + ("Character CD Vol.5.5", "Character CD Vol.4.5"), + ("Anime OST Vol.1.5", "Anime OST Vol.2.5"), + # The real CJK album names from the report. + ("TVアニメ「【推しの子】」キャラクターソングCD Vol.5", + "TVアニメ「【推しの子】」キャラクターソングCD Vol.5.5"), + ], +) +def test_decimal_volume_markers_block_match(spotify_name, lib_name) -> None: + assert not _albums_likely_match(spotify_name, lib_name) + + +@pytest.mark.parametrize( + "spotify_name,lib_name", + [ + ("Character CD Vol.5.5", "Character CD Vol.5.5"), # identical decimal vol + ("Character CD Vol.5.5", "Character CD Vol.5.5 (Deluxe)"), + ], +) +def test_same_decimal_volume_still_matches(spotify_name, lib_name) -> None: + assert _albums_likely_match(spotify_name, lib_name)