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. /// /// `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? { var components = URLComponents(string: "https://app.plex.tv/auth") components?.fragment = "?clientID=\(clientIdentifier)&code=\(code)" + "&context%5Bdevice%5D%5Bproduct%5D=\(product.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? product)" return components?.url } }