From 09d358ef693045ef037f5238a78c344587944f9c Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:06:45 -0700 Subject: [PATCH] Fix watchlist scan false failures, Spotify backfill, and wishlist remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/watchlist_scanner.py | 16 +++++++++++++--- tests/test_watchlist_scanner_scan.py | 7 +++++-- web_server.py | 19 +++++++++++++++---- webui/static/script.js | 8 +++++++- 4 files changed, 40 insertions(+), 10 deletions(-) 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'); } }