From 3467f0f69057d995d3fbf35bdd3c105f9ec22eed Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 15:32:20 +0200 Subject: [PATCH] =?UTF-8?q?Wiedergabe:=20das=20Ger=C3=A4teprofil=20zerfiel?= =?UTF-8?q?=20in=20zwei=20Dutzend=20eigene=20Parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `URLComponents` kodiert `&` und `=` innerhalb eines Abfragewerts nicht. Das Geräteprofil steckt voll davon: X-Plex-Client-Profile-Extra=add-transcode-target(type=videoProfile& context=streaming&protocol=hls&container=mpegts&…) Der Server hat das nicht als ein Profil gelesen, sondern als zwei Dutzend eigene Angaben — darunter ein zweites `protocol=`. Eine widersprüchliche Anfrage, und die Antwort darauf ist eine nackte 400 ohne Hinweis. Der Fehler steckte in jeder Transcoder-Adresse, seit es sie gibt. Bei Dateien fiel er nicht auf, weil dort meist DirectPlay greift und der Transcoder gar nicht gefragt wird. Kodiert wird der **ursprüngliche** Wert, nicht der bereits von `URLComponents` behandelte: den ein zweites Mal zu kodieren macht aus `%3D` ein `%253D` — derselbe Fehler, nur eine Ebene tiefer. Der Test prüft beides: dass das Profil ein einziger Parameter bleibt und dass `protocol` genau einmal vorkommt. Co-Authored-By: Claude Opus 5 --- .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 22 ++++---- .../Endpoints/PlexServer+Playback.swift | 50 +++++++++++++++---- .../Tests/PlexKitTests/LiveTVGuideTests.swift | 21 ++++++++ 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index c568e02..8047db1 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -691,16 +691,18 @@ extension PlexServer { // 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) } + for withProfile in [true, false] { + let outcome = await attemptStream( + path: sessionKey, + ratingKey: channel.id, + mediaIndex: 0, + includeProfile: withProfile, + 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: diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift index 62f474b..e7e716d 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift @@ -32,20 +32,38 @@ extension PlexServer { ) else { return nil } - components.queryItems = request.queryItems + [ + let items = request.queryItems + [ URLQueryItem(name: "X-Plex-Token", value: token), ] + (includeProfile ? clientProfileItems : []) + identity.queryItems + components.queryItems = items - // `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 + components.percentEncodedQueryItems = components.percentEncodedQueryItems?.map { item in + // `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 item.name == "path", keepPathEncoding { + return URLQueryItem(name: "path", value: request.percentEncodedPath) } + + // Das Geräteprofil ist ein Wert voller `&`, `=` und Klammern — + // und `URLComponents` lässt genau die unangetastet. Der Server + // liest sie dann als eigene Parameter: aus einem Profil werden + // zwei Dutzend Angaben, darunter ein zweites `protocol=`. Das + // steckte in **jeder** Transcoder-Adresse, seit es sie gibt. + if item.name.hasSuffix("Profile-Extra"), + // Der **ursprüngliche** Wert, nicht der bereits von + // `URLComponents` behandelte: den ein zweites Mal zu kodieren + // macht aus `%3D` ein `%253D`. + let value = items.first(where: { $0.name == item.name })?.value { + return URLQueryItem( + name: item.name, + value: value.addingPercentEncoding(withAllowedCharacters: .plexParameterAllowed) + ?? value + ) + } + + return item } return components.url } @@ -298,3 +316,15 @@ extension PlexServer { return components.url } } + + +extension CharacterSet { + /// Was in einem Abfragewert unangetastet bleiben darf. + /// + /// Streng gehalten: alles andere wird kodiert. `&` und `=` sind hier die + /// eigentlichen Übeltäter — sie trennen Parameter, und `URLComponents` + /// kodiert sie in Werten nicht von selbst. + static let plexParameterAllowed = CharacterSet( + charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + ) +} diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift index 6993836..6e01747 100644 --- a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -350,6 +350,27 @@ struct LiveTVGuideTests { == "/livetv/sessions/1268be73-1598-4fb9-ac9e-35825eba3fdd") } + @Test("Das Geräteprofil zerfällt nicht in eigene Parameter") + func encodesProfileExtra() async throws { + // `URLComponents` lässt `&` und `=` in einem Wert unangetastet. Das + // Geräteprofil steckt voll davon, und der Server las es als zwei + // Dutzend eigene Angaben — darunter ein zweites `protocol=`. Der + // Fehler steckte in jeder Transcoder-Adresse, seit es sie gibt. + let server = server(routes: [:]) + let url = try #require( + await server.transcodeURL(for: TranscodeRequest(ratingKey: "5", isLocal: true)) + ) + let text = url.absoluteString + let profile = try #require(text.split(separator: "&") + .first { $0.hasPrefix("X-Plex-Client-Profile-Extra=") }) + + // Das ganze Profil steht in genau einem Parameter. + #expect(profile.contains("%26")) + #expect(profile.contains("add-transcode-target%28")) + // Und `protocol` kommt nur ein einziges Mal vor. + #expect(text.split(separator: "&").filter { $0.hasPrefix("protocol=") }.count == 1) + } + @Test("Jede Wiedergabe meldet, dass sie die Entscheidungsantwort versteht") func alwaysSendsHasMDE() { // Plex' eigener Client schickt `hasMDE=1` bei jeder Wiedergabe mit.