diff --git a/api.py b/api.py index b5b2cce..29f7119 100644 --- a/api.py +++ b/api.py @@ -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,11 +297,32 @@ 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) - r.raise_for_status() - return r.json() + 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() def spotify_get_paginated(