diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index 6977f73b..02326956 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -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 diff --git a/tests/test_watchlist_scanner_scan.py b/tests/test_watchlist_scanner_scan.py index 74011cb3..5cc12222 100644 --- a/tests/test_watchlist_scanner_scan.py +++ b/tests/test_watchlist_scanner_scan.py @@ -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 diff --git a/web_server.py b/web_server.py index 17d80479..f2b9b2c9 100644 --- a/web_server.py +++ b/web_server.py @@ -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) diff --git a/webui/static/script.js b/webui/static/script.js index ecc63560..4c370c9a 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -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'); } }