Ein Werkzeug für die Fragen, die reines HTTP sind
Das Upload-Kontingent des Tages ist aufgebraucht — verbraucht von meinen eigenen Vermutungen über eine Schnittstelle, die keine Oberfläche braucht. Jede Runde ging als TestFlight-Build hinaus und kam als Screenshot zurück. `swift run FlixyProbe` macht dasselbe in Sekunden: anmelden, Server suchen, Senderliste und Programm lesen, das Einschalten durchprobieren — mit derselben Logik wie die App. Der Token kommt über die übliche Anmeldung und liegt im Schlüsselbund dieses Rechners; gedruckt werden Pfade, Kennungen und Antworten, keine vollständigen Adressen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
024f396641
commit
03a8d1f899
@@ -0,0 +1,106 @@
|
||||
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: - 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."))
|
||||
|
||||
/// 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
|
||||
}
|
||||
Reference in New Issue
Block a user