219 lines
5 KiB
Python
219 lines
5 KiB
Python
#!/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 = """
|
|
<h1>Spotify authorization received.</h1>
|
|
<p>You can close this tab.</p>
|
|
"""
|
|
else:
|
|
message = """
|
|
<h1>Spotify authorization failed.</h1>
|
|
<p>Check the terminal for details.</p>
|
|
"""
|
|
|
|
self.wfile.write(
|
|
f"""
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Spotify OAuth</title>
|
|
</head>
|
|
<body>
|
|
{message}
|
|
</body>
|
|
</html>
|
|
""".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()
|
|
|