diff --git a/functions/data-sources/main.py b/functions/data-sources/main.py index 6947a84..a3ec459 100644 --- a/functions/data-sources/main.py +++ b/functions/data-sources/main.py @@ -1,6 +1,153 @@ +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!"} - return {"status": "success", "message": "Data sources endpoint is up!"} \ No newline at end of file + + 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!"} + diff --git a/functions/data-sources/requirements.txt b/functions/data-sources/requirements.txt index 663bd1f..65c6304 100644 --- a/functions/data-sources/requirements.txt +++ b/functions/data-sources/requirements.txt @@ -1 +1,2 @@ -requests \ No newline at end of file +requests +redis \ No newline at end of file