import base64 import os import json import requests import time import redis REDIRECT_URL = "https://shsf-api.reversed.dev/api/exec/15/e7f4f8a2-5bef-413d-85fe-6c47a6cfc1e2/spoti" SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token" # Separate Redis DB from the control function (which uses db=12) r = redis.Redis(host='194.15.36.205', username="default", port=12004, db=13, password=os.getenv("REDIS")) SPOTIFY_TOKENS_KEY = "data-sources:spotify-tokens" def _save_tokens(access_token: str, refresh_token: str, expires_in: int): payload = { "access_token": access_token, "refresh_token": refresh_token, "expires_at": int(time.time()) + expires_in, } r.set(SPOTIFY_TOKENS_KEY, json.dumps(payload)) def _read_tokens(): raw = r.get(SPOTIFY_TOKENS_KEY) if raw: try: return json.loads(raw) except json.JSONDecodeError: return None return None def _refresh_access_token(refresh_token: str): client_id = os.getenv("SPOTIFY_CLIENT_ID") client_secret = os.getenv("SPOTIFY_CLIENT_SECRET") auth = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() resp = requests.post( SPOTIFY_TOKEN_URL, headers={"Authorization": f"Basic {auth}", "Content-Type": "application/x-www-form-urlencoded"}, data={"grant_type": "refresh_token", "refresh_token": refresh_token}, timeout=10, ) resp.raise_for_status() data = resp.json() _save_tokens( data["access_token"], data.get("refresh_token", refresh_token), # Spotify may or may not return a new refresh token data.get("expires_in", 3600), ) return data["access_token"] def _get_valid_access_token(): """Return a valid access token, refreshing if needed. Returns None if no tokens stored.""" tokens = _read_tokens() if not tokens: return None # Refresh 60s before expiry if time.time() >= tokens["expires_at"] - 60: return _refresh_access_token(tokens["refresh_token"]) return tokens["access_token"] def main(args): route = args.get("route") if route == "test": return {"status": "success", "text": "Text Test Success!"} if route == "auth_spoti": client_id = os.getenv("SPOTIFY_CLIENT_ID") url = ( f"https://accounts.spotify.com/authorize" f"?client_id={client_id}" f"&response_type=code" f"&redirect_uri={REDIRECT_URL}" f"&scope=user-read-playback-state%20user-modify-playback-state%20user-read-currently-playing" ) return {"_code": 302, "_location": url, "_shsf": "v2"} if route == "spoti": code = args.get("query", {}).get("code") if not code: return {"status": "error", "message": "Missing code parameter"} client_id = os.getenv("SPOTIFY_CLIENT_ID") client_secret = os.getenv("SPOTIFY_CLIENT_SECRET") if not client_id or not client_secret: return {"status": "error", "message": "Missing Spotify credentials in environment"} # Exchange authorization code for tokens auth = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() try: resp = requests.post( SPOTIFY_TOKEN_URL, headers={ "Authorization": f"Basic {auth}", "Content-Type": "application/x-www-form-urlencoded", }, data={ "grant_type": "authorization_code", "code": code, "redirect_uri": REDIRECT_URL, }, timeout=10, ) resp.raise_for_status() except requests.RequestException as e: return {"status": "error", "message": f"Token exchange failed: {e}"} data = resp.json() if "error" in data: return {"status": "error", "message": data.get("error_description", data["error"])} _save_tokens(data["access_token"], data["refresh_token"], data.get("expires_in", 3600)) return {"status": "success", "message": "Spotify authenticated and tokens saved."} if route == "get_spoti_tokens": tokens = _read_tokens() if not tokens: return {"status": "error", "message": "No Spotify tokens stored"} return {"status": "success", "tokens": tokens} if route == "get_spoti_now_playing": try: access_token = _get_valid_access_token() except requests.RequestException as e: return {"status": "error", "message": f"Token refresh failed: {e}"} if not access_token: return {"status": "error", "message": "Not authenticated with Spotify"} try: resp = requests.get( "https://api.spotify.com/v1/me/player/currently-playing", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) except requests.RequestException as e: return {"status": "error", "message": f"Spotify API request failed: {e}"} if resp.status_code == 204: return {"status": "success", "playing": False, "message": "Nothing currently playing"} if not resp.ok: return {"status": "error", "message": f"Spotify API error {resp.status_code}"} return {"status": "success", "playing": True, "data": resp.json()} return {"status": "success", "message": "Data sources endpoint is up!"}