Fix watchlist scan false failures, Spotify backfill, and wishlist remove
Watchlist scanner: empty discography (no new releases in lookback) was treated as API failure, causing "Failed to get artist discography" for artists like Kendrick Lamar who simply had no recent releases. Now distinguishes None (API failure → try next source) from [] (success, no new tracks). Spotify backfill now uses the authenticated client instance instead of creating a fresh unauthenticated one. Wishlist nebula: album remove now sends album_name (API updated to accept album_name as fallback alongside album_id). Track remove re-renders the nebula after deletion. Toned down processing pulse animation. Updated test to verify fallback triggers on API failure (None), not on empty results.
This commit is contained in:
parent
f59c564382
commit
09d358ef69
4 changed files with 40 additions and 10 deletions
|
|
@ -653,7 +653,10 @@ class WatchlistScanner:
|
|||
last_scan_timestamp,
|
||||
lookback_days=watchlist_artist.lookback_days,
|
||||
)
|
||||
if not albums:
|
||||
# albums can be None (API failure) or empty list (no new releases).
|
||||
# None means this source failed — try next source.
|
||||
# Empty list means success — artist has no new releases in the lookback window.
|
||||
if albums is None:
|
||||
return None
|
||||
|
||||
image_url = self._get_artist_image_for_source(watchlist_artist, source, client, artist_id)
|
||||
|
|
@ -1410,8 +1413,11 @@ class WatchlistScanner:
|
|||
_skip['max_pages'] = _max_pages
|
||||
albums = client.get_artist_albums(artist_id, album_type='album,single', limit=50, **_skip)
|
||||
|
||||
if albums is None:
|
||||
logger.warning(f"API failure fetching albums for artist {artist_id}")
|
||||
return None
|
||||
if not albums:
|
||||
logger.warning(f"No albums found for artist {artist_id}")
|
||||
logger.debug(f"No albums found for artist {artist_id}")
|
||||
return []
|
||||
|
||||
# Add small delay after fetching artist discography to be extra safe
|
||||
|
|
@ -1581,7 +1587,11 @@ class WatchlistScanner:
|
|||
def _match_to_spotify(self, artist_name: str) -> Optional[str]:
|
||||
"""Match artist name to Spotify ID using fuzzy name comparison."""
|
||||
try:
|
||||
client = get_client_for_source('spotify')
|
||||
# Use the authenticated spotify_client passed to the scanner,
|
||||
# not get_client_for_source which creates a fresh unauthenticated instance
|
||||
client = self.spotify_client
|
||||
if not client or not client.is_spotify_authenticated():
|
||||
client = get_client_for_source('spotify')
|
||||
if not client:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -419,12 +419,15 @@ def test_get_artist_discography_for_watchlist_prefers_primary_source(monkeypatch
|
|||
assert spotify_client.album_calls == []
|
||||
|
||||
|
||||
def test_get_artist_discography_for_watchlist_falls_back_when_primary_empty(monkeypatch):
|
||||
def test_get_artist_discography_for_watchlist_falls_back_when_primary_fails(monkeypatch):
|
||||
"""When the primary source API fails (returns None), fall back to next source."""
|
||||
monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
|
||||
monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
|
||||
monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])
|
||||
|
||||
# Deezer client returns None from get_artist_albums (API failure)
|
||||
deezer_client = _FakeSourceClient(artist_id="dz-artist", albums=[], image_url="https://example.com/deezer.jpg")
|
||||
deezer_client.get_artist_albums = lambda *args, **kwargs: None # Simulate API failure
|
||||
spotify_album = types.SimpleNamespace(id="sp-album", name="Spotify Album", release_date=None)
|
||||
spotify_client = _FakeSourceClient(artist_id="sp-artist", albums=[spotify_album], image_url="https://example.com/spotify.jpg")
|
||||
|
||||
|
|
@ -448,7 +451,7 @@ def test_get_artist_discography_for_watchlist_falls_back_when_primary_empty(monk
|
|||
assert result.source == "spotify"
|
||||
assert result.artist_id == "sp-artist"
|
||||
assert result.albums and result.albums[0].id == "sp-album"
|
||||
assert deezer_client.album_calls
|
||||
# Spotify client should have been called as fallback
|
||||
assert spotify_client.album_calls
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25024,9 +25024,10 @@ def remove_album_from_wishlist():
|
|||
|
||||
data = request.get_json()
|
||||
album_id = data.get('album_id')
|
||||
album_name_filter = data.get('album_name')
|
||||
|
||||
if not album_id:
|
||||
return jsonify({"success": False, "error": "No album_id provided"}), 400
|
||||
if not album_id and not album_name_filter:
|
||||
return jsonify({"success": False, "error": "No album_id or album_name provided"}), 400
|
||||
|
||||
wishlist_service = get_wishlist_service()
|
||||
all_tracks = wishlist_service.get_wishlist_tracks_for_download(profile_id=get_current_profile_id())
|
||||
|
|
@ -25065,8 +25066,18 @@ def remove_album_from_wishlist():
|
|||
track_album_id = re.sub(r'[^a-zA-Z0-9\s_-]', '', custom_id) # Remove special chars
|
||||
track_album_id = re.sub(r'\s+', '_', track_album_id).lower() # Replace spaces & lowercase
|
||||
|
||||
# Match by album ID
|
||||
if track_album_id == album_id:
|
||||
# Match by album ID or album name
|
||||
matched = False
|
||||
if album_id and track_album_id == album_id:
|
||||
matched = True
|
||||
elif album_name_filter:
|
||||
track_album_name = album_data.get('name', '')
|
||||
if isinstance(spotify_data.get('album'), str):
|
||||
track_album_name = spotify_data['album']
|
||||
if track_album_name and track_album_name.lower().strip() == album_name_filter.lower().strip():
|
||||
matched = True
|
||||
|
||||
if matched:
|
||||
spotify_track_id = track.get('spotify_track_id') or track.get('id')
|
||||
if spotify_track_id:
|
||||
tracks_to_remove.append(spotify_track_id)
|
||||
|
|
|
|||
|
|
@ -40711,7 +40711,13 @@ async function _removeWishlistTrack(trackId) {
|
|||
try {
|
||||
const res = await fetch('/api/wishlist/remove-track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ spotify_track_id: trackId }) });
|
||||
const data = await res.json();
|
||||
if (data.success) { const p = document.querySelector(`.wl-single-pill[data-track-id="${trackId}"]`); if (p) p.remove(); showToast('Removed', 'success'); await updateWishlistCount(); }
|
||||
if (data.success) {
|
||||
showToast('Removed', 'success');
|
||||
await updateWishlistCount();
|
||||
// Re-render nebula to reflect removal
|
||||
wishlistPageState.isInitialized = false;
|
||||
await initializeWishlistPage();
|
||||
}
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue