"""Audiofy – Verbindung Player → Server (Player-API mit Bearer-Token, nur ausgehend). Alle Aufrufe fangen Fehler selbst ab: Ist der Server nicht erreichbar, liefern sie None/False, und der Controller spielt einfach die Grund-Playlist weiter. Im Offline-Fall wird nur alle OFFLINE_RETRY_S Sekunden ein neuer Versuch gemacht. """ import logging import time from dataclasses import dataclass import httpx log = logging.getLogger("audiofy.server") OFFLINE_RETRY_S = 15 TIMEOUT_S = 5 @dataclass class Wunsch: id: int uri: str title: str = "" artist: str = "" priority: bool = False # "Als Nächstes" des Besitzers class ServerVerbindung: def __init__(self, basis_url: str, token: str): self._http = httpx.Client(base_url=basis_url.rstrip("/") + "/api/player", timeout=TIMEOUT_S, headers={"Authorization": f"Bearer {token}"}) self.online: bool | None = None self._naechster_versuch = 0.0 self._unbestaetigt: set[int] = set() # gespielte Wünsche, deren Meldung noch fehlt # --- intern ---------------------------------------------------------------- def _anfrage(self, methode: str, pfad: str, **kwargs): """JSON der Antwort oder None bei jedem Fehler (Netz, Timeout, HTTP-Fehler).""" if self.online is False and time.monotonic() < self._naechster_versuch: return None try: r = self._http.request(methode, pfad, **kwargs) if r.status_code == 401: self._offline("Token abgelehnt (401) – PLAYER_API_TOKEN prüfen") return None r.raise_for_status() if self.online is not True: log.info("Server erreichbar") self.online = True return r.json() if r.content else {} except (httpx.HTTPError, ValueError) as e: self._offline(f"{type(e).__name__}") return None def _offline(self, grund: str) -> None: if self.online is not False: log.warning("Server nicht erreichbar (%s) – spiele Grund-Playlist weiter, neuer Versuch alle %d s", grund, OFFLINE_RETRY_S) self.online = False self._naechster_versuch = time.monotonic() + OFFLINE_RETRY_S # --- Player-API ---------------------------------------------------------------- def konfiguration(self) -> dict | None: return self._anfrage("GET", "/config") def naechster_wunsch(self) -> Wunsch | None: self._bestaetigungen_nachholen() daten = self._anfrage("GET", "/next-wish") w = (daten or {}).get("wish") if not w or w["id"] in self._unbestaetigt: return None # noch nicht als gespielt bestätigt → nicht doppelt spielen return Wunsch(id=w["id"], uri=w["track_uri"], title=w.get("title", ""), artist=w.get("artist", ""), priority=bool(w.get("priority"))) def wunsch_gespielt(self, wunsch: Wunsch) -> None: self._unbestaetigt.add(wunsch.id) self._bestaetigungen_nachholen() def _bestaetigungen_nachholen(self) -> None: for wid in list(self._unbestaetigt): if self._anfrage("POST", f"/wish/{wid}/played") is not None: self._unbestaetigt.discard(wid) def melden(self, zustand: dict) -> bool: return self._anfrage("POST", "/now-playing", json=zustand) is not None def bluetooth_melden(self, zustand: dict) -> bool: return self._anfrage("POST", "/bluetooth", json=zustand) is not None def quittieren(self, befehl_id: int, ok: bool, meldung: str | None = None) -> None: self._anfrage("POST", f"/commands/{befehl_id}/ack", json={"ok": ok, "message": meldung})