Watchlist: stop treating different decimal-volume albums as duplicates (Sokhi — the real bug)

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.
This commit is contained in:
BoulderBadgeDad 2026-06-08 20:46:40 -07:00
parent 13dca2dea6
commit e6bf7c26de
2 changed files with 37 additions and 1 deletions

View file

@ -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,
)

View file

@ -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)