From 71bff55c6aa631f0ee38f4d4126b124462ea6910 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:10:54 -0700 Subject: [PATCH] Fix iTunes album tracks failing on region-restricted releases iTunes API can return collection metadata without song tracks for region-restricted albums. The _lookup fallback only checked if results was empty, so a collection-only response was accepted and cached as {'items': []}. All future lookups returned the cached empty result. Three fixes: - get_album_tracks now checks for actual song items and tries fallback storefronts when only collection metadata is returned - Skip cached results with empty items array (prevents stale cache hits) - Backend returns descriptive 404 error, frontend surfaces it in toast --- core/itunes_client.py | 35 ++++++++++++++++++++++++++++++++++- web_server.py | 4 ++-- webui/static/script.js | 3 ++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/core/itunes_client.py b/core/itunes_client.py index 2c22afcd..0007d1cf 100644 --- a/core/itunes_client.py +++ b/core/itunes_client.py @@ -735,7 +735,7 @@ class iTunesClient: # Check cache for album tracks listing cache = get_metadata_cache() cached = cache.get_entity('itunes', 'album', f"{album_id}_tracks") - if cached: + if cached and cached.get('items'): return cached results = self._lookup(id=album_id, entity='song') @@ -743,6 +743,39 @@ class iTunesClient: if not results: return None + # Check if results contain actual song tracks (not just collection metadata). + # iTunes sometimes returns only the collection/album info without songs + # (region-restricted tracks). The _lookup fallback only checks if results + # is empty, so a collection-only response bypasses fallback storefronts. + has_songs = any( + item.get('wrapperType') == 'track' and item.get('kind') == 'song' + for item in results + ) + if not has_songs: + # Try fallback storefronts for actual song tracks + logger.info(f"Album {album_id} returned collection info but no songs, trying fallback storefronts") + for fallback in self.FALLBACK_COUNTRIES: + if fallback == self.country: + continue + try: + fb_results = self.session.get( + self.LOOKUP_URL, + params={'id': album_id, 'entity': 'song', 'country': fallback}, + timeout=15 + ) + if fb_results.status_code == 200: + fb_data = fb_results.json().get('results', []) + if any(i.get('wrapperType') == 'track' and i.get('kind') == 'song' for i in fb_data): + logger.info(f"Found song tracks via fallback storefront: {fallback}") + results = fb_data + has_songs = True + break + except Exception: + continue + if not has_songs: + logger.warning(f"Album {album_id} has no song tracks in any storefront") + return None + # First result is usually the album/collection info # Extract album information to include in each track (like Spotify does) album_info = None diff --git a/web_server.py b/web_server.py index bd41981f..567c437c 100644 --- a/web_server.py +++ b/web_server.py @@ -11653,8 +11653,8 @@ def get_artist_album_tracks(artist_id, album_id): # Get album tracks tracks_data = client.get_album_tracks(resolved_album_id) - if not tracks_data or 'items' not in tracks_data: - return jsonify({"error": "No tracks found for album"}), 404 + if not tracks_data or 'items' not in tracks_data or len(tracks_data['items']) == 0: + return jsonify({"error": "No tracks found for album — it may be region-restricted or unavailable on this metadata source"}), 404 # Handle both dict and object responses from spotify_client.get_album() if isinstance(album_data, dict): diff --git a/webui/static/script.js b/webui/static/script.js index 0e2f217f..f4dc568f 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -37520,7 +37520,8 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) { if (response.status === 401) { throw new Error('Spotify not authenticated. Please check your API settings.'); } - throw new Error(`Failed to load album tracks: ${response.status}`); + const errData = await response.json().catch(() => ({})); + throw new Error(errData.error || `Failed to load album tracks: ${response.status}`); } const data = await response.json();