import Foundation import MediaKit /// Sucht den besten erreichbaren Weg zu einem Server. /// /// Alle Adressen werden **gleichzeitig** geprüft, nicht Rang für Rang. Der /// Unterschied ist erheblich: geht man die Ränge nacheinander durch, kostet /// eine nicht erreichbare lokale Adresse erst die volle Zeitgrenze, bevor der /// direkte Weg überhaupt beginnt. Aus zwei Wartezeiten wird so eine. /// /// Die Rangordnung bleibt trotzdem: gewinnt eine lokale Adresse, wird sofort /// abgebrochen — besser kann es nicht werden. Sonst wird gesammelt und am Ende /// die beste erreichbare genommen. Ein reines „wer zuerst antwortet" wäre /// falsch, weil die Vermittlung von Plex sich oft am schnellsten meldet und /// zugleich die langsamste Leitung hat. public struct PlexConnectionSelector: Sendable { private let identity: PlexClientIdentity private let transport: any PlexTransport private let probeTimeout: Duration public init( identity: PlexClientIdentity, transport: any PlexTransport = URLSession.shared, probeTimeout: Duration = .seconds(4) ) { self.identity = identity self.transport = transport self.probeTimeout = probeTimeout } public func selectConnection(for resource: PlexResource) async -> PlexConnection? { guard let token = resource.accessToken, !resource.connections.isEmpty else { return nil } return await withTaskGroup(of: PlexConnection?.self) { group in for connection in resource.connections { group.addTask { await reachable(connection, token: token, expecting: resource.clientIdentifier) ? connection : nil } } var best: PlexConnection? for await candidate in group { guard let candidate else { continue } if best.map({ candidate.tier < $0.tier }) ?? true { best = candidate } // Eine lokale Adresse ist nicht zu schlagen. Auf die übrigen // zu warten würde die Anmeldung nur verzögern. if best?.tier == 0 { group.cancelAll() break } } return best } } /// Fragt `/identity` ab und gleicht die Server-Kennung ab. private func reachable( _ connection: PlexConnection, token: String, expecting clientIdentifier: String ) async -> Bool { guard let baseURL = connection.url else { return false } let client = PlexHTTPClient( baseURL: baseURL, identity: identity, token: token, transport: transport ) do { let response = try await withTimeout(probeTimeout) { try await client.send( PlexRequest(path: "/identity", timeout: probeTimeout), as: PlexIdentity.self ) } // Der Abgleich ist keine Förmlichkeit: eine veraltete // plex.direct-Adresse kann auf einen fremden Server zeigen, und // ohne Prüfung würde die App fremde Mediatheken anzeigen. return response?.machineIdentifier == clientIdentifier } catch { return false } } /// Bricht ab, wenn die Arbeit zu lange dauert. /// /// Zusätzlich zur Zeitgrenze der Anfrage selbst, damit die Grenze auch bei /// einem Transport greift, der sie nicht beachtet — in den Tests etwa. /// Liefert `nil`, wenn die Zeit abgelaufen ist. private func withTimeout( _ duration: Duration, _ operation: @escaping @Sendable () async throws -> T ) async throws -> T? { try await withThrowingTaskGroup(of: T?.self) { group in group.addTask { try await operation() } group.addTask { try? await Task.sleep(for: duration) return nil } let first = try await group.next() ?? nil group.cancelAll() return first } } } /// Die Antwort von `/identity`. public struct PlexIdentity: Sendable, Decodable { public let machineIdentifier: String public let version: String? private enum OuterKeys: String, CodingKey { case mediaContainer = "MediaContainer" } private enum InnerKeys: String, CodingKey { case machineIdentifier, version } public init(from decoder: any Decoder) throws { let outer = try decoder.container(keyedBy: OuterKeys.self) let inner = try outer.nestedContainer(keyedBy: InnerKeys.self, forKey: .mediaContainer) self.machineIdentifier = try inner.decode(String.self, forKey: .machineIdentifier) self.version = try inner.decodeIfPresent(String.self, forKey: .version) } }