Anmelde-Adresse wurde doppelt kodiert URLComponents.fragment kodiert beim Setzen erneut, aus %5B wurde %255B. Plex antwortete mit „Diese Anfrage konnte nicht abgeschlossen werden". Die Adresse wird jetzt von Hand zusammengesetzt. Verbindungssuche hing bis zu anderthalb Minuten probeTimeout war deklariert, aber nirgends angewendet — es galt der URLSession-Standard von 60 Sekunden. Zusammen mit der Prüfung Rang für Rang kostete eine schweigende lokale Adresse die volle Zeit, bevor der direkte Weg überhaupt begann. Jetzt laufen alle Adressen gleichzeitig, mit Zeitgrenze, und die Rangordnung entscheidet unter den erreichbaren. Kein Zurück während des Ladens Die Kopfzeile mit dem Schließen-Knopf wurde nur im Zustand .playing gezeichnet. Bei langsamer Verbindung saß man ohne Ausweg vor einem Ladekreis. Sie liegt jetzt als eigene Ebene darüber und erscheint auch beim Vorbereiten — samt Titel, damit erkennbar bleibt, worauf gewartet wird. Sechs neue Tests, darunter drei, die die Zeitgrenze der Verbindungssuche an einer schweigenden Adresse nachstellen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.3 KiB
Swift
92 lines
3.3 KiB
Swift
import Foundation
|
|
|
|
/// 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
|
|
|
|
public init(
|
|
method: Method = .get,
|
|
path: String,
|
|
query: [URLQueryItem] = [],
|
|
additionalHeaders: [String: String] = [:],
|
|
timeout: Duration = .seconds(20)
|
|
) {
|
|
self.method = method
|
|
self.path = path
|
|
self.query = query
|
|
self.additionalHeaders = additionalHeaders
|
|
self.timeout = timeout
|
|
}
|
|
|
|
/// 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: path),
|
|
resolvingAgainstBaseURL: false
|
|
) else {
|
|
throw .transport(description: "Ungültige Basisadresse: \(baseURL)")
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
extension Duration {
|
|
/// Die Dauer als Gleitkommazahl in Sekunden.
|
|
///
|
|
/// Foundation und AVFoundation rechnen in `TimeInterval`; `Duration` ist
|
|
/// der genauere Typ, den der übrige Code verwendet. Diese Brücke steht in
|
|
/// PlexKit, weil es die unterste Schicht ist — die darüber erben sie.
|
|
public var seconds: Double {
|
|
Double(components.seconds) + Double(components.attoseconds) / 1e18
|
|
}
|
|
}
|