diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 19a7511..f3ced83 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -702,6 +702,22 @@ extension PlexServer { ), ] + // Die Sitzung selbst fragen — und **nur** ihr die Abspieladresse + // glauben. Die Antwort auf das Belegen nennt eine Adresse mit der + // Sitzungskennung darin; die Sitzung nennt dieselbe Adresse mit der + // **Gerätekennung**. Nur die zweite gibt es wirklich. Ich habe die + // erste genommen und mich über 404 gewundert. + if let sessionKey = Self.firstSessionKey(in: data), + let sessionData = try? await client.send( + PlexRequest(path: sessionKey, timeout: .seconds(20)) + ), + let livePart = Self.firstPartKey(in: sessionData), + let direct = directStreamURL(forPath: livePart) { + let outcome = await probeStream(url: direct) + probes.append(ChannelProbe(path: "\(livePart) (aus der Sitzung)", result: outcome)) + if outcome.hasPrefix("HTTP 200") { return (direct, subscription, probes) } + } + // 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 diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift index 8e27e07..199a187 100644 --- a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -359,6 +359,38 @@ struct LiveTVGuideTests { #expect(items.contains { $0.name == "hasMDE" && $0.value == "1" }) } + @Test("Die Abspieladresse kommt aus der Sitzung, nicht aus der Tune-Antwort") + func prefersPartFromSession() async throws { + // Beide Antworten nennen eine Adresse derselben Form, aber mit + // verschiedenen Kennungen darin: die Tune-Antwort setzt die + // Sitzungskennung ein, die Sitzung selbst die Gerätekennung. Nur die + // zweite gibt es wirklich — die erste ergab jedes Mal 404. + let tuned = """ + {"MediaContainer":{"size":1,"MediaSubscription":[{"key":"22", + "MetadataItem":{"key":"/livetv/sessions/abc","live":"1", + "Media":[{"id":1,"Part":[{"key":"/livetv/sessions/abc/SITZUNGSKENNUNG/index.m3u8"}]}]}}]}} + """ + let session = """ + {"MediaContainer":{"size":1,"Metadata":[{"key":"/livetv/sessions/abc","live":"1", + "Media":[{"id":1,"Part":[{"key":"/livetv/sessions/abc/GERAETEKENNUNG/index.m3u8?offset=-1.000000"}]}]}]}} + """ + let server = server(routes: [ + "/channels/kanal-7/tune": tuned, + "/livetv/sessions/abc": session, + // Nur unter der Gerätekennung liefert der Server eine + // Abspielliste; die andere Form gibt es nicht. + "/GERAETEKENNUNG/index.m3u8": "#EXTM3U", + ]) + let channel = PlexChannel(id: "kanal-7", title: "SRF 1 HD", key: "2") + + let result = await server.tune(channel: channel, dvr: PlexDVR(key: "2"), maximumBitrate: nil) + let url = try #require(result.url) + + #expect(url.path() == "/livetv/sessions/abc/GERAETEKENNUNG/index.m3u8") + #expect(!url.absoluteString.contains("SITZUNGSKENNUNG")) + #expect(!url.absoluteString.contains("/transcode/")) + } + @Test("Die Teil-Adresse wird gefunden, egal wie tief sie liegt") func findsPartAtAnyDepth() throws { // Der Vorgang liegt je nach Serverfassung unterschiedlich tief