From e6bf7c26deea8e1be38d532977388d53b323b447 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 8 Jun 2026 20:46:40 -0700 Subject: [PATCH] =?UTF-8?q?Watchlist:=20stop=20treating=20different=20deci?= =?UTF-8?q?mal-volume=20albums=20as=20duplicates=20(Sokhi=20=E2=80=94=20th?= =?UTF-8?q?e=20real=20bug)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sokhi's log showed the actual cause: with the wishlist "allow duplicates" toggle on, is_track_missing_from_library skips a track when _albums_likely_match() says the wanted album and a library album are the same — and it was matching albums that differ ONLY by a decimal volume number: [AllowDup] Album match — skipping (wanted: '...Vol.5', library: '...Vol.5.5') [AllowDup] Album match — skipping (wanted: '...Vol.5.5', library: '...Vol.4.5') His character-song CDs share track titles across volumes, so tracks from albums he DOESN'T have got skipped because a same-titled track sits in a similarly-named volume he DOES have — the partially-filled discography never completed. Root cause: _normalize_album_for_match strips the dot ("Vol.5.5" -> "vol 5 5"), and _VOLUME_MARKER_RE captured only a single trailing digit, so Vol.5, Vol.5.5 and Vol.4.5 all reduced to marker "5" and the volume-disagreement guard never fired. Fix: capture the full multi-part number ((\d+(?:\s+\d+)*)) so "5" / "5 5" / "4 5" are distinct and the guard correctly rejects the match. (Not lookback — the log confirms lookback=all was already honored.) Tests: decimal/multi-part volumes (incl. the real CJK names) now block the match; identical decimal volumes + naming-drift cases still match. 111 watchlist tests pass. --- core/watchlist_scanner.py | 9 ++++++++- tests/test_watchlist_album_match.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) 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)