Spotify: manueller PKCE-Login ohne SSH-Port-Forward (Redirect-URI ist hart auf localhost validiert)
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user