From 6a6b320f37946cd0b397c3d88454a90ef50a076a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 30 Sep 2026 21:50:43 +0200 Subject: [PATCH] fix(jellyfin): eingeschlafene Sitzung erkennen und selbst neu aufbauen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim zweiten Film ging nichts mehr ('Session abgelaufen') — ARIA musste Jellyfin von Hand neu starten. Ursache: Jellyfin listet Clients noch, wenn die App laengst weg ist, und quittiert den Play-Befehl auch dann mit Erfolg. Es lief nur nichts. - /Sessions wird jetzt mit activeWithinSeconds gefiltert (Play + devices) — Karteileichen tauchen gar nicht erst als Ziel auf. - _jellyfin_playing(): nach dem Play-Befehl wird nachgesehen, ob NowPlayingItem wirklich gesetzt ist (bis ~6s). Ohne das meldet man faelschlich Erfolg. - Laeuft nichts und ist 'host' gesetzt: Jellyfin wird ueber den Host-Agenten neu gestartet (host_app_launch), frische Sitzung geholt, einmal nachgelegt — also genau der Handgriff, den ARIA vorher manuell machen musste. Ohne 'host' gibt es eine klare Handlungsanweisung statt eines stillen Fehlschlags. - Tool-Beschreibung draengt auf 'host', damit die Selbstheilung greift. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 82 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 130984a..cae4df1 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1549,14 +1549,18 @@ META_TOOLS = [ "description": ( "Startet die Wiedergabe eines Titels auf einem Jellyfin-Geraet. Ablauf: " "1. jellyfin_search -> itemId, 2. jellyfin_devices -> Geraet/Sitzung, " - "3. jellyfin_play. Laeuft die Jellyfin-App auf dem Zielgeraet noch nicht, " - "vorher host_app_launch (query='Jellyfin') aufrufen." + "3. jellyfin_play. Prueft selbst nach, ob die Wiedergabe wirklich " + "angelaufen ist. GIB IMMER 'host' MIT (Name des Host-Agenten am " + "Fernseher, z.B. 'Fire-TV Schlafzimmer'): dann starte ich Jellyfin bei " + "einer eingeschlafenen Sitzung automatisch neu und lege nochmal nach, " + "statt faelschlich Erfolg zu melden." ), "parameters": { "type": "object", "properties": { "item_id": {"type": "string", "description": "itemId aus jellyfin_search."}, "device": {"type": "string", "description": "Geraete-/Sitzungsname aus jellyfin_devices (z.B. 'Fire TV')."}, + "host": {"type": "string", "description": "Host-Agent am Geraet (fuer automatischen Neustart bei toter Sitzung)."}, "server": {"type": "string", "description": "Server-Name (optional)."}, "resume": {"type": "boolean", "description": "Wenn moeglich fortsetzen statt von vorn (Default true)."}, }, @@ -2876,9 +2880,15 @@ class Agent: except Exception as exc: return None, f"Jellyfin nicht erreichbar: {exc}" - def _jellyfin_find_session(self, server: dict, device: str): - """Sucht die Sitzung eines Geraets. Gibt (session, fehlertext).""" - sessions, err = self._jellyfin_call(server, "/Sessions") + def _jellyfin_find_session(self, server: dict, device: str, fresh_sec: int = 600): + """Sucht die Sitzung eines Geraets. Gibt (session, fehlertext). + + `activeWithinSeconds` filtert eingeschlafene Sitzungen aus: Jellyfin + listet Clients noch eine Weile, nachdem die App laengst weg ist — ein + Play-Befehl an so eine Karteileiche verpufft wortlos. + """ + sessions, err = self._jellyfin_call( + server, "/Sessions", params={"activeWithinSeconds": fresh_sec}) if err: return None, err d = (device or "").strip().lower() @@ -2895,6 +2905,27 @@ class Agent: f"Geraet GEOEFFNET sein — sonst vorher host_app_launch " f"(query='Jellyfin') aufrufen.") + def _jellyfin_playing(self, server: dict, sess: dict, item_id: str, + tries: int = 4, wait: float = 1.5) -> bool: + """Prueft, ob auf der Sitzung wirklich etwas abgespielt wird. + + Jellyfin quittiert einen Play-Befehl auch dann mit 204, wenn die Sitzung + laengst tot ist — ohne diese Kontrolle meldet man faelschlich Erfolg. + """ + want = (item_id or "").strip() + for _ in range(max(1, tries)): + time.sleep(wait) + sessions, err = self._jellyfin_call(server, "/Sessions", timeout=10.0) + if err: + continue + for s in (sessions or []): + if s.get("Id") != sess.get("Id"): + continue + now = s.get("NowPlayingItem") or {} + if now and (not want or now.get("Id") == want): + return True + return False + def _dispatch_jellyfin(self, name: str, arguments: dict) -> str: server = self._jellyfin_server(arguments.get("server") or "") if not server: @@ -2925,7 +2956,10 @@ class Agent: return f"Treffer auf {label}:\n" + "\n".join(lines) if name == "jellyfin_devices": - sessions, err = self._jellyfin_call(server, "/Sessions") + # Nur Sitzungen, die zuletzt aktiv waren — sonst listet Jellyfin + # auch laengst geschlossene Apps, an die kein Befehl mehr geht. + sessions, err = self._jellyfin_call( + server, "/Sessions", params={"activeWithinSeconds": 600}) if err: return f"FEHLER: {err}" out = [] @@ -2967,8 +3001,40 @@ class Agent: params=play_params) if err: return f"FEHLER: {err}" - return (f"OK — Wiedergabe auf '{sess.get('DeviceName')}' gestartet" - f"{note} (Server {label}).") + + # Nachsehen, ob wirklich etwas laeuft. Eine eingeschlafene Sitzung + # nimmt den Befehl wortlos an, ohne dass der Film startet — genau + # der Fall "beim zweiten Mal ging es nicht". + if self._jellyfin_playing(server, sess, item_id): + return (f"OK — Wiedergabe auf '{sess.get('DeviceName')}' gestartet" + f"{note} (Server {label}).") + + # Sitzung war tot. Wenn wir den zugehoerigen Agenten kennen: + # Jellyfin dort neu starten, frische Sitzung holen, einmal + # nachlegen. Sonst ehrlich sagen, was zu tun ist. + host = (arguments.get("host") or "").strip() + if not host: + return ("FEHLER: Der Befehl wurde angenommen, aber es laeuft nichts — " + "die Jellyfin-Sitzung auf dem Geraet war eingeschlafen. " + "Starte die Jellyfin-App neu (host_app_launch mit " + "query='Jellyfin') und versuche es erneut. Tipp: Gib " + "jellyfin_play den Parameter 'host' mit (Name des " + "Host-Agenten), dann mache ich das automatisch.") + self._dispatch_host("host_app_launch", {"host": host, "query": "Jellyfin"}) + time.sleep(6) # App braucht einen Moment, bis sie sich anmeldet + sess2, err2 = self._jellyfin_find_session(server, device, fresh_sec=120) + if err2: + return (f"FEHLER: Jellyfin auf '{host}' neu gestartet, aber es meldet " + f"sich keine Sitzung: {err2}") + self._jellyfin_call(server, f"/Sessions/{sess2['Id']}/Playing", + method="POST", params=play_params) + if self._jellyfin_playing(server, sess2, item_id): + return (f"OK — Wiedergabe laeuft auf '{sess2.get('DeviceName')}'" + f"{note}. (Die Sitzung war eingeschlafen, ich habe Jellyfin " + f"auf '{host}' neu gestartet.)") + return (f"FEHLER: Auch nach dem Neustart von Jellyfin auf '{host}' startet " + f"die Wiedergabe nicht. Laeuft die App auf dem Bildschirm? " + f"Ggf. dort einmal anmelden.") if name == "jellyfin_control": device = (arguments.get("device") or "").strip()