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 }