import Foundation /// Ein Gerät, das am Plex-Konto hängt — für uns nur Server interessant. public struct PlexResource: Sendable, Equatable, Decodable, Identifiable { public let name: String public let product: String? public let productVersion: String? public let platform: String? public let clientIdentifier: String public let provides: String public let owned: Bool /// Der Token für genau diesen Server. /// /// Nicht derselbe wie der Konto-Token: bei geteilten Servern bekommt man /// einen eigenen, eingeschränkten. Wer stattdessen den Konto-Token /// verwendet, bekommt bei fremden Servern wortlos 401. public let accessToken: String? public let connections: [PlexConnection] public var id: String { clientIdentifier } /// Ob es sich um einen Medienserver handelt — `provides` kann auch /// „client", „player" oder eine Kombination sein. public var isServer: Bool { provides.split(separator: ",").contains("server") } private enum CodingKeys: String, CodingKey { case name, product, productVersion, platform, clientIdentifier case provides, owned, accessToken, connections } public init(from decoder: any Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.name = try values.decode(String.self, forKey: .name) self.product = try values.decodeIfPresent(String.self, forKey: .product) self.productVersion = try values.decodeIfPresent(String.self, forKey: .productVersion) self.platform = try values.decodeIfPresent(String.self, forKey: .platform) self.clientIdentifier = try values.decode(String.self, forKey: .clientIdentifier) self.provides = try values.decodeIfPresent(String.self, forKey: .provides) ?? "" self.owned = try values.decodeIfPresent(Bool.self, forKey: .owned) ?? false self.accessToken = try values.decodeIfPresent(String.self, forKey: .accessToken) self.connections = try values.decodeIfPresent([PlexConnection].self, forKey: .connections) ?? [] } public init( name: String, clientIdentifier: String, provides: String = "server", owned: Bool = true, accessToken: String? = nil, connections: [PlexConnection] = [], product: String? = nil, productVersion: String? = nil, platform: String? = nil ) { self.name = name self.product = product self.productVersion = productVersion self.platform = platform self.clientIdentifier = clientIdentifier self.provides = provides self.owned = owned self.accessToken = accessToken self.connections = connections } } /// Ein Weg zu einem Server. public struct PlexConnection: Sendable, Equatable, Decodable, Identifiable { public let uri: String public let address: String? public let port: Int? public let local: Bool /// Ob der Weg über die Vermittlung von Plex läuft. Der ist immer /// erreichbar, aber auf wenige Mbit gedrosselt — taugt für Metadaten, /// nicht für 4K. public let relay: Bool public let `protocol`: String? public let iPv6: Bool public var id: String { uri } public var url: URL? { URL(string: uri) } private enum CodingKeys: String, CodingKey { case uri, address, port, local, relay case `protocol` case iPv6 = "IPv6" } public init(from decoder: any Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.uri = try values.decode(String.self, forKey: .uri) self.address = try values.decodeIfPresent(String.self, forKey: .address) self.port = try values.decodeIfPresent(Int.self, forKey: .port) self.local = try values.decodeIfPresent(Bool.self, forKey: .local) ?? false self.relay = try values.decodeIfPresent(Bool.self, forKey: .relay) ?? false self.protocol = try values.decodeIfPresent(String.self, forKey: .protocol) self.iPv6 = try values.decodeIfPresent(Bool.self, forKey: .iPv6) ?? false } public init( uri: String, local: Bool = false, relay: Bool = false, address: String? = nil, port: Int? = nil, protocol: String? = nil, iPv6: Bool = false ) { self.uri = uri self.local = local self.relay = relay self.address = address self.port = port self.protocol = `protocol` self.iPv6 = iPv6 } /// In welcher Reihenfolge die Wege probiert werden. /// /// Nicht „wer zuerst antwortet, gewinnt": die Vermittlung von Plex meldet /// sich oft schneller als der Server im eigenen Netz, ist aber gedrosselt. /// Ein Rennen ohne Rangordnung würde also ausgerechnet den langsamsten Weg /// prämieren. public var tier: Int { if local { return 0 } if relay { return 2 } return 1 } }