Spotify: manueller PKCE-Login ohne SSH-Port-Forward (Redirect-URI ist hart auf localhost validiert)

This commit is contained in:
ARIA
2026-07-19 23:33:57 +00:00
parent f448538158
commit cb7bd566e6
2 changed files with 175 additions and 0 deletions
+19
View File
@@ -230,6 +230,25 @@ aufmachen:
ssh -N -L 43827:127.0.0.1:43827 <user>@<hermes-maschine>
```
**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.
+156
View File
@@ -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()