feat: implement Spotify authentication and token management in the data sources
Some checks failed
Build App / build (push) Has been cancelled
Some checks failed
Build App / build (push) Has been cancelled
This commit is contained in:
@@ -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!"}
|
||||
|
||||
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!"}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
requests
|
||||
requests
|
||||
redis
|
||||
Reference in New Issue
Block a user