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) """) } }