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