From 2c66e61c71433850d40ce43b952d059314743aa0 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 15:50:20 +0200 Subject: [PATCH] =?UTF-8?q?Live-TV:=20abwarten,=20bis=20der=20Empf=C3=A4ng?= =?UTF-8?q?er=20abgestimmt=20hat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Antwort auf das Belegen sagt es deutlich: "status":"inprogress", "currentSize":0, "percent":-1.0 Der Tuner fängt in diesem Moment erst an. Ich habe im selben Atemzug nach dem Strom gefragt — nach etwas, das es noch nicht gab. Dass darauf 400 und 404 kommen, ist kein Rätsel, sondern die richtige Antwort auf eine zu frühe Frage. Der Vorgang hat eine eigene Adresse, unter der ihm zuzusehen ist: /media/grabbers/operations/… Dort wird jetzt gewartet, bis `currentSize` über null steht, höchstens zwölf Sekunden. Länger hieße, jemanden vor einem schwarzen Bild sitzen zu lassen. Dazu steht die Sitzung selbst im Bericht — dorthin zeigt der Pfad, den der Transcoder bekommt, und wie sie nach dem Abstimmen aussieht, habe ich noch nie gesehen. Co-Authored-By: Claude Opus 5 --- .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 4a6b608..81e0dc7 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -702,6 +702,27 @@ extension PlexServer { ), ] + // Abwarten, bis der Empfänger abgestimmt hat. Er meldet + // `status: inprogress` und `currentSize: 0` — es gibt in dem Moment + // noch keinen Strom, und jede Frage danach musste scheitern. Ein + // Tuner ist ein Stück Hardware; er braucht Sekunden, keine + // Millisekunden. + if let operation = Self.grabOperationKey(in: data) { + probes.append(ChannelProbe( + path: "Abstimmen abwarten", + result: await waitForGrab(atKey: operation) + )) + } + + // Und wie die Sitzung danach aussieht — dorthin zeigt der Pfad, den + // der Transcoder bekommt. + if let sessionKey = Self.firstSessionKey(in: data) { + probes.append(ChannelProbe( + path: sessionKey, + result: await rawResponse(forPath: sessionKey, limit: 700) + )) + } + // Der Weg des Plex-Clients: die **Sitzung**, die der Server gerade // angelegt hat, wird umgerechnet — nicht die Abspielliste darin und // nicht meine eigene Kennung. Ihr Schlüssel steht in der Antwort. @@ -771,6 +792,64 @@ extension PlexServer { return (outcome.url, subscription, probes) } + /// Die Adresse, unter der dem Empfänger beim Abstimmen zuzusehen ist. + static func grabOperationKey(in data: Data) -> String? { + guard let root = try? JSONSerialization.jsonObject(with: data) else { return nil } + return firstKey(in: root, matching: "/media/grabbers/operations/") + } + + private static func firstKey(in value: Any, matching prefix: String) -> String? { + if let dictionary = value as? [String: Any] { + if let key = dictionary["key"] as? String, key.hasPrefix(prefix) { return key } + for nested in dictionary.values { + if let found = firstKey(in: nested, matching: prefix) { return found } + } + } + if let array = value as? [Any] { + for nested in array { + if let found = firstKey(in: nested, matching: prefix) { return found } + } + } + return nil + } + + /// Wartet, bis der Empfänger Daten liefert. + /// + /// Höchstens zwölf Sekunden. Länger zu warten hieße, den Nutzer vor einem + /// schwarzen Bild sitzen zu lassen; gar nicht zu warten hieß bisher, nach + /// einem Strom zu fragen, den es noch nicht gibt. + private func waitForGrab(atKey key: String, limit: Int = 12) async -> String { + for second in 1...limit { + try? await Task.sleep(for: .seconds(1)) + + guard let data = try? await client.send( + PlexRequest(path: key, timeout: .seconds(10)) + ) else { continue } + + guard let root = try? JSONSerialization.jsonObject(with: data), + let size = Self.firstNumber(in: root, forKey: "currentSize") + else { continue } + + if size > 0 { return "nach \(second) s: \(size) Bytes" } + } + return "nach \(limit) s noch keine Daten" + } + + private static func firstNumber(in value: Any, forKey name: String) -> Int? { + if let dictionary = value as? [String: Any] { + if let number = dictionary[name] as? NSNumber { return number.intValue } + for nested in dictionary.values { + if let found = firstNumber(in: nested, forKey: name) { return found } + } + } + if let array = value as? [Any] { + for nested in array { + if let found = firstNumber(in: nested, forKey: name) { return found } + } + } + return nil + } + /// Die Antwort des Entscheiders im Wortlaut. private func rawDecision(percentEncodedItems items: [URLQueryItem]) async -> String { guard let baseURL = connection.url,