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:
parent
4a1b3d0627
commit
377254572b
2 changed files with 64 additions and 5 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
50
tests/test_watchlist_itunes_match.py
Normal file
50
tests/test_watchlist_itunes_match.py
Normal 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
|
||||
Loading…
Reference in a new issue