Compare commits

...

2 Commits

Author SHA1 Message Date
ee5a1376ee fix: playlist field: tracks -> items 2026-05-14 12:23:33 +02:00
e42cddd645 feat: exponential backoff 2026-05-14 12:23:18 +02:00

53
api.py
View File

@@ -30,6 +30,9 @@ from authomatic.adapters import WerkzeugAdapter
from authomatic.providers import oauth2
from authomatic.providers import PROVIDER_ID_MAP as AUTHOMATIC_PROVIDER_ID_MAP
import random
import time
import requests
@@ -294,9 +297,30 @@ def get_valid_access_token(spotify_user_id: str) -> str:
# ----------------------------
def spotify_get(url: str, access_token: str, params: dict | None = None) -> dict:
"""GET a Spotify Web API endpoint and return the parsed JSON body."""
"""
GET a Spotify Web API endpoint and return the parsed JSON body.
Retries on HTTP 429 and 503 with exponential backoff and optional Retry-After,
so brief Spotify rate limits often clear before we surface an error to the app.
"""
headers = {"Authorization": f"Bearer {access_token}"}
r = requests.get(url, headers=headers, params=params)
backoff_sec = 1.0
max_attempts = 8
params_eff = params
for attempt in range(max_attempts):
r = requests.get(url, headers=headers, params=params_eff)
if r.status_code in (429, 503) and attempt < max_attempts - 1:
wait = backoff_sec
ra = r.headers.get("Retry-After")
if ra:
try:
wait = max(wait, float(ra))
except ValueError:
pass
wait = min(wait, 120.0)
time.sleep(wait + random.random() * 0.25 * wait)
backoff_sec = min(backoff_sec * 2.0, 60.0)
continue
r.raise_for_status()
return r.json()
@@ -656,22 +680,25 @@ def playlist(playlist_id):
access_token,
)
# The playlist response embeds a `tracks` paging object whose first page
# contains up to 100 items. For playlists larger than that, follow the
# dedicated tracks endpoint until exhausted and splice the full list back
# into the response.
tracks_obj = playlist_data.get("tracks") or {}
if tracks_obj.get("next"):
all_tracks = spotify_get_paginated(
# Full playlist objects use a paging object at `items` (current Spotify shape)
# or legacy `tracks`. Follow `next` on whichever is present.
paging_key = (
"items"
if isinstance(playlist_data.get("items"), dict)
else "tracks"
)
paging = playlist_data.get(paging_key) or {}
if paging.get("next"):
all_items = spotify_get_paginated(
f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks",
access_token,
limit=100,
)
playlist_data["tracks"] = {
**tracks_obj,
"items": all_tracks,
playlist_data[paging_key] = {
**paging,
"items": all_items,
"offset": 0,
"limit": len(all_tracks),
"limit": len(all_items),
"next": None,
"previous": None,
}