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.