import Foundation /// Führt Anfragen gegen einen Host aus. /// /// Es gibt zwei davon: einen für plex.tv (Anmeldung, Serverliste) und einen je /// Server. Sie unterscheiden sich nur in Basisadresse und Token — deshalb /// dieselbe Klasse statt zweier ähnlicher. public actor PlexHTTPClient { private let baseURL: URL private let identity: PlexClientIdentity private let transport: any PlexTransport private var token: String? public init( baseURL: URL, identity: PlexClientIdentity, token: String? = nil, transport: any PlexTransport = URLSession.shared ) { self.baseURL = baseURL self.identity = identity self.token = token self.transport = transport } public func setToken(_ token: String?) { self.token = token } public var currentToken: String? { token } public func send(_ request: PlexRequest) async throws(PlexError) -> Data { let urlRequest = try request.urlRequest(baseURL: baseURL, identity: identity, token: token) let data: Data let response: URLResponse do { (data, response) = try await transport.data(for: urlRequest) } catch let error as URLError { // Der Klartext von URLError nennt Zeitüberschreitung, fehlende // Verbindung oder Zertifikatsprobleme beim Namen. Das ist der // Unterschied zwischen einer brauchbaren Fehlermeldung und // „Etwas ist schiefgelaufen". throw .transport(description: error.localizedDescription) } catch { throw .transport(description: String(describing: error)) } return try PlexResponse.validate(data: data, response: response) } public func send( _ request: PlexRequest, as type: T.Type ) async throws(PlexError) -> T { let data = try await send(request) return try PlexResponse.decode(type, from: data) } /// Baut die Adresse eines Bildes, ohne es zu laden. /// /// Plex kann Bilder serverseitig verkleinern — ein Poster in Kachelgröße /// statt in Originalauflösung spart bei einer Reihe aus zwanzig Titeln ein /// Vielfaches an Übertragung. public nonisolated func artworkURL( path: String, width: Int, height: Int, token: String ) -> URL? { guard var components = URLComponents( url: baseURL.appending(path: "/photo/:/transcode"), resolvingAgainstBaseURL: false ) else { return nil } components.queryItems = [ URLQueryItem(name: "width", value: String(width)), URLQueryItem(name: "height", value: String(height)), URLQueryItem(name: "minSize", value: "1"), URLQueryItem(name: "upscale", value: "1"), URLQueryItem(name: "url", value: path), // Hier muss der Token ausnahmsweise in die Adresse: das Bild lädt // AsyncImage selbst, und dort lassen sich keine Kopfzeilen setzen. URLQueryItem(name: "X-Plex-Token", value: token), ] return components.url } }