#!/usr/bin/env python3 import base64 import http.server import urllib.parse import webbrowser import requests CLIENT_ID = "4442e700b9ee46c08b24f351abbe030c" REDIRECT_URI = "http://127.0.0.1:8888/callback" SCOPES = [ "user-read-currently-playing", "user-read-playback-state", ] AUTH_URL = "https://accounts.spotify.com/authorize" TOKEN_URL = "https://accounts.spotify.com/api/token" class CallbackHandler(http.server.BaseHTTPRequestHandler): code = None error = None def do_GET(self): parsed = urllib.parse.urlparse(self.path) params = urllib.parse.parse_qs(parsed.query) CallbackHandler.code = params.get("code", [None])[0] CallbackHandler.error = params.get("error", [None])[0] self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() if CallbackHandler.code: message = """

Spotify authorization received.

You can close this tab.

""" else: message = """

Spotify authorization failed.

Check the terminal for details.

""" self.wfile.write( f""" Spotify OAuth {message} """.encode("utf-8") ) def log_message(self, format, *args): pass def get_client_secret(): secret = input("SPOTIFY_CLIENT_SECRET: ").strip() if not secret: raise SystemExit("Client secret cannot be empty.") return secret def build_authorization_url(): params = urllib.parse.urlencode( { "client_id": CLIENT_ID, "response_type": "code", "redirect_uri": REDIRECT_URI, "scope": " ".join(SCOPES), } ) return f"{AUTH_URL}?{params}" def exchange_code_for_token(client_secret, code): credentials = f"{CLIENT_ID}:{client_secret}" encoded_credentials = base64.b64encode( credentials.encode("utf-8") ).decode("ascii") response = requests.post( TOKEN_URL, data={ "grant_type": "authorization_code", "code": code, "redirect_uri": REDIRECT_URI, }, headers={ "Authorization": f"Basic {encoded_credentials}", "Content-Type": "application/x-www-form-urlencoded", }, timeout=15, ) return response def main(): print() print("Spotify OAuth token generator") print("=" * 60) print() client_secret = get_client_secret() server = http.server.HTTPServer( ("127.0.0.1", 8888), CallbackHandler, ) authorization_url = build_authorization_url() print() print("Opening Spotify authorization page...") print() print(authorization_url) print() webbrowser.open(authorization_url) print("Waiting for Spotify callback...") print() while CallbackHandler.code is None and CallbackHandler.error is None: server.handle_request() server.server_close() if CallbackHandler.error: print("Spotify authorization failed:") print(CallbackHandler.error) raise SystemExit(1) code = CallbackHandler.code if not code: raise SystemExit("No authorization code received.") print("Authorization code received.") print("Exchanging code for tokens...") print() response = exchange_code_for_token(client_secret, code) if not response.ok: print("=" * 60) print("SPOTIFY TOKEN REQUEST FAILED") print("=" * 60) print() print(f"HTTP status: {response.status_code}") print() print("Spotify response:") print(response.text) print() raise SystemExit(1) try: data = response.json() except ValueError: print("Spotify returned invalid JSON:") print(response.text) raise SystemExit(1) refresh_token = data.get("refresh_token") access_token = data.get("access_token") expires_in = data.get("expires_in") scope = data.get("scope") print("=" * 60) print("SPOTIFY TOKENS") print("=" * 60) print() if refresh_token: print("SPOTIFY_REFRESH_TOKEN:") print(refresh_token) else: print("WARNING: Spotify did not return a refresh_token.") print() if access_token: print("Access token received: YES") else: print("Access token received: NO") print(f"Expires in: {expires_in} seconds") print(f"Granted scopes: {scope}") print() print("=" * 60) print("IMPORTANT") print("=" * 60) print() print("Store the refresh token in your .env file.") print("Do not commit it to Git.") print() if __name__ == "__main__": main()