Fix: Spotify playlist sync shows only 100 tracks (#736)
Reported by @Yug1900: a Spotify playlist's overview shows the correct count (e.g. 203) but the sync modal lists only 100, and only 100 would sync. Root cause in /api/spotify/playlist/<id> (the Spotify-tab track fetch): owned playlists fetch + paginate fine (sp.next loops over all pages). But when the authenticated playlist_items call FAILS (intermittently, often a 403 on followed / not-owned playlists) it fell straight back to the public embed scraper, which is hard-capped at ~100 tracks and has no pagination — and the result was returned as if complete. The overview total is correct because it comes from the playlist *metadata* listing, which succeeds independently. Fixes (additive — the owned/working path is unchanged): - Retry the items fetch once before resorting to the embed scraper, so a transient failure no longer silently truncates a large playlist. - Guard against silent truncation: compare fetched count against the playlist's known total; if short (or via the capped embed fallback), log a warning and return `incomplete: true` + `expected_total` instead of presenting a partial list as complete. No behavior change when the fetch succeeds in full (incomplete=false, extra fields ignored by the frontend). Lets a future UI tweak surface "got 100 of 203 — retry" rather than silently dropping tracks.
This commit is contained in:
parent
70249cade4
commit
779d729a08
1 changed files with 38 additions and 7 deletions
|
|
@ -18956,11 +18956,24 @@ def get_playlist_tracks(playlist_id):
|
||||||
|
|
||||||
# Fetch all tracks with full album data
|
# Fetch all tracks with full album data
|
||||||
tracks = []
|
tracks = []
|
||||||
try:
|
# The items endpoint intermittently fails for followed playlists — retry
|
||||||
results = spotify_client._get_playlist_items_page(playlist_id, limit=100)
|
# once before resorting to the (≈100-capped) public embed scraper, so a
|
||||||
except Exception as items_err:
|
# transient failure doesn't silently truncate a large playlist.
|
||||||
# 403 on followed playlists — try the public embed scraper as fallback
|
results = None
|
||||||
logger.warning(f"Playlist items fetch failed ({items_err}), trying public embed scraper")
|
items_err = None
|
||||||
|
for _attempt in range(2):
|
||||||
|
try:
|
||||||
|
results = spotify_client._get_playlist_items_page(playlist_id, limit=100)
|
||||||
|
break
|
||||||
|
except Exception as _e:
|
||||||
|
items_err = _e
|
||||||
|
if _attempt == 0:
|
||||||
|
logger.warning(f"Playlist items fetch failed ({_e}); retrying once")
|
||||||
|
time.sleep(0.5)
|
||||||
|
if results is None:
|
||||||
|
# Both attempts failed (often a 403 on followed playlists) — fall back
|
||||||
|
# to the public embed scraper as a last resort (capped at ~100 tracks).
|
||||||
|
logger.warning(f"Playlist items unavailable after retry ({items_err}), trying public embed scraper")
|
||||||
try:
|
try:
|
||||||
from core.spotify_public_scraper import scrape_spotify_embed
|
from core.spotify_public_scraper import scrape_spotify_embed
|
||||||
embed_data = scrape_spotify_embed('playlist', playlist_id)
|
embed_data = scrape_spotify_embed('playlist', playlist_id)
|
||||||
|
|
@ -18986,7 +18999,10 @@ def get_playlist_tracks(playlist_id):
|
||||||
'track_count': len(tracks),
|
'track_count': len(tracks),
|
||||||
'image_url': playlist_data['images'][0]['url'] if playlist_data.get('images') else None,
|
'image_url': playlist_data['images'][0]['url'] if playlist_data.get('images') else None,
|
||||||
'snapshot_id': playlist_data.get('snapshot_id', ''),
|
'snapshot_id': playlist_data.get('snapshot_id', ''),
|
||||||
'tracks': tracks
|
'tracks': tracks,
|
||||||
|
# Embed scrape is capped at ~100 — mark as incomplete.
|
||||||
|
'incomplete': True,
|
||||||
|
'expected_total': (playlist_data.get('tracks') or {}).get('total'),
|
||||||
}
|
}
|
||||||
return jsonify(playlist_dict)
|
return jsonify(playlist_dict)
|
||||||
except Exception as scrape_err:
|
except Exception as scrape_err:
|
||||||
|
|
@ -19014,6 +19030,19 @@ def get_playlist_tracks(playlist_id):
|
||||||
else:
|
else:
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
|
# Guard against silent truncation: Spotify occasionally returns a short
|
||||||
|
# first page (next=None) for some playlists, so the loop above can end
|
||||||
|
# early. Compare against the playlist's known total and flag it rather
|
||||||
|
# than presenting a partial list as complete (bug #736).
|
||||||
|
expected_total = (playlist_data.get('tracks') or {}).get('total')
|
||||||
|
incomplete = bool(expected_total) and len(tracks) < expected_total
|
||||||
|
if incomplete:
|
||||||
|
logger.warning(
|
||||||
|
f"Playlist {playlist_id} ('{playlist_data.get('name')}'): fetched "
|
||||||
|
f"{len(tracks)}/{expected_total} tracks — Spotify returned incomplete "
|
||||||
|
f"items data (flagged, not silently truncated)"
|
||||||
|
)
|
||||||
|
|
||||||
# Convert playlist to dict
|
# Convert playlist to dict
|
||||||
playlist_dict = {
|
playlist_dict = {
|
||||||
'id': playlist_data['id'],
|
'id': playlist_data['id'],
|
||||||
|
|
@ -19025,7 +19054,9 @@ def get_playlist_tracks(playlist_id):
|
||||||
'track_count': len(tracks),
|
'track_count': len(tracks),
|
||||||
'image_url': playlist_data['images'][0]['url'] if playlist_data.get('images') else None,
|
'image_url': playlist_data['images'][0]['url'] if playlist_data.get('images') else None,
|
||||||
'snapshot_id': playlist_data.get('snapshot_id', ''),
|
'snapshot_id': playlist_data.get('snapshot_id', ''),
|
||||||
'tracks': tracks
|
'tracks': tracks,
|
||||||
|
'incomplete': incomplete,
|
||||||
|
'expected_total': expected_total,
|
||||||
}
|
}
|
||||||
return jsonify(playlist_dict)
|
return jsonify(playlist_dict)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue