mirror of
https://github.com/jellyfin/jellyfin-sdk-swift.git
synced 2024-11-23 06:09:58 +00:00
Provide Quick Connect Authorization Flow (#36)
This commit is contained in:
parent
30957ea3fe
commit
eae2ab5ed7
26
README.md
26
README.md
@ -26,6 +26,32 @@ let response = jellyfinClient.signIn(username: "jelly", password: "fin")
|
|||||||
|
|
||||||
Alternatively, you can use your own network stack with the generated **Entities** and **Paths**.
|
Alternatively, you can use your own network stack with the generated **Entities** and **Paths**.
|
||||||
|
|
||||||
|
## Quick Connect
|
||||||
|
|
||||||
|
The `QuickConnect` object has been provided to perform the Quick Connect authorization flow.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
/// Create a QuickConnect object with a JellyfinClient
|
||||||
|
let quickConnect = QuickConnect(client: client)
|
||||||
|
|
||||||
|
let quickConnectState = Task {
|
||||||
|
/// Listen to QuickConnect states with async/await or Combine
|
||||||
|
for await state in quickConnect.$state.values {
|
||||||
|
switch state {
|
||||||
|
/// Other cases ommitted
|
||||||
|
case let .polling(code: code):
|
||||||
|
print(code)
|
||||||
|
case let .authenticated(secret: secret):
|
||||||
|
/// Sign in with the Quick Connect secret
|
||||||
|
client.signIn(quickConnectSecret: secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the Quick Connect authorization flow
|
||||||
|
quickConnect.start()
|
||||||
|
```
|
||||||
|
|
||||||
## Generation
|
## Generation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
191
Sources/QuickConnect.swift
Normal file
191
Sources/QuickConnect.swift
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
//
|
||||||
|
// jellyfin-sdk-swift is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// A provider for the Quick Connect authorization flow.
|
||||||
|
///
|
||||||
|
/// To start the authorization flow, call `start()`. The `state` variable
|
||||||
|
/// will be updated to the current flow state and can be subscribed to with
|
||||||
|
/// async/await or Combine. See `QuickConnect.State` for all possible states.
|
||||||
|
///
|
||||||
|
/// To stop the authorization flow, typically for user cancellation, call `stop()`.
|
||||||
|
public final class QuickConnect: ObservableObject {
|
||||||
|
|
||||||
|
// MARK: State
|
||||||
|
|
||||||
|
public enum State: Equatable {
|
||||||
|
|
||||||
|
/// Idle
|
||||||
|
case idle
|
||||||
|
|
||||||
|
/// Retrieving Quick Connect code
|
||||||
|
case retrievingCode
|
||||||
|
|
||||||
|
/// Polling with code
|
||||||
|
case polling(code: String)
|
||||||
|
|
||||||
|
/// Authenticated with secret
|
||||||
|
case authenticated(secret: String)
|
||||||
|
|
||||||
|
/// An internal error has occurred
|
||||||
|
case error(QuickConnectError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Error
|
||||||
|
|
||||||
|
public enum QuickConnectError: LocalizedError, Equatable {
|
||||||
|
|
||||||
|
/// Polling has hit its maximum
|
||||||
|
case maxPollingHit
|
||||||
|
|
||||||
|
/// An other error has occurred, typically a network error
|
||||||
|
case other(String)
|
||||||
|
|
||||||
|
/// Retrieving the Quick Connect code failed.
|
||||||
|
///
|
||||||
|
/// Only thrown when incorrect/incomplete expected data
|
||||||
|
/// is returned from the server.
|
||||||
|
case retrievingCodeFailed
|
||||||
|
|
||||||
|
var localizedError: String {
|
||||||
|
switch self {
|
||||||
|
case .maxPollingHit:
|
||||||
|
"Max polling hit"
|
||||||
|
case let .other(message):
|
||||||
|
message
|
||||||
|
case .retrievingCodeFailed:
|
||||||
|
"Retrieving code failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state of the authorization flow.
|
||||||
|
@Published
|
||||||
|
public private(set) var state: State = .idle
|
||||||
|
|
||||||
|
private let client: JellyfinClient
|
||||||
|
private let pollInterval: Int
|
||||||
|
private let maxPolls: Int
|
||||||
|
|
||||||
|
private var mainTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
/// Creates a manager for performing a Quick Connect authorization flow.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - client: The `JellyfinClient` to perform Quick Connect authorization with.
|
||||||
|
/// - pollInterval: The polling interval, in seconds, while in the `polling` state.
|
||||||
|
/// - maxPolls: The maximum number of polls while in the `polling` state. Hitting
|
||||||
|
/// this amount of polls will throw a `maxPollingHit` error state.
|
||||||
|
///
|
||||||
|
/// - Precondition: `pollInterval > 0`
|
||||||
|
/// - Precondition: `maxPolls > 0`
|
||||||
|
public init(
|
||||||
|
client: JellyfinClient,
|
||||||
|
pollInterval: Int = 5,
|
||||||
|
maxPolls: Int = 200
|
||||||
|
) {
|
||||||
|
precondition(pollInterval > 0, "Polling interval must be at least one second")
|
||||||
|
precondition(maxPolls > 0, "Maximum polling must be positive")
|
||||||
|
|
||||||
|
self.client = client
|
||||||
|
self.pollInterval = pollInterval
|
||||||
|
self.maxPolls = maxPolls
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the Quick Connect authorization flow.
|
||||||
|
///
|
||||||
|
/// - Important: Make sure to subscribe or await for `state` changes
|
||||||
|
/// prior to starting Quick Connect.
|
||||||
|
@MainActor
|
||||||
|
public func start() {
|
||||||
|
guard state == .idle else { return }
|
||||||
|
|
||||||
|
mainTask = Task {
|
||||||
|
await run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the current Quick Connect authorization flow.
|
||||||
|
@MainActor
|
||||||
|
public func stop() {
|
||||||
|
mainTask?.cancel()
|
||||||
|
state = .idle
|
||||||
|
}
|
||||||
|
|
||||||
|
private func run() async {
|
||||||
|
do {
|
||||||
|
await MainActor.run {
|
||||||
|
state = .retrievingCode
|
||||||
|
}
|
||||||
|
|
||||||
|
let (secret, code) = try await retrieveSecretAndCode()
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .polling(code: code)
|
||||||
|
}
|
||||||
|
|
||||||
|
let authorizedSecret = try await poll(secret: secret)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
state = .authenticated(secret: authorizedSecret)
|
||||||
|
}
|
||||||
|
} catch let error as QuickConnectError {
|
||||||
|
await MainActor.run {
|
||||||
|
state = .error(error)
|
||||||
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
// Task was cancelled, not an issue
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
state = .error(.other(error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retrieveSecretAndCode() async throws -> (secret: String, code: String) {
|
||||||
|
|
||||||
|
let initiatePath = Paths.initiate
|
||||||
|
let response = try await client.send(initiatePath)
|
||||||
|
|
||||||
|
guard let secret = response.value.secret,
|
||||||
|
let code = response.value.code
|
||||||
|
else {
|
||||||
|
throw QuickConnectError.retrievingCodeFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return (secret, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: `Task.sleep` doesn't guarantee actual time == given time, but
|
||||||
|
// variance is fairly tight and exact time doesn't matter.
|
||||||
|
private func poll(secret: String) async throws -> String {
|
||||||
|
|
||||||
|
for _ in 0 ..< maxPolls {
|
||||||
|
if let authSecret = try await checkAuthorization(secret: secret) {
|
||||||
|
return authSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * pollInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
throw QuickConnectError.maxPollingHit
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkAuthorization(secret: String) async throws -> String? {
|
||||||
|
|
||||||
|
let request = Paths.connect(secret: secret)
|
||||||
|
let response = try await client.send(request)
|
||||||
|
|
||||||
|
let isAuthenticated = response.value.isAuthenticated ?? false
|
||||||
|
|
||||||
|
guard isAuthenticated, let authorizedSecret = response.value.secret else { return nil }
|
||||||
|
|
||||||
|
return authorizedSecret
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user