From 9f8472223342fcd2ef564fe0b25e8d60911861ec Mon Sep 17 00:00:00 2001 From: x3 Date: Sun, 6 Sep 2026 08:30:19 +0000 Subject: [PATCH] refactor: manueller auth flow ohne server - user pastet URL --- README.md | 6 ++-- spotify_cli/auth.py | 81 +++++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 5f9fde4..80d7c1d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Terminal-basierter Spotify Client im Stil von ncmpcpp. 1. Gehe zu https://developer.spotify.com/dashboard 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** ### 2. Installation @@ -37,7 +37,9 @@ python -m spotify_cli.main 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 diff --git a/spotify_cli/auth.py b/spotify_cli/auth.py index ca24e5a..c52e8cf 100644 --- a/spotify_cli/auth.py +++ b/spotify_cli/auth.py @@ -4,13 +4,14 @@ import secrets import webbrowser import time import json -from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse, parse_qs import requests from .config import load_config, save_token, load_token, clear_token SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize" SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token" +REDIRECT_URI = "http://127.0.0.1:8888/callback" SCOPES = [ "user-read-playback-state", "user-modify-playback-state", @@ -23,17 +24,20 @@ SCOPES = [ "streaming", ] + def _generate_pkce(): code_verifier = secrets.token_urlsafe(64)[:128] digest = hashlib.sha256(code_verifier.encode()).digest() code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() return code_verifier, code_challenge + class SpotifyAuth: def __init__(self): self.cfg = load_config() self.client_id = self.cfg.get("client_id", "") self.token_data = load_token() + self._code_verifier = None def _is_token_valid(self): if not self.token_data: @@ -78,12 +82,13 @@ class SpotifyAuth: save_config(self.cfg) code_verifier, code_challenge = _generate_pkce() + self._code_verifier = code_verifier state = secrets.token_urlsafe(16) params = { "client_id": self.client_id, "response_type": "code", - "redirect_uri": "http://localhost:8888/callback", + "redirect_uri": REDIRECT_URI, "code_challenge_method": "S256", "code_challenge": code_challenge, "state": state, @@ -91,58 +96,61 @@ class SpotifyAuth: } auth_url = f"{SPOTIFY_AUTH_URL}?{urlencode(params)}" - print(f"\nOeffne Browser fuer Spotify Login...") - print(f"Falls der Browser nicht öffnet, nutze diese URL:\n{auth_url}\n") - webbrowser.open(auth_url) + print(f"\n{'='*60}") + print(f"Oeffne Browser fuer Spotify Login...") + 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: print("Authentifizierung fehlgeschlagen.") return None return self._exchange_code(code, code_verifier) - def _wait_for_callback(self, timeout=300): - from http.server import HTTPServer, BaseHTTPRequestHandler + def _wait_for_code(self): + 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): - def do_GET(self): - if "/callback" in self.path: - 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"

Authentifizierung erfolgreich! Du kannst das Fenster schliessen.

") - else: - self.send_response(404) - self.end_headers() + code = self._extract_code(url_input) + if code: + return code - def log_message(self, format, *args): - pass + print("Code konnte nicht extrahiert werden. Versuche es erneut.") - server = HTTPServer(("localhost", 8888), Handler) - server.timeout = timeout + return None - start = time.time() - while time.time() - start < timeout: - server.handle_request() - if received_code["code"]: - break + def _extract_code(self, url_or_code): + if "code=" in url_or_code: + parsed = urlparse(url_or_code) + params = parse_qs(parsed.query) + return params.get("code", [None])[0] - server.server_close() - return received_code["code"] + if url_or_code.startswith("AQ"): + return url_or_code + + return url_or_code if len(url_or_code) > 20 else None def _exchange_code(self, code, code_verifier): resp = requests.post(SPOTIFY_TOKEN_URL, data={ "grant_type": "authorization_code", "code": code, - "redirect_uri": "http://localhost:8888/callback", + "redirect_uri": REDIRECT_URI, "client_id": self.client_id, "code_verifier": code_verifier, }) @@ -155,6 +163,7 @@ class SpotifyAuth: "expires_at": time.time() + data["expires_in"], } save_token(self.token_data) + print("Erfolgreich eingelogged!") return data["access_token"] print(f"Token-Austausch fehlgeschlagen: {resp.status_code}")