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
@@ -20,6 +20,11 @@ let package = Package(
|
|||||||
// Sprechen mit dem Server: Anmeldung, Endpunkte, Modelle. Kein SwiftUI,
|
// Sprechen mit dem Server: Anmeldung, Endpunkte, Modelle. Kein SwiftUI,
|
||||||
// damit die gesamte Protokoll-Schicht ohne Oberfläche testbar bleibt.
|
// damit die gesamte Protokoll-Schicht ohne Oberfläche testbar bleibt.
|
||||||
.target(name: "PlexKit"),
|
.target(name: "PlexKit"),
|
||||||
|
|
||||||
|
// Ein Werkzeug für die Fragen, die reines HTTP sind — ohne Oberfläche,
|
||||||
|
// ohne Gerät, ohne Upload. Eine Runde dauert damit Sekunden statt
|
||||||
|
// eines halben Tages, und das Upload-Kontingent bleibt für Builds.
|
||||||
|
.executableTarget(name: "FlixyProbe", dependencies: ["PlexKit"]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "PlexKitTests",
|
name: "PlexKitTests",
|
||||||
dependencies: ["PlexKit"],
|
dependencies: ["PlexKit"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -127,3 +127,22 @@ Die Oberfläche wird von Hand im Simulator geprüft.
|
|||||||
- Xcode 26 oder neuer
|
- Xcode 26 oder neuer
|
||||||
- iOS 26.0 auf dem Zielgerät
|
- iOS 26.0 auf dem Zielgerät
|
||||||
- XcodeGen (`brew install xcodegen`)
|
- XcodeGen (`brew install xcodegen`)
|
||||||
|
|
||||||
|
## Live-TV untersuchen, ohne zu bauen
|
||||||
|
|
||||||
|
Die Live-TV-Schnittstelle von Plex ist nirgends beschrieben, und jede Vermutung
|
||||||
|
darüber ging bisher als TestFlight-Build hinaus und kam als Screenshot zurück.
|
||||||
|
Irgendwann war das Upload-Kontingent des Tages aufgebraucht — für eine Frage,
|
||||||
|
die keine Oberfläche braucht.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Packages/FlixyKit && swift run FlixyProbe
|
||||||
|
```
|
||||||
|
|
||||||
|
Beim ersten Lauf zeigt das Werkzeug einen Code für plex.tv/link; danach liegt
|
||||||
|
der Token im Schlüsselbund dieses Rechners. Es sucht den Server, liest
|
||||||
|
Senderliste und Programm und probiert das Einschalten durch — mit derselben
|
||||||
|
Logik wie die App, nur in Sekunden statt in einem halben Tag.
|
||||||
|
|
||||||
|
Ausgegeben werden Pfade, Kennungen und die Antworten des Servers. Keine
|
||||||
|
vollständigen Adressen, also auch kein Token.
|
||||||
|
|||||||
Reference in New Issue
Block a user