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:
co-authored by
Claude Opus 5
parent
03a8d1f899
commit
7987dbf9a4
@@ -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»")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user