refactor: manueller auth flow ohne server - user pastet URL
This commit is contained in:
@@ -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
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user