Probe: den Entscheider fragen, wenn der Transcoder schweigt

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 15:12:51 +02:00
co-authored by Claude Opus 5
parent 03a8d1f899
commit 7987dbf9a4
2 changed files with 98 additions and 0 deletions
@@ -61,6 +61,21 @@ else { fatalError("Kein Weg zu \(resource.name) erreichbar.") }
print("Server: \(resource.name) über \(connection.local ? "lokal" : "entfernt")\n") 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 // MARK: - Live-TV
let availability = await server.liveTVAvailability() let availability = await server.liveTVAvailability()
@@ -93,6 +108,20 @@ for probe in result.trace {
print("\n" + (result.url == nil ? "Kein Weg trug." : "Erfolg.")) 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. /// Eine Kennung, die über Läufe hinweg gleich bleibt.
/// ///
/// Wechselt sie, gilt die Probe am Konto als neues Gerät, und in der /// Wechselt sie, gilt die Probe am Konto als neues Gerät, und in der
@@ -1042,3 +1042,72 @@ public enum LiveTVVerdict: Sendable, Equatable {
/// Der Entscheider hat nicht geantwortet — kein Grund, es nicht zu versuchen. /// Der Entscheider hat nicht geantwortet — kein Grund, es nicht zu versuchen.
case unknown(String) 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»")
}
}