refactor: manueller auth flow ohne server - user pastet URL

This commit is contained in:
x3
2026-09-06 08:30:19 +00:00
parent 305cc9313b
commit 9f84722233
2 changed files with 49 additions and 38 deletions
+4 -2
View File
@@ -14,7 +14,7 @@ Terminal-basierter Spotify Client im Stil von ncmpcpp.
1. Gehe zu https://developer.spotify.com/dashboard 1. Gehe zu https://developer.spotify.com/dashboard
2. Erstelle eine neue App 2. Erstelle eine neue App
3. Setze den Redirect URI auf `http://localhost:8888/callback` 3. Setze den Redirect URI auf `http://127.0.0.1:8888/callback`
4. Kopiere die **Client ID** 4. Kopiere die **Client ID**
### 2. Installation ### 2. Installation
@@ -37,7 +37,9 @@ python -m spotify_cli.main
spotify-cli --login spotify-cli --login
``` ```
Beim ersten Start wirst du nach deiner Client ID gefragt. Der Browser öffnet sich automatisch zur Spotify-Authentifizierung. Beim ersten Start wirst du nach deiner Client ID gefragt. Der Browser öffnet sich zur Spotify-Authentifizierung.
**Wichtig:** Nach der Authentifizierung wirst du zu `http://127.0.0.1:8888/callback` weitergeleitet. Der Redirect wird fehlschlagen - das ist normal! Kopiere die **ganze URL** aus der Browser-Adresszeile und füge sie im Terminal ein.
## Benutzung ## Benutzung
+45 -36
View File
@@ -4,13 +4,14 @@ import secrets
import webbrowser import webbrowser
import time import time
import json import json
from urllib.parse import urlencode from urllib.parse import urlencode, urlparse, parse_qs
import requests import requests
from .config import load_config, save_token, load_token, clear_token from .config import load_config, save_token, load_token, clear_token
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize" SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token" SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
REDIRECT_URI = "http://127.0.0.1:8888/callback"
SCOPES = [ SCOPES = [
"user-read-playback-state", "user-read-playback-state",
"user-modify-playback-state", "user-modify-playback-state",
@@ -23,17 +24,20 @@ SCOPES = [
"streaming", "streaming",
] ]
def _generate_pkce(): def _generate_pkce():
code_verifier = secrets.token_urlsafe(64)[:128] code_verifier = secrets.token_urlsafe(64)[:128]
digest = hashlib.sha256(code_verifier.encode()).digest() digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
return code_verifier, code_challenge return code_verifier, code_challenge
class SpotifyAuth: class SpotifyAuth:
def __init__(self): def __init__(self):
self.cfg = load_config() self.cfg = load_config()
self.client_id = self.cfg.get("client_id", "") self.client_id = self.cfg.get("client_id", "")
self.token_data = load_token() self.token_data = load_token()
self._code_verifier = None
def _is_token_valid(self): def _is_token_valid(self):
if not self.token_data: if not self.token_data:
@@ -78,12 +82,13 @@ class SpotifyAuth:
save_config(self.cfg) save_config(self.cfg)
code_verifier, code_challenge = _generate_pkce() code_verifier, code_challenge = _generate_pkce()
self._code_verifier = code_verifier
state = secrets.token_urlsafe(16) state = secrets.token_urlsafe(16)
params = { params = {
"client_id": self.client_id, "client_id": self.client_id,
"response_type": "code", "response_type": "code",
"redirect_uri": "http://localhost:8888/callback", "redirect_uri": REDIRECT_URI,
"code_challenge_method": "S256", "code_challenge_method": "S256",
"code_challenge": code_challenge, "code_challenge": code_challenge,
"state": state, "state": state,
@@ -91,58 +96,61 @@ class SpotifyAuth:
} }
auth_url = f"{SPOTIFY_AUTH_URL}?{urlencode(params)}" auth_url = f"{SPOTIFY_AUTH_URL}?{urlencode(params)}"
print(f"\nOeffne Browser fuer Spotify Login...") print(f"\n{'='*60}")
print(f"Falls der Browser nicht öffnet, nutze diese URL:\n{auth_url}\n") print(f"Oeffne Browser fuer Spotify Login...")
webbrowser.open(auth_url) print(f"Falls der Browser nicht öffnet, nutze diese URL:")
print(f"\n{auth_url}\n")
print(f"{'='*60}")
print(f"\nNach der Authentifizierung wirst du weitergeleitet.")
print(f"Der Redirect wird fehlschlagen - das ist normal!")
print(f"\nKopiere die URL aus der Browser-Adresszeile")
print(f"und fuege sie hier ein.\n")
code = self._wait_for_callback() try:
webbrowser.open(auth_url)
except Exception:
pass
code = self._wait_for_code()
if not code: if not code:
print("Authentifizierung fehlgeschlagen.") print("Authentifizierung fehlgeschlagen.")
return None return None
return self._exchange_code(code, code_verifier) return self._exchange_code(code, code_verifier)
def _wait_for_callback(self, timeout=300): def _wait_for_code(self):
from http.server import HTTPServer, BaseHTTPRequestHandler for attempt in range(5):
url_input = input("URL einfügen: ").strip()
received_code = {"code": None, "state": None} if not url_input:
if attempt < 4:
print("Bitte URL einfügen.")
continue
class Handler(BaseHTTPRequestHandler): code = self._extract_code(url_input)
def do_GET(self): if code:
if "/callback" in self.path: return code
from urllib.parse import urlparse, parse_qs
query = urlparse(self.path).query
params = parse_qs(query)
received_code["code"] = params.get("code", [None])[0]
received_code["state"] = params.get("state", [None])[0]
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<h1>Authentifizierung erfolgreich! Du kannst das Fenster schliessen.</h1>")
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args): print("Code konnte nicht extrahiert werden. Versuche es erneut.")
pass
server = HTTPServer(("localhost", 8888), Handler) return None
server.timeout = timeout
start = time.time() def _extract_code(self, url_or_code):
while time.time() - start < timeout: if "code=" in url_or_code:
server.handle_request() parsed = urlparse(url_or_code)
if received_code["code"]: params = parse_qs(parsed.query)
break return params.get("code", [None])[0]
server.server_close() if url_or_code.startswith("AQ"):
return received_code["code"] return url_or_code
return url_or_code if len(url_or_code) > 20 else None
def _exchange_code(self, code, code_verifier): def _exchange_code(self, code, code_verifier):
resp = requests.post(SPOTIFY_TOKEN_URL, data={ resp = requests.post(SPOTIFY_TOKEN_URL, data={
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": code, "code": code,
"redirect_uri": "http://localhost:8888/callback", "redirect_uri": REDIRECT_URI,
"client_id": self.client_id, "client_id": self.client_id,
"code_verifier": code_verifier, "code_verifier": code_verifier,
}) })
@@ -155,6 +163,7 @@ class SpotifyAuth:
"expires_at": time.time() + data["expires_in"], "expires_at": time.time() + data["expires_in"],
} }
save_token(self.token_data) save_token(self.token_data)
print("Erfolgreich eingelogged!")
return data["access_token"] return data["access_token"]
print(f"Token-Austausch fehlgeschlagen: {resp.status_code}") print(f"Token-Austausch fehlgeschlagen: {resp.status_code}")