From 7987dbf9a4646d753e2ea7e63edfafb4ffd8fc29 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 15:12:51 +0200 Subject: [PATCH] Probe: den Entscheider fragen, wenn der Transcoder schweigt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Transcoder antwortet auf jede Form der Adresse mit einer nackten „400 Bad Request"-Seite — ohne Plex-Meldung, ohne Hinweis. Der Entscheider bekommt dieselben Angaben und gibt eine Plex-Antwort zurück. Das ist die letzte Stelle, an der dieser Server noch erklärt, woran es liegt. Schlägt das Einschalten fehl, zeigt der Probe deshalb jetzt zusätzlich die Antwort des Entscheiders — in beiden Kodierformen — sowie den Empfänger, das Gerät und /livetv/sessions im Wortlaut. Dazu kann der Probe beliebige Adressen abfragen: `swift run FlixyProbe /livetv/dvrs/2`. Eine Schnittstelle ohne Beschreibung erkundet man, indem man sie fragt. Der Token wird aus jeder Ausgabe entfernt, falls der Server ihn zurückspiegelt. Co-Authored-By: Claude Opus 5 --- .../FlixyKit/Sources/FlixyProbe/main.swift | 29 ++++++++ .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 69 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/Packages/FlixyKit/Sources/FlixyProbe/main.swift b/Packages/FlixyKit/Sources/FlixyProbe/main.swift index a195819..b3c5384 100644 --- a/Packages/FlixyKit/Sources/FlixyProbe/main.swift +++ b/Packages/FlixyKit/Sources/FlixyProbe/main.swift @@ -61,6 +61,21 @@ else { fatalError("Kein Weg zu \(resource.name) erreichbar.") } print("Server: \(resource.name) über \(connection.local ? "lokal" : "entfernt")\n") +// MARK: - Freie Abfrage + +// `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. +let requested = CommandLine.arguments.dropFirst().filter { !$0.hasPrefix("--") } +if !requested.isEmpty { + for path in requested { + print("=== \(path) ===") + print(await server.rawResponse(forPath: path)) + print() + } + exit(0) +} + // MARK: - Live-TV let availability = await server.liveTVAvailability() @@ -93,6 +108,20 @@ for probe in result.trace { print("\n" + (result.url == nil ? "Kein Weg trug." : "Erfolg.")) +if result.url == nil { + // Der Transcoder antwortet mit einer nackten 400-Seite. Der Entscheider + // bekommt dieselben Angaben und erklärt sich — die letzte Stelle, an der + // dieser Server noch sagt, woran es liegt. + print("\n=== Was der Entscheider dazu sagt ===") + print(await server.decisionDump(channel: channel, program: program)) + + print("\n=== Empfänger und Gerät ===") + for path in ["/livetv/dvrs/\(dvr.key)", "/media/grabbers/devices/1", "/livetv/sessions"] { + print("--- \(path) ---") + print(await server.rawResponse(forPath: path, limit: 700)) + } +} + /// Eine Kennung, die über Läufe hinweg gleich bleibt. /// /// Wechselt sie, gilt die Probe am Konto als neues Gerät, und in der diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 18aa58f..1d9ed16 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -1042,3 +1042,72 @@ public enum LiveTVVerdict: Sendable, Equatable { /// Der Entscheider hat nicht geantwortet — kein Grund, es nicht zu versuchen. case unknown(String) } + + +extension PlexServer { + /// Was der Entscheider zu einer Sendung sagt — im Wortlaut. + /// + /// Der Transcoder antwortet mit einer nackten „400 Bad Request"-Seite und + /// verrät nichts. Der Entscheider bekommt dieselben Angaben, gibt aber + /// eine Plex-Antwort zurück, in der steht, woran es liegt. Das ist die + /// letzte Stelle, an der dieser Server noch etwas erklärt. + public func decisionDump(channel: PlexChannel, program: PlexProgram?) async -> String { + guard let metadataKey = program?.metadataKey else { + return "Keine Sendung — ohne sie gibt es nichts zu entscheiden." + } + + var report: [String] = [] + for keepEncoding in [true, false] { + let request = TranscodeRequest( + ratingKey: channel.id, + isLocal: connection.local, + pathOverride: metadataKey + ) + guard let url = transcodeURL( + for: request, + keepPathEncoding: keepEncoding + ) else { continue } + + guard let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + else { continue } + + report.append("--- Entscheider, \(keepEncoding ? "einfach" : "doppelt") kodiert ---") + do { + let data = try await client.send(PlexRequest( + path: "/video/:/transcode/universal/decision", + query: items, + timeout: .seconds(20) + )) + report.append(redacted(String(decoding: data.prefix(900), as: UTF8.self))) + } catch { + report.append(Self.shortDescription(of: error)) + } + } + return report.joined(separator: "\n") + } + + /// Die rohe Antwort auf eine Adresse, gekürzt und ohne Token. + /// + /// Zum Erkunden gedacht: die Live-Schnittstelle von Plex ist nirgends + /// beschrieben, und die Antwort auf „was steht unter dieser Adresse" ist + /// die einzige verlässliche Quelle. Der Token wird aus der Ausgabe + /// entfernt, falls der Server ihn irgendwo zurückspiegelt. + public func rawResponse(forPath path: String, limit: Int = 1_200) async -> String { + let encoded = path.contains("%") + do { + let data = try await client.send(PlexRequest( + path: path, + timeout: .seconds(20), + isPathEncoded: encoded + )) + let text = String(decoding: data.prefix(limit), as: UTF8.self) + return redacted(text) + } catch { + return Self.shortDescription(of: error) + } + } + + private nonisolated func redacted(_ text: String) -> String { + text.replacingOccurrences(of: token, with: "«token»") + } +}