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,