diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index ee6128f1..0ce55e89 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -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}") diff --git a/tests/test_watchlist_itunes_match.py b/tests/test_watchlist_itunes_match.py new file mode 100644 index 00000000..5f86886f --- /dev/null +++ b/tests/test_watchlist_itunes_match.py @@ -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