Files
spotify-cli/spotify_cli/auth.py
T

177 lines
5.5 KiB
Python

import hashlib
import base64
import secrets
import webbrowser
import time
import json
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",
"user-read-currently-playing",
"playlist-read-private",
"playlist-read-collaborative",
"user-library-read",
"user-read-recently-played",
"user-top-read",
"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:
return False
return time.time() < self.token_data.get("expires_at", 0) - 60
def get_token(self):
if self._is_token_valid():
return self.token_data["access_token"]
if self.token_data and self.token_data.get("refresh_token"):
refreshed = self._refresh(self.token_data["refresh_token"])
if refreshed:
return self.token_data["access_token"]
return self._full_auth()
def _refresh(self, refresh_token):
resp = requests.post(SPOTIFY_TOKEN_URL, data={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": self.client_id,
})
if resp.status_code == 200:
data = resp.json()
self.token_data = {
"access_token": data["access_token"],
"refresh_token": data.get("refresh_token", refresh_token),
"expires_at": time.time() + data["expires_in"],
}
save_token(self.token_data)
return True
clear_token()
self.token_data = None
return False
def _full_auth(self):
if not self.client_id:
self.client_id = input("Spotify Client ID: ").strip()
self.cfg["client_id"] = self.client_id
from .config import save_config
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": REDIRECT_URI,
"code_challenge_method": "S256",
"code_challenge": code_challenge,
"state": state,
"scope": " ".join(SCOPES),
}
auth_url = f"{SPOTIFY_AUTH_URL}?{urlencode(params)}"
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")
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_code(self):
for attempt in range(5):
url_input = input("URL einfügen: ").strip()
if not url_input:
if attempt < 4:
print("Bitte URL einfügen.")
continue
code = self._extract_code(url_input)
if code:
return code
print("Code konnte nicht extrahiert werden. Versuche es erneut.")
return None
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]
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": REDIRECT_URI,
"client_id": self.client_id,
"code_verifier": code_verifier,
})
if resp.status_code == 200:
data = resp.json()
self.token_data = {
"access_token": data["access_token"],
"refresh_token": data["refresh_token"],
"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}")
print(resp.text)
return None
def logout(self):
clear_token()
self.token_data = None
print("Ausgelogged.")