Compare commits
2 Commits
b0a7202f32
...
ee5a1376ee
| Author | SHA1 | Date | |
|---|---|---|---|
| ee5a1376ee | |||
| e42cddd645 |
53
api.py
53
api.py
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user