From 024f3966412cdfc2d4b923d79074e012c01a071b Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 14:58:47 +0200 Subject: [PATCH] Live-TV: die Kennung der Sendung muss zweimal kodiert werden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Bericht nennt zwei Zeilen, die alles erklären: Media.protocol = livetv ratingKey = plex%3A%2F%2Fepisode%2F6a9a1255715fa1c9375df709 Plex führt die Kennung selbst schon kodiert — auch im JSON, wo es dafür keinen Grund gäbe. Als Abfragewert geschickt dekodiert der Server sie einmal, bevor er sie als Pfad liest, und bekommt `plex://episode/…`. Das ist kein gültiger Schlüssel, und die Antwort darauf war die nackte „400 Bad Request"-Seite. Für eine Datei in der Mediathek ist das kein Thema: `/library/metadata/123` enthält nichts zu kodieren. Deshalb ist beides nötig, und welche Form gilt, hängt daran, woher die Adresse stammt. Ich habe in diesem Punkt inzwischen jede Variante einmal für die richtige gehalten — entpackt, einfach, doppelt. Ein Test hält jetzt alle drei Formen fest, damit die Frage nicht ein viertes Mal aufkommt. Co-Authored-By: Claude Opus 5 --- .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 24 +++++++++++-- .../Endpoints/PlexServer+Playback.swift | 34 +++++++++++++------ .../Tests/PlexKitTests/LiveTVGuideTests.swift | 23 +++++++++++++ 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index fe3efc2..18aa58f 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -446,7 +446,15 @@ extension PlexServer { // Ohne Profil als dritter Versuch: die Profilangaben stecken // voller `&` und `=`, die in einem Abfragewert mehrdeutig sind. // Trägt es ohne, liegt es daran — und das steht dann in der Spur. - let variants: [(index: Int, profile: Bool)] = [(0, true), (-1, true), (0, false)] + // Doppelt kodiert zuerst: der Server dekodiert den Abfragewert + // einmal, bevor er ihn als Pfad liest. Nur so kommt bei ihm + // wieder `plex%3A%2F%2Fepisode%2F…` an statt `plex://episode/…`. + let variants: [(index: Int, profile: Bool, keep: Bool)] = [ + (0, true, false), + (-1, true, false), + (0, true, true), + (0, false, false), + ] for variant in variants { attempts += 1 let outcome = await attemptStream( @@ -454,6 +462,7 @@ extension PlexServer { ratingKey: channel.id, mediaIndex: variant.index, includeProfile: variant.profile, + keepPathEncoding: variant.keep, maximumBitrate: maximumBitrate, session: session ) @@ -606,6 +615,7 @@ extension PlexServer { ratingKey: String, mediaIndex: Int, includeProfile: Bool, + keepPathEncoding: Bool = true, maximumBitrate: Int?, session: String ) async -> (url: URL?, probe: ChannelProbe) { @@ -619,12 +629,18 @@ extension PlexServer { pathOverride: path ) - guard let url = transcodeURL(for: request, includeProfile: includeProfile) else { + guard let url = transcodeURL( + for: request, + includeProfile: includeProfile, + keepPathEncoding: keepPathEncoding + ) else { return (nil, ChannelProbe(path: path, result: "Adresse nicht baubar")) } let outcome = await probeStream(url: url) - let label = "\(path) (mediaIndex \(mediaIndex)\(includeProfile ? "" : ", ohne Profil"))" + let label = "\(path) (mediaIndex \(mediaIndex)" + + "\(includeProfile ? "" : ", ohne Profil")" + + "\(keepPathEncoding ? "" : ", doppelt kodiert"))" return (outcome.hasPrefix("HTTP 200") ? url : nil, ChannelProbe(path: label, result: outcome)) } @@ -713,6 +729,8 @@ extension PlexServer { } paths += sessionKeys(channel: channel, program: program).map { "\(sessionPath)/\($0)" } + // Und die Sitzung ohne Schlüssel — bisher nie probiert. + paths.append(sessionPath) var seen = Set() return paths.filter { seen.insert($0).inserted } diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift index d39e22d..c3781cd 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift @@ -9,9 +9,21 @@ extension PlexServer { /// Bitrate während der Wiedergabe von selbst, je nachdem was die Leitung /// hergibt. Genau das ist die zugesagte automatische Qualitätsanpassung — /// sie wird nicht selbst gerechnet, sondern kommt mit dem Format. + /// - Parameter keepPathEncoding: Ob `path` unverändert eingesetzt wird. + /// + /// Hier steckt der Unterschied zwischen einer laufenden Sendung und + /// „400 Bad Request". Eine Datei in der Mediathek liegt unter + /// `/library/metadata/123` — da gibt es nichts zu kodieren. Eine Sendung + /// beim Programmführer liegt unter + /// `/…/metadata/plex%3A%2F%2Fepisode%2F…`, und der Server dekodiert den + /// Abfragewert einmal, bevor er ihn als Pfad liest. Unverändert + /// eingesetzt kommt bei ihm `plex://episode/…` an — kein gültiger + /// Schlüssel. Er braucht die Kennung ein zweites Mal kodiert, damit nach + /// seinem Dekodieren wieder `%3A%2F%2F` dasteht. public nonisolated func transcodeURL( for request: TranscodeRequest, - includeProfile: Bool = true + includeProfile: Bool = true, + keepPathEncoding: Bool = true ) -> URL? { guard let baseURL = connection.url, var components = URLComponents( @@ -24,16 +36,16 @@ extension PlexServer { URLQueryItem(name: "X-Plex-Token", value: token), ] + (includeProfile ? clientProfileItems : []) + identity.queryItems - // `path` wird von Hand eingesetzt, nachdem alles andere kodiert ist. - // Die Adresse kommt beim Programmanbieter bereits kodiert an, und - // beide Automatiken liegen daneben: `URLComponents` kodiert ein - // zweites Mal (%3A wird zu %253A), vorheriges Entpacken schreibt - // `plex://…` im Klartext in die Anfrage, und daran verschluckt sich - // der Server mit einer nackten 400-Seite. - components.percentEncodedQueryItems = components.percentEncodedQueryItems?.map { item in - item.name == "path" - ? URLQueryItem(name: "path", value: request.percentEncodedPath) - : item + // `path` von Hand einsetzen heißt: genau einmal kodiert. Das ist für + // Dateien richtig. Für eine Sendung beim Programmführer muss die + // Kennung ein zweites Mal kodiert werden — dann übernimmt das + // `URLComponents` von selbst, und hier ist nichts zu tun. + if keepPathEncoding { + components.percentEncodedQueryItems = components.percentEncodedQueryItems?.map { item in + item.name == "path" + ? URLQueryItem(name: "path", value: request.percentEncodedPath) + : item + } } return components.url } diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift index 86e89d2..c9fde4f 100644 --- a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -170,6 +170,29 @@ struct LiveTVGuideTests { #expect(!text.contains("plex://")) } + @Test("Für eine Sendung wird die Kennung zweimal kodiert") + func doubleEncodesForLive() async throws { + // Der Server dekodiert den Abfragewert einmal, bevor er ihn als Pfad + // liest. Genau einmal kodiert kommt bei ihm `plex://episode/…` an — + // kein gültiger Schlüssel, und die Antwort ist eine nackte 400. + let server = server(routes: [:]) + let request = TranscodeRequest( + ratingKey: "5", + isLocal: true, + pathOverride: "/tv.plex.providers.epg.cloud:2/metadata/plex%3A%2F%2Fepisode%2Fabc" + ) + + let once = try #require(await server.transcodeURL(for: request)).absoluteString + let twice = try #require( + await server.transcodeURL(for: request, keepPathEncoding: false) + ).absoluteString + + #expect(once.contains("plex%3A%2F%2Fepisode%2Fabc")) + #expect(twice.contains("plex%253A%252F%252Fepisode%252Fabc")) + // Und niemals im Klartext: daran verschluckt sich der Server. + #expect(!twice.contains("plex://")) + } + @Test("Nur was tatsächlich mit 200 antwortet, geht an den Player") func rejectsStreamsTheServerRefuses() async throws { // Vorher galt schon eine gebaute Adresse als Erfolg. Der Fehler