From ed6576b0f0313189348edb56037003b04ad034e1 Mon Sep 17 00:00:00 2001 From: TheAchiever6823 <61914223+ShawnMadadha@users.noreply.github.com> Date: Mon, 18 May 2026 15:37:13 -0700 Subject: [PATCH] [shawn] chore: gitignore DEAD_CODE_REPORT.md + ship diagnose-spotify.py from earlier --- .gitignore | 1 + scripts/diagnose-spotify.py | 181 ++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 scripts/diagnose-spotify.py diff --git a/.gitignore b/.gitignore index 336000aa..b8d26fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ Thumbs.db ehthumbs.db desktop.ini frontend/tsconfig.tsbuildinfo +DEAD_CODE_REPORT.md diff --git a/scripts/diagnose-spotify.py b/scripts/diagnose-spotify.py new file mode 100644 index 00000000..d7a5b101 --- /dev/null +++ b/scripts/diagnose-spotify.py @@ -0,0 +1,181 @@ +#!/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())