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:
BoulderBadgeDad 2026-05-29 09:16:47 -07:00
parent 70249cade4
commit 779d729a08

View file

@ -18956,11 +18956,24 @@ def get_playlist_tracks(playlist_id):
# Fetch all tracks with full album data
tracks = []
try:
results = spotify_client._get_playlist_items_page(playlist_id, limit=100)
except Exception as items_err:
# 403 on followed playlists — try the public embed scraper as fallback
logger.warning(f"Playlist items fetch failed ({items_err}), trying public embed scraper")
# The items endpoint intermittently fails for followed playlists — retry
# once before resorting to the (≈100-capped) public embed scraper, so a
# transient failure doesn't silently truncate a large playlist.
results = None
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:
from core.spotify_public_scraper import scrape_spotify_embed
embed_data = scrape_spotify_embed('playlist', playlist_id)
@ -18986,7 +18999,10 @@ def get_playlist_tracks(playlist_id):
'track_count': len(tracks),
'image_url': playlist_data['images'][0]['url'] if playlist_data.get('images') else None,
'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)
except Exception as scrape_err:
@ -19014,6 +19030,19 @@ def get_playlist_tracks(playlist_id):
else:
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
playlist_dict = {
'id': playlist_data['id'],
@ -19025,7 +19054,9 @@ def get_playlist_tracks(playlist_id):
'track_count': len(tracks),
'image_url': playlist_data['images'][0]['url'] if playlist_data.get('images') else None,
'snapshot_id': playlist_data.get('snapshot_id', ''),
'tracks': tracks
'tracks': tracks,
'incomplete': incomplete,
'expected_total': expected_total,
}
return jsonify(playlist_dict)
except Exception as e: