import Foundation #if canImport(UIKit) import UIKit #endif /// Wer wir gegenüber Plex sind. /// /// Plex verlangt bei jeder Anfrage eine Handvoll `X-Plex-*`-Kopfzeilen. Die /// wichtigste ist `X-Plex-Client-Identifier`: darüber erkennt der Server das /// Gerät wieder. Ändert sie sich, gilt die App als neues Gerät — die /// Anmeldung ist futsch und in der Geräteliste des Kontos sammelt sich Müll /// an. Deshalb wird sie einmal erzeugt und dann festgehalten. public struct PlexClientIdentity: Sendable, Equatable { public let product: String public let version: String public let clientIdentifier: String public let platform: String public let platformVersion: String public let device: String public let deviceName: String public init( product: String, version: String, clientIdentifier: String, platform: String, platformVersion: String, device: String, deviceName: String ) { self.product = product self.version = version self.clientIdentifier = clientIdentifier self.platform = platform self.platformVersion = platformVersion self.device = device self.deviceName = deviceName } /// Die Kopfzeilen für eine Anfrage — ohne Token, das kommt je nach Ziel /// (plex.tv oder Server) aus einer anderen Quelle. public var headers: [String: String] { [ "X-Plex-Product": product, "X-Plex-Version": version, "X-Plex-Client-Identifier": clientIdentifier, "X-Plex-Platform": platform, "X-Plex-Platform-Version": platformVersion, "X-Plex-Device": device, "X-Plex-Device-Name": deviceName, // Ohne diese Zeile antwortet Plex in XML. Die Modelle hier sind auf // JSON ausgelegt, also fragen wir es ausdrücklich an. "Accept": "application/json", ] } /// Dieselben Angaben als Abfrageteile der Adresse. /// /// Für Adressen, die nicht die App abruft, sondern der Systemplayer: /// AVPlayer schickt keine Plex-Kopfzeilen mit. Bei Live-TV ist das /// entscheidend — der Server legt die Sitzung unter der Gerätekennung an /// und lehnt die Abholung ohne sie mit „HTTP 400" ab. public var queryItems: [URLQueryItem] { headers .filter { $0.key.hasPrefix("X-Plex-") } .map { URLQueryItem(name: $0.key, value: $0.value) } .sorted { $0.name < $1.name } } } extension PlexClientIdentity { private static let storageKey = "flixy.plex.clientIdentifier" /// Baut die Kennung für das laufende Gerät und merkt sich die Client-ID. /// /// `@MainActor`, weil `UIDevice.current` dort beheimatet ist. Das mit /// `assumeIsolated` zu umgehen wäre ein Absturz mit Anlauf — die Kennung /// wird ohnehin einmal beim Start gebaut. @MainActor public static func current( product: String, version: String, defaults: UserDefaults = .standard ) -> PlexClientIdentity { let identifier: String if let stored = defaults.string(forKey: storageKey) { identifier = stored } else { identifier = UUID().uuidString defaults.set(identifier, forKey: storageKey) } #if canImport(UIKit) let systemVersion = UIDevice.current.systemVersion let model = UIDevice.current.model let name = UIDevice.current.name #else let systemVersion = ProcessInfo.processInfo.operatingSystemVersionString let model = "Unknown" let name = ProcessInfo.processInfo.hostName #endif return PlexClientIdentity( product: product, version: version, clientIdentifier: identifier, platform: "iOS", platformVersion: systemVersion, device: model, deviceName: name ) } }