From c24938590da486fa9fb0176ac37a3399e28bc96c Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 15:28:49 +0200 Subject: [PATCH] Live-TV: die Sitzung des Servers umrechnen, und hasMDE mitschicken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nachgelesen im Quelltext des Plex-Clients, Funktion `video`: path: server.resolveUrl(item.get("key")), // /livetv/sessions/1268be73-… hasMDE: 1, mediaIndex: …, partIndex: …, protocol: "hls", … Zwei Dinge fehlten. Umgerechnet wird die **Sitzung, die der Server beim Belegen angelegt hat** — nicht die Abspielliste darin, nicht die Sendung beim Programmführer, und schon gar nicht die Kennung, die ich selbst erzeuge. Genau die hatte ich mehrfach eingesetzt und mich über 404 gewundert. Und `hasMDE=1` — „ich verstehe die Entscheidungsantwort". Plex' Client schickt das bei jeder Wiedergabe mit. Es fehlte bei allen bisherigen Versuchen, auch bei denen, die sonst richtig waren. Der Schlüssel wird suchend gelesen und dabei von der Abspielliste unterschieden: beide beginnen mit /livetv/sessions/, aber nur eine endet auf .m3u8. Co-Authored-By: Claude Opus 5 --- .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 50 +++++++++++++++++-- .../PlexKit/Endpoints/TranscodeRequest.swift | 4 ++ .../Tests/PlexKitTests/LiveTVGuideTests.swift | 26 ++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 5b21d97..c568e02 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -687,9 +687,24 @@ extension PlexServer { var probes = [ChannelProbe(path: "POST \(path)", result: "Tuner belegt: \(part)")] - // Erst unmittelbar: was beim Belegen herauskommt, ist bereits eine - // fertige HLS-Abspielliste. Sie an den Transcoder zu geben heißt, ihn - // um die Umrechnung von etwas zu bitten, das schon fertig ist. + // 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. + if let sessionKey = Self.firstSessionKey(in: data) { + let outcome = await attemptStream( + path: sessionKey, + ratingKey: channel.id, + mediaIndex: 0, + includeProfile: true, + maximumBitrate: maximumBitrate, + session: session + ) + probes.append(outcome.probe) + if let url = outcome.url { return (url, subscription, probes) } + } + + // Und die Abspielliste unmittelbar. Sie antwortete mit 404 statt 400: + // die Adresse stimmt, die Ressource steht nur nicht bereit. if let direct = directStreamURL(forPath: part) { let outcome = await probeStream(url: direct) probes.append(ChannelProbe(path: "\(part) (unmittelbar)", result: outcome)) @@ -708,6 +723,35 @@ extension PlexServer { return (outcome.url, subscription, probes) } + /// Der Schlüssel der Live-Sitzung, die der Server angelegt hat. + /// + /// `/livetv/sessions/1268be73-…` — die Sitzung des **Servers**, nicht die + /// Kennung, die der Client für seine Wiedergabe erzeugt. Ich habe + /// mehrfach die eigene eingesetzt und mich über 404 gewundert. + static func firstSessionKey(in data: Data) -> String? { + guard let root = try? JSONSerialization.jsonObject(with: data) else { return nil } + return firstSessionKey(in: root) + } + + private static func firstSessionKey(in value: Any) -> String? { + if let dictionary = value as? [String: Any] { + if let key = dictionary["key"] as? String, + key.hasPrefix("/livetv/sessions/"), + !key.hasSuffix(".m3u8") { + return key + } + for nested in dictionary.values { + if let found = firstSessionKey(in: nested) { return found } + } + } + if let array = value as? [Any] { + for nested in array { + if let found = firstSessionKey(in: nested) { return found } + } + } + return nil + } + /// Die Kennung des Aufnahme-/Live-Vorgangs in einer Antwort. static func subscriptionKey(in data: Data) -> String? { guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/TranscodeRequest.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/TranscodeRequest.swift index bc4f407..958b347 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/TranscodeRequest.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/TranscodeRequest.swift @@ -74,6 +74,10 @@ public struct TranscodeRequest: Sendable, Equatable { var queryItems: [URLQueryItem] { var items = [ URLQueryItem(name: "path", value: percentEncodedPath), + // „Ich verstehe die Entscheidungsantwort." Plex' eigener Client + // schickt das bei jeder Wiedergabe mit. Ohne diese Angabe lehnt + // der Server eine Live-Sitzung mit einer nackten 400 ab. + URLQueryItem(name: "hasMDE", value: "1"), URLQueryItem(name: "mediaIndex", value: String(mediaIndex)), URLQueryItem(name: "partIndex", value: String(partIndex)), URLQueryItem(name: "protocol", value: "hls"), diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift index 5829733..6993836 100644 --- a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -334,6 +334,32 @@ struct LiveTVGuideTests { #expect(result.subscriptionKey == "77") } + @Test("Umgerechnet wird die Sitzung des Servers, nicht die eigene Kennung") + func transcodesTheServersSession() throws { + // `/livetv/sessions/1268be73-…` ist die Sitzung, die der Server beim + // Belegen angelegt hat. Ich habe mehrfach meine eigene erzeugte + // Kennung eingesetzt und mich über 404 gewundert. + let json = Data(""" + {"MediaContainer":{"MediaSubscription":[{"key":"77","MetadataItem":{ + "key":"/livetv/sessions/1268be73-1598-4fb9-ac9e-35825eba3fdd","live":"1", + "Media":[{"id":1,"Part":[{"key":"/livetv/sessions/1268be73/ABC/index.m3u8?offset=-1"}]}]}}]}} + """.utf8) + + // Die Abspielliste selbst ist nicht gemeint — sie endet auf .m3u8. + #expect(PlexServer.firstSessionKey(in: json) + == "/livetv/sessions/1268be73-1598-4fb9-ac9e-35825eba3fdd") + } + + @Test("Jede Wiedergabe meldet, dass sie die Entscheidungsantwort versteht") + func alwaysSendsHasMDE() { + // Plex' eigener Client schickt `hasMDE=1` bei jeder Wiedergabe mit. + // Ohne diese Angabe lehnt der Server eine Live-Sitzung mit einer + // nackten 400 ab — ohne zu sagen, was ihm fehlt. + let items = TranscodeRequest(ratingKey: "5", isLocal: true).queryItems + + #expect(items.contains { $0.name == "hasMDE" && $0.value == "1" }) + } + @Test("Die Teil-Adresse wird gefunden, egal wie tief sie liegt") func findsPartAtAnyDepth() throws { // Der Vorgang liegt je nach Serverfassung unterschiedlich tief