import Foundation import MediaKit /// Führt die Anmeldung bei plex.tv durch. public actor PlexAuthenticator { /// Wo das Konto-Token im Schlüsselbund steht. public static let accountTokenAccount = "plex.account" private let client: PlexHTTPClient private let identity: PlexClientIdentity private let tokenStore: any TokenStoring private let clock: any Clock public init( identity: PlexClientIdentity, tokenStore: any TokenStoring, transport: any PlexTransport = URLSession.shared, clock: any Clock = ContinuousClock() ) { self.identity = identity self.tokenStore = tokenStore self.clock = clock self.client = PlexHTTPClient( baseURL: URL(string: "https://plex.tv")!, identity: identity, transport: transport ) } /// Der bereits gespeicherte Token, falls vorhanden. public nonisolated var storedToken: String? { tokenStore.token(for: Self.accountTokenAccount) } /// Fordert einen neuen Anmeldecode an. /// /// Bewusst ohne `strong=true`: das liefert zwar einen schwerer zu /// erratenden Code, aber einen dreißigstelligen. Bei plex.tv/link /// abzutippen ist der nicht — und genau das ist der Weg, den Flixy /// anbietet. Der kurze Code ist vier Zeichen lang und läuft nach wenigen /// Minuten ab, was das Raten ausreichend aussichtslos macht. public func requestPin() async throws(PlexError) -> PlexPin { try await client.send( PlexRequest(method: .post, path: "/api/v2/pins"), as: PlexPin.self ) } /// Fragt einmal nach, ob der Code inzwischen bestätigt wurde. /// /// Gibt `nil` zurück, solange er es nicht ist. Für Aufrufer, die ihre /// eigene Schleife führen — etwa weil sie nebenher noch auf das Schließen /// eines Anmeldefensters achten müssen. public func token(for pin: PlexPin) async throws(PlexError) -> String? { let current = try await client.send( PlexRequest(path: "/api/v2/pins/\(pin.id)"), as: PlexPin.self ) guard let token = current.authToken, !token.isEmpty else { return nil } try? tokenStore.store(token, for: Self.accountTokenAccount) await client.setToken(token) return token } /// Wartet darauf, dass der Nutzer den Code im Browser bestätigt. /// /// Plex bietet keinen Rückkanal — es bleibt nur regelmäßiges Nachfragen. /// Das Intervall ist bewusst großzügig: der Weg über den Browser dauert /// ohnehin Sekunden, und sekündliches Anklopfen brächte nichts außer Last. /// /// Die Wartezeit ist an die Gültigkeit des Codes angelehnt. Kürzer wäre /// verkehrt herum: die App gäbe auf, obwohl der Code noch gilt, und wer /// zwischendurch etwas anderes gemacht hat, stünde vor „Zeit abgelaufen" /// bei einem Code, der funktioniert hätte. public func waitForAuthorization( pin: PlexPin, pollInterval: Duration = .seconds(2), timeout: Duration = .seconds(900) ) async throws(PlexError) -> String { // Über die Anzahl der Versuche statt über einen Zeitpunkt: eine // existenzielle `any Clock` gibt ihren Instant-Typ nicht preis, und // deshalb lassen sich zwei ihrer Zeitpunkte nicht vergleichen. Das // Ergebnis ist dasselbe, und in Tests reicht eine Uhr, die sofort // zurückkehrt. // // Nach oben begrenzt, damit ein versehentlich winziges Intervall nicht // in einen Überlauf oder Millionen Anfragen läuft. let attempts = pollInterval > .zero ? min(1000, max(1, Int(timeout / pollInterval))) : 1 for _ in 0..