mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-20 16:47:40 +02:00
182 lines
7.6 KiB
Python
182 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Spotify integration diagnostic.
|
|
|
|
Loads the saved refresh_token from OpenSwarm's tool config, refreshes
|
|
against Spotify, then hits every endpoint we care about and reports
|
|
exactly which one fails. Strips speculation — you see the real Spotify
|
|
response codes.
|
|
|
|
Run from the openswarm repo root:
|
|
backend/.venv/bin/python scripts/diagnose-spotify.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Pull env from backend/.env even if running outside backend.
|
|
try:
|
|
from dotenv import load_dotenv
|
|
load_dotenv(Path(__file__).resolve().parent.parent / "backend" / ".env")
|
|
except Exception:
|
|
pass
|
|
|
|
import httpx
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
TOOLS_DIR = REPO / "backend" / "data" / "tools"
|
|
|
|
CYAN = "\033[36m"; GREEN = "\033[32m"; RED = "\033[31m"; YELLOW = "\033[33m"; DIM = "\033[2m"; RESET = "\033[0m"
|
|
|
|
def section(text): print(f"\n{CYAN}=== {text} ==={RESET}")
|
|
def ok(text): print(f"{GREEN}✓{RESET} {text}")
|
|
def bad(text): print(f"{RED}✗{RESET} {text}")
|
|
def warn(text): print(f"{YELLOW}⚠{RESET} {text}")
|
|
|
|
|
|
def find_spotify_tool() -> dict | None:
|
|
if not TOOLS_DIR.is_dir():
|
|
return None
|
|
for f in TOOLS_DIR.glob("*.json"):
|
|
try:
|
|
data = json.loads(f.read_text())
|
|
if (data.get("name") or "").lower() == "spotify":
|
|
return data
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def main() -> int:
|
|
section("1. App credentials from backend/.env")
|
|
cid = os.environ.get("OPENSWARM_SPOTIFY_CLIENT_ID", "").strip()
|
|
csec = os.environ.get("OPENSWARM_SPOTIFY_CLIENT_SECRET", "").strip()
|
|
if not cid or not csec:
|
|
bad("OPENSWARM_SPOTIFY_CLIENT_ID/_SECRET not set in backend/.env")
|
|
return 1
|
|
ok(f"client_id loaded ({cid[:8]}...)")
|
|
ok("client_secret loaded")
|
|
|
|
section("2. Saved Spotify tool config")
|
|
tool = find_spotify_tool()
|
|
if not tool:
|
|
bad(f"No Spotify tool config in {TOOLS_DIR} — click Connect Spotify in OpenSwarm first.")
|
|
return 1
|
|
print(f" {DIM}tool id:{RESET} {tool.get('id')}")
|
|
print(f" {DIM}auth_status:{RESET} {tool.get('auth_status')}")
|
|
print(f" {DIM}connected as:{RESET} {tool.get('connected_account_email')}")
|
|
creds = tool.get("credentials") or {}
|
|
rt = creds.get("SPOTIFY_REFRESH_TOKEN") or ""
|
|
if not rt:
|
|
bad("No SPOTIFY_REFRESH_TOKEN saved. Reconnect via Tools page.")
|
|
return 1
|
|
ok(f"refresh_token saved ({len(rt)} chars)")
|
|
saved_scopes = creds.get("SPOTIFY_GRANTED_SCOPES", "") or "(not stored)"
|
|
print(f" {DIM}saved granted_scopes:{RESET} {saved_scopes}")
|
|
|
|
section("3. Refresh token against Spotify")
|
|
import base64
|
|
basic = base64.b64encode(f"{cid}:{csec}".encode()).decode()
|
|
try:
|
|
resp = httpx.post(
|
|
"https://accounts.spotify.com/api/token",
|
|
headers={"Authorization": f"Basic {basic}", "Content-Type": "application/x-www-form-urlencoded"},
|
|
data={"grant_type": "refresh_token", "refresh_token": rt},
|
|
timeout=15.0,
|
|
)
|
|
except Exception as e:
|
|
bad(f"HTTP error: {e}")
|
|
return 1
|
|
if resp.status_code != 200:
|
|
bad(f"Spotify rejected refresh: {resp.status_code} {resp.text[:300]}")
|
|
return 1
|
|
payload = resp.json()
|
|
access_token = payload.get("access_token", "")
|
|
granted_scopes_now = (payload.get("scope") or "").split()
|
|
ok(f"refresh OK — token expires in {payload.get('expires_in')}s")
|
|
print(f" {DIM}scopes ACTUALLY on the access_token right now ({len(granted_scopes_now)}):{RESET}")
|
|
for s in sorted(granted_scopes_now):
|
|
print(f" - {s}")
|
|
|
|
required_playlist = {"playlist-modify-private", "playlist-modify-public"}
|
|
missing = required_playlist - set(granted_scopes_now)
|
|
if missing:
|
|
bad(f"Missing playlist write scopes on the live access_token: {sorted(missing)}")
|
|
print(f"\n{YELLOW}This is the root cause of your 403. The token Spotify just gave us")
|
|
print(f"does not include playlist write permission. We need a fresh OAuth.{RESET}\n")
|
|
else:
|
|
ok("playlist-modify-private and playlist-modify-public BOTH present on live token")
|
|
|
|
section("4. /me — confirm account identity")
|
|
h = {"Authorization": f"Bearer {access_token}"}
|
|
me = httpx.get("https://api.spotify.com/v1/me", headers=h, timeout=15.0)
|
|
if me.status_code != 200:
|
|
bad(f"/me failed: {me.status_code} {me.text[:200]}")
|
|
return 1
|
|
me_json = me.json()
|
|
user_id = me_json.get("id")
|
|
print(f" {DIM}id:{RESET} {user_id}")
|
|
print(f" {DIM}display_name:{RESET} {me_json.get('display_name')}")
|
|
print(f" {DIM}product:{RESET} {me_json.get('product')} {DIM}(must be 'premium' for playback control){RESET}")
|
|
print(f" {DIM}country:{RESET} {me_json.get('country')}")
|
|
print(f" {DIM}email verified:{RESET} {me_json.get('email')}")
|
|
ok("/me works — auth is valid for read")
|
|
|
|
section("5. /me/playlists — verify playlist-read works")
|
|
pls = httpx.get("https://api.spotify.com/v1/me/playlists?limit=1", headers=h, timeout=15.0)
|
|
if pls.status_code == 200:
|
|
ok(f"/me/playlists works — you own/follow {pls.json().get('total', '?')} playlists")
|
|
else:
|
|
bad(f"/me/playlists failed: {pls.status_code} {pls.text[:200]}")
|
|
|
|
section("6. POST /users/{me}/playlists — the actual write that's 403ing")
|
|
create_body = {"name": "OpenSwarm Diagnostic — Delete Me", "public": False, "description": "diagnostic"}
|
|
cr = httpx.post(
|
|
f"https://api.spotify.com/v1/users/{user_id}/playlists",
|
|
headers={**h, "Content-Type": "application/json"},
|
|
json=create_body,
|
|
timeout=15.0,
|
|
)
|
|
if cr.status_code in (200, 201):
|
|
pl = cr.json()
|
|
ok(f"✓✓✓ playlist creation WORKS. id={pl.get('id')}")
|
|
print(f"\n{GREEN}Playlist '{pl.get('name')}' created successfully.")
|
|
print(f"You can delete it from your Spotify library.{RESET}\n")
|
|
return 0
|
|
bad(f"Playlist creation 403'd: {cr.status_code}")
|
|
try:
|
|
err = cr.json()
|
|
print(f" {DIM}Spotify error JSON:{RESET}")
|
|
print(f" {json.dumps(err, indent=2)}")
|
|
except Exception:
|
|
print(f" {DIM}raw body:{RESET} {cr.text[:500]}")
|
|
|
|
section("Diagnosis summary")
|
|
if missing:
|
|
bad("Confirmed: the access_token does not carry playlist write scopes.")
|
|
print()
|
|
print(f"{YELLOW}Fix (definitive sequence):{RESET}")
|
|
print(" 1. Open https://www.spotify.com/account/apps — REMOVE the OpenSwarm app")
|
|
print(" 2. Stop OpenSwarm (Ctrl+C)")
|
|
print(" 3. Delete the saved Spotify tool config to force a clean state:")
|
|
spot_id = tool.get("id")
|
|
print(f" rm {TOOLS_DIR / (str(spot_id) + '.json')}")
|
|
print(" 4. bash run.sh")
|
|
print(" 5. Connect Spotify again — the consent screen should now list every permission")
|
|
print(" including 'Modify your private playlists' and 'Modify your public playlists'")
|
|
print(" 6. Re-run this script to confirm. All sections should be green.")
|
|
else:
|
|
warn("Scopes look right but Spotify still refused the write.")
|
|
print(f"\n{YELLOW}This is unusual. Likely causes (in order):{RESET}")
|
|
print(" - Your Spotify account is a Family/Kids/restricted sub-account that can't create playlists")
|
|
print(" - Regional restriction (try VPN-disabled in your account's country)")
|
|
print(f" - The user_id in the URL ('{user_id}') doesn't match the token's account — file a bug")
|
|
print(f"\nSend this entire diagnostic output to debug further.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|