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 oauth2
from authomatic.providers import PROVIDER_ID_MAP as AUTHOMATIC_PROVIDER_ID_MAP from authomatic.providers import PROVIDER_ID_MAP as AUTHOMATIC_PROVIDER_ID_MAP
import random
import time
import requests 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: 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}"} 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() r.raise_for_status()
return r.json() return r.json()
@@ -656,22 +680,25 @@ def playlist(playlist_id):
access_token, access_token,
) )
# The playlist response embeds a `tracks` paging object whose first page # Full playlist objects use a paging object at `items` (current Spotify shape)
# contains up to 100 items. For playlists larger than that, follow the # or legacy `tracks`. Follow `next` on whichever is present.
# dedicated tracks endpoint until exhausted and splice the full list back paging_key = (
# into the response. "items"
tracks_obj = playlist_data.get("tracks") or {} if isinstance(playlist_data.get("items"), dict)
if tracks_obj.get("next"): else "tracks"
all_tracks = spotify_get_paginated( )
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", f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks",
access_token, access_token,
limit=100, limit=100,
) )
playlist_data["tracks"] = { playlist_data[paging_key] = {
**tracks_obj, **paging,
"items": all_tracks, "items": all_items,
"offset": 0, "offset": 0,
"limit": len(all_tracks), "limit": len(all_items),
"next": None, "next": None,
"previous": None, "previous": None,
} }