From d28df346f215bbbca50cf9c51fdf6c047c8d8020 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 16:28:55 +0200 Subject: [PATCH] Wiedergabe: ohne Basisprofil lehnt der Transcoder alles ab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Unterschied zwischen 400 und 200 war ein einziger Parameter: …&protocol=hls&X-Plex-Token=… → 400 …&protocol=hls&X-Plex-Client-Profile-Name=Chrome&… → 200 Flixy hat immer nur `X-Plex-Client-Profile-Extra` geschickt. Das **erweitert** ein Profil, es ersetzt keins — ohne Basis gibt es nichts zu erweitern, und der Server kann keine Entscheidung bilden. Seine Antwort darauf ist eine nackte 400 ohne jeden Hinweis. Das betrifft alles, was durch den Transcoder geht: die Qualitätswahl im Player, komprimierte Downloads und Live-TV. Bei Filmen fiel es nicht auf, weil dort DirectPlay greift. „Chrome" als Basis, weil dieses Profil auf jedem Server vorhanden ist und genau das kann, was AVPlayer kann: HLS mit H.264 und AAC. Die Feinheiten für iOS stehen weiterhin im Zusatz. Gefunden wurde es, indem die Anfragen Angabe für Angabe aufgebaut wurden, bis eine davon umschlug — nicht durch Nachdenken. Co-Authored-By: Claude Opus 5 --- .../FlixyKit/Sources/FlixyProbe/main.swift | 15 +++++++ .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 40 +++++++++++++++++++ .../Endpoints/PlexServer+Playback.swift | 13 ++++++ .../Tests/PlexKitTests/LiveTVGuideTests.swift | 14 +++++++ 4 files changed, 82 insertions(+) diff --git a/Packages/FlixyKit/Sources/FlixyProbe/main.swift b/Packages/FlixyKit/Sources/FlixyProbe/main.swift index 360c58f..c03d8d8 100644 --- a/Packages/FlixyKit/Sources/FlixyProbe/main.swift +++ b/Packages/FlixyKit/Sources/FlixyProbe/main.swift @@ -68,6 +68,12 @@ let selector = PlexConnectionSelector(identity: identity) let chosen: PlexConnection? if forceLocal { chosen = resource.connections.first { $0.local } +} else if let index = CommandLine.arguments.firstIndex(of: "--weg"), + let number = CommandLine.arguments.dropFirst(index + 1).first.flatMap(Int.init), + resource.connections.indices.contains(number) { + // Ein bestimmter Weg, zum Vergleich: die Vermittlung von Plex nimmt + // einen anderen Weg als die Portweiterleitung nach Hause. + chosen = resource.connections[number] } else { chosen = await selector.selectConnection(for: resource) } @@ -83,6 +89,15 @@ print("\nGenommen: \(connection.local ? "lokal" : "entfernt") — \(connection.u // `swift run FlixyProbe /pfad /noch-ein-pfad …` fragt genau diese Adressen ab // und zeigt die Antwort. Dafür ist das Werkzeug da: eine Schnittstelle ohne // Beschreibung erkundet man, indem man sie fragt, nicht indem man rät. +// Was hinter `--try` steht, ist der Einzelversuch und keine freie Abfrage. +let tryTarget = CommandLine.arguments.firstIndex(of: "--try") + .flatMap { CommandLine.arguments.dropFirst($0 + 1).first } + +if let tryTarget { + print(await server.attempt(target: tryTarget, headers: !CommandLine.arguments.contains("--bare"))) + exit(0) +} + var requested = CommandLine.arguments.dropFirst().filter { !$0.hasPrefix("--") } // Was hinter `--channel` steht, ist keine Adresse. if let wanted = CommandLine.arguments.drop(while: { $0 != "--channel" }).dropFirst().first { diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index f3ced83..1599e67 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -1635,3 +1635,43 @@ extension PlexServer { return await probeStream(url: url) } } + + +extension PlexServer { + /// Schickt eine beliebige Anfrage und zeigt Status und Antwort. + /// + /// Zum Prüfen einzelner Vermutungen. `headers:` entscheidet, ob die + /// Anfrage so aussieht wie die der App oder wie die des Systemplayers — + /// der schickt keine Kopfzeilen mit, und dieser Unterschied war schon + /// einmal die Ursache. + public func attempt(target: String, headers: Bool = true) async -> String { + guard let baseURL = connection.url else { return "keine Verbindung" } + + let pieces = target.split(separator: "?", maxSplits: 1) + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) + else { return "Adresse nicht baubar" } + components.percentEncodedPath = String(pieces[0]) + if pieces.count > 1 { + components.percentEncodedQuery = String(pieces[1]) + .replacingOccurrences(of: "{token}", with: token) + } + guard let url = components.url else { return "Adresse nicht baubar" } + + var request = URLRequest(url: url) + request.timeoutInterval = 25 + if headers { + for (field, value) in identity.headers { request.setValue(value, forHTTPHeaderField: field) } + request.setValue(token, forHTTPHeaderField: "X-Plex-Token") + } + + do { + let (data, response) = try await transport.data(for: request) + let status = (response as? HTTPURLResponse)?.statusCode ?? 0 + let body = String(decoding: data.prefix(500), as: UTF8.self) + .trimmingCharacters(in: .whitespacesAndNewlines) + return "HTTP \(status)\n\(redacted(body))" + } catch { + return Self.shortDescription(of: error) + } + } +} diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift index 36c3f28..71c087f 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift @@ -132,6 +132,19 @@ extension PlexServer { /// schwarz. private nonisolated var clientProfileItems: [URLQueryItem] { [ + // Das **Basisprofil**. Ohne diese eine Angabe lehnt der Server + // jede Anfrage an den Transcoder mit einer nackten 400 ab — und + // zwar jede, auch die an eine gewöhnliche Datei. + // + // Der Grund ist banal und war zwei Tage lang unsichtbar: + // `X-Plex-Client-Profile-Extra` **erweitert** ein Profil, es + // ersetzt keins. Ohne Basis gibt es nichts zu erweitern, und der + // Server kann keine Entscheidung bilden. + // + // „Chrome", weil dieses Profil auf jedem Server vorhanden ist und + // genau das kann, was auch AVPlayer kann: HLS mit H.264 und AAC. + // Die Feinheiten für iOS stehen ohnehin im Zusatz darunter. + URLQueryItem(name: "X-Plex-Client-Profile-Name", value: "Chrome"), URLQueryItem( name: "X-Plex-Client-Profile-Extra", value: [ diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift index 199a187..35951c0 100644 --- a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -349,6 +349,20 @@ struct LiveTVGuideTests { #expect(text.split(separator: "&").filter { $0.hasPrefix("protocol=") }.count == 1) } + @Test("Jede Wiedergabe nennt ein Basisprofil") + func alwaysNamesBaseProfile() async throws { + // Ohne `X-Plex-Client-Profile-Name` lehnt der Server jede Anfrage an + // den Transcoder mit einer nackten 400 ab. `…-Profile-Extra` + // erweitert ein Profil, es ersetzt keins — ohne Basis gibt es nichts + // zu erweitern. Das hat zwei Tage gekostet. + let server = server(routes: [:]) + let url = try #require( + await server.transcodeURL(for: TranscodeRequest(ratingKey: "5", isLocal: true)) + ).absoluteString + + #expect(url.contains("X-Plex-Client-Profile-Name=Chrome")) + } + @Test("Jede Wiedergabe meldet, dass sie die Entscheidungsantwort versteht") func alwaysSendsHasMDE() { // Plex' eigener Client schickt `hasMDE=1` bei jeder Wiedergabe mit.