diff --git a/README.md b/README.md index a55633d..cb093a4 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,25 @@ aufmachen: ssh -N -L 43827:127.0.0.1:43827 @ ``` +**Kein SSH-Port-Forward moeglich?** Hermes validiert `redirect_uri` hart auf +`127.0.0.1`/`localhost` (kein oeffentlicher Callback moeglich), aber der +lokale Listener laesst sich umgehen: `scripts/spotify_manual_auth.py` baut +dieselbe PKCE-Autorisierungs-URL, wartet aber nicht auf den Callback, +sondern fragt Dich nach dem `code`-Parameter, den Du von Hand aus der +(fehlschlagenden) Redirect-URL in der Adressleiste kopierst. Das Ergebnis +landet 1:1 im selben Format in `~/.hermes/auth.json` wie beim normalen +Login. +```bash +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 +``` +Details/Ablauf stehen im Docstring des Skripts. Du kannst dabei dieselbe +Spotify-Dev-App wie fuer ARIA wiederverwenden — dafuer im +[Spotify-Dashboard](https://developer.spotify.com/dashboard) bei der +bestehenden App unter *Settings → Redirect URIs* zusaetzlich +`http://127.0.0.1:43827/spotify/callback` eintragen (kein neues +Client-Secret noetig, PKCE braucht nur die Client-ID). + Hinweis: Free-Spotify-Account reicht fuer Suche/Playlists/Library, fuer echte Playback-Steuerung (play/pause/skip auf einem Geraet) braucht's Spotify Premium. diff --git a/scripts/spotify_manual_auth.py b/scripts/spotify_manual_auth.py new file mode 100644 index 0000000..5588f5c --- /dev/null +++ b/scripts/spotify_manual_auth.py @@ -0,0 +1,156 @@ +#!/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()