import Foundation /// Sucht den besten erreichbaren Weg zu einem Server. /// /// Geht die Wege nach Rang durch — erst lokal, dann direkt von außen, zuletzt /// über die Vermittlung von Plex. Innerhalb eines Rangs laufen die Versuche /// gleichzeitig, weil sie sich gegenseitig nicht ausschließen und ein /// unerreichbarer Weg sonst die volle Zeitüberschreitung kostet, bevor der /// nächste überhaupt beginnt. 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(5) ) { self.identity = identity self.transport = transport self.probeTimeout = probeTimeout } public func selectConnection(for resource: PlexResource) async -> PlexConnection? { guard let token = resource.accessToken else { return nil } let tiers = Dictionary(grouping: resource.connections, by: \.tier) for tier in tiers.keys.sorted() { guard let candidates = tiers[tier] else { continue } if let winner = await race(candidates, token: token, expecting: resource.clientIdentifier) { return winner } } return nil } /// Lässt die Wege eines Rangs gegeneinander antreten; der erste, der sich /// korrekt ausweist, gewinnt. private func race( _ connections: [PlexConnection], token: String, expecting clientIdentifier: String ) async -> PlexConnection? { await withTaskGroup(of: PlexConnection?.self) { group in for connection in connections { group.addTask { await probe(connection, token: token, expecting: clientIdentifier) ? connection : nil } } for await result in group { if let result { // Die übrigen Versuche interessieren nicht mehr. Ohne das // Abbrechen liefe eine unerreichbare Adresse noch bis zur // Zeitüberschreitung weiter und hielte die Aufgabe offen. group.cancelAll() return result } } return nil } } /// Fragt `/identity` ab — der einzige Endpunkt, der ohne Token antwortet /// und die Kennung des Servers nennt. private func probe( _ 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 identityResponse = try await client.send( PlexRequest(path: "/identity"), as: PlexIdentity.self ) // Der Abgleich der Kennung 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 identityResponse.machineIdentifier == clientIdentifier } catch { return false } } } /// 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) } }