#!/usr/bin/env python3 """Zero-LLM Spotify-Transportsteuerung fuer Hermes' offizielle `quick_commands`. Hintergrund: Wir hatten zuerst ein eigenes Fast-Path-Plugin gebaut (Regex vor dem LLM abfangen), das ist wieder raus -- der Hook (`pre_gateway_dispatch`), auf den es registriert war, feuert nur fuer die Gateway-Plattformen (Telegram/Discord/...), NICHT fuer die TUI (siehe `gateway/run.py` vs. `tui_gateway/server.py` im echten Hermes-Sourcecode). Ein eigener Patch an `tui_gateway/server.py` waere noetig gewesen -- riskant, weil die Funktion dort ~700 Zeilen Concurrency-kritischen Code enthaelt (History-Lock, Busy-Queue, Session-State). Hermes hat dafuer aber bereits einen EIGENEN, offiziellen Mechanismus: `quick_commands` (type: exec) in config.yaml. Laut Sourcecode (cli.py:process_command, tui_gateway/server.py, gateway/run.py) bypassen die NACHWEISLICH den Agent-Loop komplett -- kein LLM-Call, keine Tokens -- und funktionieren auf ALLEN Plattformen (CLI/TUI, Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant), offiziell dokumentiert unter website/docs/user-guide/configuration.md#quick-commands. Der Haken (ehrlich, nicht schoengeredet): quick_commands reichen KEINE Argumente durch -- jeder Befehl ist ein fester Slash-Befehl (z.B. "/next"), kein Freitext wie ARIAs Regex-`fast_patterns`. Fuer feste Steuerbefehle (naechstes Lied, Pause, lauter/leiser) reicht das aber exakt so gut wie Alexas Intent-Slots -- nur ohne Slot-Fuellung. Freitext-Faelle ("spiel Playlist X auf Geraet Y ab") bleiben bewusst beim LLM, weil das ohne Modell so oder so nicht geht (siehe README). Ein Skript, ein Argument pro Aktion -- vermeidet zehn Mini-Skripte. Nutzt denselben `SpotifyClient` (plugins.spotify.client) wie der echte Tool-Dispatch: gleiche OAuth-Session, kein doppelter Token-Code. Aufruf (aus quick_commands heraus, IM Container): python3 /opt/data/scripts/spotify_quick.py next python3 /opt/data/scripts/spotify_quick.py previous python3 /opt/data/scripts/spotify_quick.py pause python3 /opt/data/scripts/spotify_quick.py play python3 /opt/data/scripts/spotify_quick.py volume_up python3 /opt/data/scripts/spotify_quick.py volume_down python3 /opt/data/scripts/spotify_quick.py current """ from __future__ import annotations import sys try: from plugins.spotify.client import ( SpotifyAPIError, SpotifyAuthRequiredError, SpotifyClient, SpotifyError, ) except ImportError as exc: # pragma: no cover sys.exit( "Konnte plugins.spotify.client nicht importieren -- Skript muss " f"INNERHALB des hermes-agent-Containers laufen. Fehler: {exc}" ) VOLUME_STEP = 10 def _current_volume(client: "SpotifyClient") -> int: state = client.get_playback_state() or {} device = state.get("device") or {} vol = device.get("volume_percent") return int(vol) if isinstance(vol, (int, float)) else 50 def run(action: str) -> str: client = SpotifyClient() if action == "next": client.skip_next() return "⏭ naechstes Lied" if action == "previous": client.skip_previous() return "⏮ vorheriges Lied" if action == "pause": client.pause_playback() return "⏸ pausiert" if action == "play": client.start_playback() return "▶ weiter" if action == "volume_up": new_vol = min(100, _current_volume(client) + VOLUME_STEP) client.set_volume(volume_percent=new_vol) return f"🔊 Lautstaerke {new_vol}%" if action == "volume_down": new_vol = max(0, _current_volume(client) - VOLUME_STEP) client.set_volume(volume_percent=new_vol) return f"🔉 Lautstaerke {new_vol}%" if action == "current": state = client.get_currently_playing() or {} item = state.get("item") or {} name = item.get("name") artists = ", ".join(a.get("name", "") for a in (item.get("artists") or [])) if not name: return "Gerade laeuft nichts." return f"Läuft: {name} – {artists}" if artists else f"Läuft: {name}" return ( f"Unbekannte Aktion '{action}'. Erlaubt: next, previous, pause, play, " "volume_up, volume_down, current" ) def main() -> None: if len(sys.argv) < 2: sys.exit("Usage: spotify_quick.py ") action = sys.argv[1].strip().lower() try: print(run(action)) except SpotifyAuthRequiredError as exc: sys.exit(f"Spotify nicht eingeloggt: {exc}") except SpotifyAPIError as exc: # Echten error.reason zitieren statt zu raten (z.B. NO_ACTIVE_DEVICE, # ALREADY_PAUSED, PREMIUM_REQUIRED) -- gleiche Regel wie ueberall sonst. sys.exit(f"Spotify-Fehler (status={exc.status_code}): {exc}") except SpotifyError as exc: sys.exit(f"Spotify-Fehler: {exc}") if __name__ == "__main__": main()