import Foundation import MediaKit /// Der schmale Spalt, durch den PlexKit ans Netz kommt. /// /// Alles darüber kennt nur dieses Protokoll. Dadurch laufen die Tests gegen /// aufgezeichnete Antworten statt gegen einen echten Server — und zwar ohne /// `URLProtocol`-Verrenkungen, die bei paralleler Testausführung erfahrungs- /// gemäß ins Rutschen kommen. public protocol PlexTransport: Sendable { func data(for request: URLRequest) async throws -> (Data, URLResponse) } extension URLSession: PlexTransport {} /// Eine Anfrage an Plex, unabhängig davon, an welchen Host sie geht. /// /// Plex nimmt den Token wahlweise als Kopfzeile oder als Abfrageparameter. /// Hier steht er in der Kopfzeile — sonst landet er in jedem Protokoll und in /// jedem Absturzbericht, der URLs mitschreibt. public struct PlexRequest: Sendable, Equatable { public enum Method: String, Sendable { case get = "GET", post = "POST", put = "PUT", delete = "DELETE" } public var method: Method public var path: String public var query: [URLQueryItem] public var additionalHeaders: [String: String] /// Wie lange auf Antwort gewartet wird. /// /// Der Standardwert von URLSession sind 60 Sekunden. Für einen Server, der /// nicht antwortet, ist das eine Ewigkeit — und bei der Verbindungssuche /// summiert es sich über mehrere Adressen. public var timeout: Duration /// Ob `path` bereits kodiert ist und unverändert übernommen werden soll. public let isPathEncoded: Bool public init( method: Method = .get, path: String, query: [URLQueryItem] = [], additionalHeaders: [String: String] = [:], timeout: Duration = .seconds(20), isPathEncoded: Bool = false ) { self.method = method self.path = path self.query = query self.additionalHeaders = additionalHeaders self.timeout = timeout self.isPathEncoded = isPathEncoded } /// Setzt die Anfrage gegen einen Host zusammen. public func urlRequest( baseURL: URL, identity: PlexClientIdentity, token: String? ) throws(PlexError) -> URLRequest { guard var components = URLComponents( url: baseURL.appending(path: isPathEncoded ? "/" : path), resolvingAgainstBaseURL: false ) else { throw .transport(description: "Ungültige Basisadresse: \(baseURL)") } // Adressen beim Programmanbieter enthalten bereits kodierte Zeichen // (`plex%3A%2F%2Fmovie%2F…`). `appending(path:)` kodiert das Prozent // ein zweites Mal, und der Server findet nichts mehr. if isPathEncoded { components.percentEncodedPath = path } components.queryItems = query.isEmpty ? nil : query guard let url = components.url else { throw .transport(description: "Anfrage ließ sich nicht zusammensetzen: \(path)") } var request = URLRequest(url: url) request.httpMethod = method.rawValue request.timeoutInterval = timeout.seconds for (field, value) in identity.headers { request.setValue(value, forHTTPHeaderField: field) } for (field, value) in additionalHeaders { request.setValue(value, forHTTPHeaderField: field) } if let token { request.setValue(token, forHTTPHeaderField: "X-Plex-Token") } return request } }