"""Shared Spotify Client-Credentials token fetch for the pipeline scripts. The scripts (spotify-retag.py, spotify-genre.py, fix-track-metadata.py) each run as `python /app/pipeline/lib/.py`, so this directory is on sys.path and they can `from _spotify_auth import get_token`. Each still sources its own credentials (env vars vs a rendered .env file); only the token request itself, which was copy-pasted four ways, lives here. """ import base64 import json import urllib.parse import urllib.request _TOKEN_URL = "https://accounts.spotify.com/api/token" _cache: dict[tuple[str, str, str | None], str] = {} def get_token(client_id: str, client_secret: str, refresh_token: str | None = None) -> str: """Return an access token, cached per (id, secret, refresh_token) for the life of the process. If refresh_token is set (an account has been connected via /connect/spotify), mints a user token -- required for playlist reads on newly created Spotify apps, which reject client-credentials tokens. Otherwise falls back to client-credentials.""" key = (client_id, client_secret, refresh_token) if key in _cache: return _cache[key] creds = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() grant = ( {"grant_type": "refresh_token", "refresh_token": refresh_token} if refresh_token else {"grant_type": "client_credentials"} ) req = urllib.request.Request( _TOKEN_URL, data=urllib.parse.urlencode(grant).encode(), headers={ "Authorization": f"Basic {creds}", "Content-Type": "application/x-www-form-urlencoded", }, ) with urllib.request.urlopen(req, timeout=15) as r: _cache[key] = json.loads(r.read())["access_token"] return _cache[key]