#!/usr/bin/env python3 """Manueller Spotify-PKCE-Login fuer Hermes -- OHNE lokalen Callback-Server und OHNE SSH-Port-Forward. Hintergrund: `hermes auth spotify` startet einen HTTP-Listener auf 127.0.0.1:43827 IM Container und wartet auf den OAuth-Redirect. Der redirect_uri-Wert ist von Hermes hart auf 127.0.0.1/localhost validiert (hermes_cli/auth.py:_spotify_validate_redirect_uri) -- eine oeffentliche Callback-URL ist nicht moeglich. Ohne `ssh -N -L 43827:127.0.0.1:43827` kann der Browser auf dem Laptop/Handy diesen Listener nie erreichen. Dieses Skript umgeht genau das: es baut dieselbe Autorisierungs-URL wie `hermes auth spotify` (identischer PKCE-Code, identischer Redirect-URI), aber statt auf den Callback zu warten, fragt es Dich nach dem `code`, den Du von Hand aus der Adressleiste kopierst (der Redirect schlaegt fehl, weil nichts auf 127.0.0.1:43827 Deines eigenen Geraets lauscht -- die Adresse mit dem code-Parameter bleibt aber sichtbar). Das Ergebnis wird 1:1 in ~/.hermes/auth.json geschrieben, exakt im selben Format wie beim normalen Login -- Hermes' Auto-Refresh (resolve_spotify_runtime_credentials) funktioniert danach unveraendert. Nutzung (im hermes-agent-Container, wo hermes_cli installiert ist): docker cp scripts/spotify_manual_auth.py hermes-agent:/tmp/spotify_manual_auth.py docker exec -it hermes-agent python3 /tmp/spotify_manual_auth.py Du kannst dieselbe Spotify-Dev-App wie fuer ARIA wiederverwenden -- dafuer nur im Spotify-Dashboard (developer.spotify.com/dashboard -> eure App -> Settings -> Redirect URIs) zusaetzlich http://127.0.0.1:43827/spotify/callback eintragen. Ein Client-Secret wird hier NICHT gebraucht (PKCE, public client) -- nur die Client-ID. """ from __future__ import annotations import sys import uuid try: from hermes_cli.auth import ( _auth_store_lock, _load_auth_store, _save_auth_store, _spotify_accounts_base_url, _spotify_api_base_url, _spotify_build_authorize_url, _spotify_client_id, _spotify_code_challenge, _spotify_code_verifier, _spotify_exchange_code_for_tokens, _spotify_redirect_uri, _spotify_scope_string, _spotify_token_payload_to_state, _store_provider_state, get_provider_auth_state, AuthError, ) except ImportError as exc: # pragma: no cover sys.exit( "Konnte hermes_cli.auth nicht importieren -- Skript muss INNERHALB " f"des hermes-agent-Containers laufen (python3 dort). Fehler: {exc}" ) def main() -> None: existing_state = get_provider_auth_state("spotify") or {} default_client_id = "" try: default_client_id = _spotify_client_id(state=existing_state) except AuthError: pass prompt = "Spotify Client-ID" prompt += f" [{default_client_id}]: " if default_client_id else ": " client_id = input(prompt).strip() or default_client_id if not client_id: sys.exit("Keine Client-ID angegeben -- Abbruch.") redirect_uri = _spotify_redirect_uri(state=existing_state) scope = _spotify_scope_string(existing_state.get("scope")) accounts_base_url = _spotify_accounts_base_url(existing_state) api_base_url = _spotify_api_base_url(existing_state) code_verifier = _spotify_code_verifier() code_challenge = _spotify_code_challenge(code_verifier) state_nonce = uuid.uuid4().hex authorize_url = _spotify_build_authorize_url( client_id=client_id, redirect_uri=redirect_uri, scope=scope, state=state_nonce, code_challenge=code_challenge, accounts_base_url=accounts_base_url, ) print() print("=" * 78) print("1) Diese URL in IRGENDEINEM Browser oeffnen (Handy reicht) und bei") print(" Spotify einloggen + 'Zugriff erlauben' bestaetigen:") print() print(authorize_url) print() print(f" (Diese Redirect-URI muss im Spotify-Dashboard eingetragen sein: {redirect_uri})") print() print("2) Der Browser versucht danach zu http://127.0.0.1:43827/... zu") print(" springen und wird das NICHT laden koennen -- das ist erwartet,") print(" da nichts auf DEINEM Geraet dort lauscht. Die Adresse bleibt in") print(" der Adressleiste sichtbar (oder in der Fehlermeldung).") print("3) Den Wert hinter 'code=' kopieren (bis exklusive '&state=').") print("=" * 78) print() pasted_code = input("code=... hier einfuegen: ").strip() if not pasted_code: sys.exit("Kein Code eingegeben -- Abbruch.") pasted_state = input( f"state=... hier einfuegen (erwartet '{state_nonce}', Enter zum Ueberspringen): " ).strip() if pasted_state and pasted_state != state_nonce: sys.exit( "State-Wert stimmt nicht mit dem erwarteten ueberein -- Abbruch " "(evtl. eine alte URL/Fenster benutzt, bitte Skript neu starten)." ) print("\nTausche Code gegen Access-/Refresh-Token...") token_payload = _spotify_exchange_code_for_tokens( client_id=client_id, code=pasted_code, redirect_uri=redirect_uri, code_verifier=code_verifier, accounts_base_url=accounts_base_url, ) spotify_state = _spotify_token_payload_to_state( token_payload, client_id=client_id, redirect_uri=redirect_uri, requested_scope=scope, accounts_base_url=accounts_base_url, api_base_url=api_base_url, ) with _auth_store_lock(): auth_store = _load_auth_store() _store_provider_state(auth_store, "spotify", spotify_state, set_active=False) saved_to = _save_auth_store(auth_store) print() print(f"Spotify-Login gespeichert unter: {saved_to}") print("providers.spotify ist jetzt gesetzt.") print("Nicht vergessen: `docker exec -it hermes-agent hermes tools` -> Spotify aktivieren, falls noch nicht geschehen.") if __name__ == "__main__": main()