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>
77 lines
3.3 KiB
Swift
77 lines
3.3 KiB
Swift
import Foundation
|
|
|
|
/// Der Anmeldecode aus dem PIN-Verfahren.
|
|
///
|
|
/// Plex legt auf Anfrage einen vierstelligen Code an. Der Nutzer bestätigt ihn
|
|
/// im Browser bei plex.tv, danach trägt derselbe Datensatz den Token. Die App
|
|
/// selbst bekommt nie Benutzername oder Passwort zu sehen — deshalb ist dieser
|
|
/// Weg dem manuellen Eintippen eines Tokens vorzuziehen.
|
|
public struct PlexPin: Sendable, Equatable, Decodable {
|
|
public let id: Int
|
|
public let code: String
|
|
public let authToken: String?
|
|
public let expiresAt: Date?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, code, authToken, expiresAt
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.id = try values.decode(Int.self, forKey: .id)
|
|
self.code = try values.decode(String.self, forKey: .code)
|
|
self.authToken = try values.decodeIfPresent(String.self, forKey: .authToken)
|
|
|
|
// Plex liefert ISO-8601 mit Zeitzone. Ein eigener Formatierer statt
|
|
// einer Decoder-Strategie, weil sonst alle anderen Datumsfelder in
|
|
// dieser Antwort mitbetroffen wären.
|
|
if let text = try values.decodeIfPresent(String.self, forKey: .expiresAt) {
|
|
self.expiresAt = ISO8601DateFormatter().date(from: text)
|
|
} else {
|
|
self.expiresAt = nil
|
|
}
|
|
}
|
|
|
|
public init(id: Int, code: String, authToken: String? = nil, expiresAt: Date? = nil) {
|
|
self.id = id
|
|
self.code = code
|
|
self.authToken = authToken
|
|
self.expiresAt = expiresAt
|
|
}
|
|
|
|
/// Die Adresse, unter der der Nutzer den Code bestätigt.
|
|
///
|
|
/// Von Hand zusammengesetzt statt über `URLComponents`. Der Grund ist die
|
|
/// Form, die Plex verlangt: die Parameter stehen im Fragment (hinter dem
|
|
/// `#`), nicht in der Abfrage, und einer davon heißt buchstäblich
|
|
/// `context[device][product]`. Die eckigen Klammern müssen genau einmal
|
|
/// kodiert sein.
|
|
///
|
|
/// `URLComponents.fragment` kodiert beim Setzen ein zweites Mal: aus `%5B`
|
|
/// wird `%255B`. Plex sieht dann einen Parameter, den es nicht kennt, und
|
|
/// antwortet mit „Diese Anfrage konnte nicht abgeschlossen werden".
|
|
/// Festgehalten in `AuthorizationURLTests`.
|
|
///
|
|
/// `forwardUrl` fehlt bewusst: der Rücksprung in die App wäre bequemer,
|
|
/// verlangt aber ein eigenes URL-Schema. Solange die App den Code ohnehin
|
|
/// im Hintergrund abfragt, merkt niemand einen Unterschied.
|
|
public func authorizationURL(clientIdentifier: String, product: String) -> URL? {
|
|
// `&`, `=` und `+` müssen raus: sie trennen Parameter bzw. stehen für
|
|
// ein Leerzeichen. In einem Wert würden sie die Adresse zerreißen.
|
|
let allowed = CharacterSet.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "&=+"))
|
|
func encode(_ value: String) -> String? {
|
|
value.addingPercentEncoding(withAllowedCharacters: allowed)
|
|
}
|
|
|
|
guard let client = encode(clientIdentifier),
|
|
let code = encode(code),
|
|
let product = encode(product)
|
|
else { return nil }
|
|
|
|
return URL(string: """
|
|
https://app.plex.tv/auth#?clientID=\(client)&code=\(code)\
|
|
&context%5Bdevice%5D%5Bproduct%5D=\(product)
|
|
""")
|
|
}
|
|
}
|