Files
Flaro/Packages/FlixyKit/Sources/FlixyProbe/main.swift
T
ScarriffleandClaude Opus 5 458396c2b7 Probe: prüfen, ob der Transcoder überhaupt etwas tut
Das Abwarten brachte nichts: der Vorgang meldet auch nach zwölf Sekunden
keine Daten. Und jede Anfrage an den Transcoder endet mit einer nackten
400 — auch die an den Entscheider, der sich sonst erklärt.

Das ist ein Muster, das nicht nach Live-TV aussieht, sondern nach dem
Transcoder selbst. Dass er funktioniert, habe ich die ganze Zeit
vorausgesetzt und nie geprüft: Filme laufen über DirectPlay, da wird er
gar nicht gefragt. Live-TV ist das Erste, was ihn zwingend braucht.

Also die Gegenprobe, vor allem weiteren Suchen: eine gewöhnliche Datei
aus der Mediathek durch den Transcoder. Läuft sie durch, liegt es an
Live-TV. Läuft sie nicht durch, habe ich tagelang am falschen Ende
gesucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 15:53:54 +02:00

160 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import PlexKit
// Ein Werkzeug für die Fragen, die reines HTTP sind.
//
// Der Grund, dass es das gibt: die Live-TV-Schnittstelle von Plex ist
// nirgends beschrieben, und jede Vermutung darüber ging bisher als
// TestFlight-Build zum Nutzer und kam einen halben Tag später als Screenshot
// zurück. Irgendwann war das Upload-Kontingent des Tages aufgebraucht — für
// eine Frage, die keine Oberfläche braucht.
//
// Der Token wird über die übliche Anmeldung geholt und liegt im Schlüsselbund
// dieses Rechners. Er steht nirgends in der Ausgabe: gedruckt werden Pfade,
// Kennungen und Antworten, keine vollständigen Adressen.
let identity = PlexClientIdentity(
product: "Flixy-Probe",
version: "1.0",
clientIdentifier: storedClientIdentifier(),
platform: "macOS",
platformVersion: ProcessInfo.processInfo.operatingSystemVersionString,
device: "Mac",
deviceName: Host.current().localizedName ?? "Mac"
)
let store = KeychainTokenStore(service: "com.scarriffleservices.flixy.probe")
let authenticator = PlexAuthenticator(identity: identity, tokenStore: store)
let token: String
if let existing = authenticator.storedToken {
print("Angemeldet (Token aus dem Schlüsselbund).")
token = existing
} else {
let pin = try await authenticator.requestPin()
print("""
Anmelden: https://plex.tv/link
Code: \(pin.code)
Warte …
""")
var found: String?
for _ in 0..<150 where found == nil {
try await Task.sleep(for: .seconds(2))
found = try await authenticator.token(for: pin)
}
guard let found else { fatalError("Anmeldung abgelaufen.") }
token = found
print("Angemeldet.\n")
}
let directory = PlexServerDirectory(identity: identity, token: token)
let servers = try await directory.servers()
guard let resource = servers.first else { fatalError("Kein Server am Konto.") }
let selector = PlexConnectionSelector(identity: identity)
guard let connection = await selector.selectConnection(for: resource),
let server = PlexServer(resource: resource, connection: connection, identity: identity)
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: - Gegenprobe
// Bevor weiter an Live-TV gesucht wird: tut der Transcoder überhaupt etwas?
// Jede Anfrage an ihn endet mit einer nackten 400 — auch die an den
// Entscheider. Das ist eine Annahme, die ich die ganze Zeit ungeprüft
// mitgeschleppt habe, und sie gehört als Erstes geprüft: läuft eine
// gewöhnliche Datei durch, liegt es an Live-TV. Läuft sie nicht durch, habe
// ich tagelang am falschen Ende gesucht.
if let hubs = try? await server.homeHubs(),
let movie = hubs.flatMap(\.items).first(where: { $0.kind == .movie }) {
let request = TranscodeRequest(
ratingKey: movie.ratingKey,
maximumBitrate: 4_000,
isLocal: connection.local
)
if let url = server.transcodeURL(for: request) {
print("Gegenprobe — Transcoder mit: \(movie.title)")
print(" → \(await server.probeStream(url: url))")
print()
}
} else {
print("Gegenprobe: keine Datei zum Prüfen gefunden.\n")
}
// MARK: - Live-TV
let availability = await server.liveTVAvailability()
guard case .available(let dvrs) = availability, let dvr = dvrs.first else {
print("Live-TV: \(availability)")
exit(0)
}
print("Empfänger: \(dvr.key), Programmführer: \(dvr.epgIdentifier ?? "–")")
let guide = await server.liveTVGuide(dvr: dvr, hours: 2)
print("Sender: \(guide.channels.count) — \(guide.source)\n")
guard let channel = guide.channels.first else { exit(0) }
let program = guide.programs[channel.id]?.first { $0.isRunning() }
print("Einschalten: \(channel.title) (\(channel.channelIdentifier ?? "–"))")
print("Läuft: \(program?.title ?? "–")\n")
let result = await server.tune(
channel: channel,
program: program,
dvr: dvr,
maximumBitrate: nil
)
for probe in result.trace {
print(probe.path)
print(" → \(probe.result)")
}
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
/// Geräteliste sammelt sich Müll an.
func storedClientIdentifier() -> String {
let key = "flixy.probe.clientIdentifier"
if let stored = UserDefaults.standard.string(forKey: key) { return stored }
let created = UUID().uuidString
UserDefaults.standard.set(created, forKey: key)
return created
}