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
This commit is contained in:
Broque Thomas 2026-04-20 11:10:54 -07:00
parent 8c96e0e197
commit 71bff55c6a
3 changed files with 38 additions and 4 deletions

View file

@ -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

View file

@ -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):

View file

@ -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();