Watchlist: iTunes ID backfill never worked in the normal wiring — use the registry client

Boulder noticed his recently added watchlist artists (June 4 batch) have no
iTunes match while Spotify/Deezer/MusicBrainz matched fine. The rotated log
has the receipt: "Cannot match to iTunes - MetadataService not available" ×8
→ "Backfilled 0/8 artists with itunes IDs", every scan.

_match_to_itunes was the only matcher with no fallback: it read the PRIVATE
_metadata_service attr, which is None whenever the scanner is constructed
from a spotify_client — the normal web_server wiring — and gave up, while a
lazy-loading metadata_service property sat right next to it and the deezer/
discogs/musicbrainz matchers all fall back to their registry clients. Bonus
landmine: even when set, metadata_service.itunes is the FALLBACK-client slot
and may actually be a DeezerClient (per _match_to_deezer's own comment), so
"iTunes" matching could have stored a Deezer artist ID as itunes_artist_id.

Now mirrors the other matchers: canonical registry get_itunes_client().
Self-healing — missing IDs are re-attempted every scan, so existing
watchlists backfill on the next run with no migration needed.

Tests: match works with _metadata_service=None (the exact production
condition), unconfident result returns None, missing client degrades
gracefully. 103 watchlist tests pass.
This commit is contained in:
BoulderBadgeDad 2026-06-07 11:22:03 -07:00
parent 4a1b3d0627
commit 377254572b
2 changed files with 64 additions and 5 deletions

View file

@ -1748,12 +1748,21 @@ class WatchlistScanner:
def _match_to_itunes(self, artist_name: str) -> Optional[str]:
"""Match artist name to iTunes ID using fuzzy name comparison."""
try:
if hasattr(self, '_metadata_service') and self._metadata_service:
results = self._metadata_service.itunes.search_artists(artist_name, limit=5)
else:
logger.warning("Cannot match to iTunes - MetadataService not available")
# Use the canonical iTunes client like _match_to_deezer/_discogs/
# _musicbrainz do. The old path read the PRIVATE _metadata_service
# attr (None when the scanner is built from a spotify_client — the
# normal web_server wiring) and just gave up — the only matcher
# with no fallback — so watchlist iTunes backfills failed wholesale
# ("MetadataService not available" ×8, Backfilled 0/8). And even
# when set, metadata_service.itunes is the FALLBACK-client slot,
# which may actually be a DeezerClient (see _match_to_deezer) —
# matching against it could store a Deezer ID as itunes_artist_id.
from core.metadata.registry import get_itunes_client
client = get_itunes_client()
if client is None:
logger.warning("Cannot match to iTunes - client unavailable")
return None
results = client.search_artists(artist_name, limit=5)
return self._best_artist_match(results, artist_name)
except Exception as e:
logger.warning(f"Could not match {artist_name} to iTunes: {e}")

View file

@ -0,0 +1,50 @@
"""Watchlist iTunes ID backfill (Boulder: recent watchlist artists never got
their iTunes match 'MetadataService not available' ×8, Backfilled 0/8).
_match_to_itunes was the only matcher with no fallback: it read the PRIVATE
_metadata_service attr, which is None in the normal web_server wiring
(scanner built from a spotify_client), and gave up. It must use the canonical
registry iTunes client like the deezer/discogs/musicbrainz matchers do.
"""
from __future__ import annotations
from types import SimpleNamespace
import core.watchlist_scanner as ws
from core.watchlist_scanner import WatchlistScanner
def _scanner():
# The normal web_server wiring: spotify_client only, no MetadataService.
return WatchlistScanner(spotify_client=SimpleNamespace())
def _registry(monkeypatch, results):
client = SimpleNamespace(search_artists=lambda name, limit=5: results)
import core.metadata.registry as registry
monkeypatch.setattr(registry, 'get_itunes_client', lambda *a, **k: client)
return client
def test_itunes_match_works_without_metadata_service(monkeypatch):
s = _scanner()
assert s._metadata_service is None # the exact production condition
_registry(monkeypatch, [SimpleNamespace(name='Green Day', id='it123', popularity=80)])
assert s._match_to_itunes('Green Day') == 'it123'
def test_itunes_match_unconfident_returns_none(monkeypatch):
s = _scanner()
_registry(monkeypatch, [SimpleNamespace(name='Completely Different Band', id='x1', popularity=10)])
assert s._match_to_itunes('Green Day') is None
def test_itunes_match_no_client_returns_none(monkeypatch):
s = _scanner()
import core.metadata.registry as registry
monkeypatch.setattr(registry, 'get_itunes_client', lambda *a, **k: None)
assert s._match_to_itunes('Green Day') is None