Update JellyfinClient.swift

This commit is contained in:
Ethan Pippin 2022-08-17 11:20:55 -06:00
parent afd8c22dc9
commit fc7adb746a

View File

@ -9,37 +9,60 @@
import Foundation
import Get
/// Basic wrapper of `Get.APIClient` with helper methods for interfacing with the
/// `JellyfinAPI` package, like injecting required headers for server calls.
public final class JellyfinClient {
public var accessToken: String?
/// Current user access token
private(set) public var accessToken: String?
/// Configuration for this instance of `JellyfinClient`
public let configuration: Configuration
private var _apiClient: APIClient!
private let sessionConfiguration: URLSessionConfiguration
private let delegate: APIClientDelegate?
public init(configuration: Configuration) {
/// Create a `JellyfinClient` instance given a configuration and optional access token
public init(configuration: Configuration,
sessionConfiguration: URLSessionConfiguration = .default,
delegate: APIClientDelegate? = nil,
accessToken: String? = nil) {
self.configuration = configuration
let apiClientConfiguration = APIClient.Configuration(baseURL: configuration.url, sessionConfiguration: .default, delegate: self)
self._apiClient = APIClient(configuration: apiClientConfiguration)
self.sessionConfiguration = sessionConfiguration
self.delegate = delegate
self.accessToken = accessToken
self._apiClient = APIClient(baseURL: configuration.url) { configuration in
configuration.sessionConfiguration = sessionConfiguration
configuration.decoder = JSONDecoder()
configuration.encoder = JSONEncoder()
configuration.delegate = self
}
}
public struct Configuration {
/// Server URL
public let url: URL
/// Client name
///
/// Example: Jellyfin iOS
/// - Example: `Jellyfin iOS`
public let client: String
/// Client device name
/// Device name
///
/// - Example: `iPhone 13 Pro`
public let deviceName: String
/// Unique device ID
/// Unique device ID. A `UUID` is recommended.
public let deviceID: String
/// Current app version
public let appVersion: String
/// Version of your application
///
/// - Example: `1.2.3`
public let version: String
public init(
url: URL,
@ -52,7 +75,7 @@ public final class JellyfinClient {
self.client = client
self.deviceName = deviceName
self.deviceID = deviceID
self.appVersion = version
self.version = version
}
}
@ -64,6 +87,7 @@ public final class JellyfinClient {
try await _apiClient.send(request, delegate: delegate, configure: configure)
}
@discardableResult
public func send(
_ request: Request<Void>,
delegate: URLSessionDataDelegate? = nil,
@ -95,12 +119,12 @@ public final class JellyfinClient {
try await _apiClient.download(resumeFrom: resumeData, delegate: delegate)
}
public func authHeaders() -> String {
private func authHeaders() -> String {
let fields = [
"DeviceId": configuration.deviceID,
"Device": configuration.deviceName,
"Client": configuration.client,
"Version": configuration.appVersion,
"Version": configuration.version,
"Token": accessToken ?? ""
]
.map { "\($0.key)=\($0.value)" }
@ -110,8 +134,131 @@ public final class JellyfinClient {
}
}
// MARK: APIClientDelegate
extension JellyfinClient: APIClientDelegate {
/// Allows you to modify the request right before it is sent.
/// Also injects required Jellyfin headers for every request.
///
/// Gets called right before sending the request. If the retries are enabled,
/// is called before every attempt.
///
/// - parameters:
/// - client: The client that sends the request.
/// - request: The request about to be sent. Can be modified
public func client(_ client: APIClient, willSendRequest request: inout URLRequest) async throws {
// Inject required headers
request.addValue(authHeaders(), forHTTPHeaderField: "Authorization")
try await delegate?.client(_apiClient, willSendRequest: &request)
}
/// Validates response for the given request.
///
/// - parameters:
/// - client: The client that sent the request.
/// - response: The response with an invalid status code.
/// - data: Body of the response, if any.
/// - request: Failing request.
///
/// - throws: An error to be returned to the user. By default, throws
/// ``APIError/unacceptableStatusCode(_:)`` if the code is outside of
/// the `200..<300` range.
public func client(_ client: APIClient, validateResponse response: HTTPURLResponse, data: Data, task: URLSessionTask) throws {
if let delegate = delegate {
try delegate.client(_apiClient, validateResponse: response, data: data, task: task)
} else {
guard (200..<300).contains(response.statusCode) else {
throw APIError.unacceptableStatusCode(response.statusCode)
}
}
}
/// Gets called after a networking failure. Only one retry attempt is allowed.
///
/// - important: This method will only be called for network requests, but not for
/// response body decoding failures or failures with creating requests using
/// ``client(_:makeURLFor:query:)-9bylj`` and ``client(_:willSendRequest:)-2d1ke``.
///
/// - parameters:
/// - client: The client that sent the request.
/// - task: The failed task.
/// - error: The encountered error.
/// - attempts: The number of already performed attempts.
///
/// - returns: Return `true` to retry the request.
public func client(_ client: APIClient, shouldRetry task: URLSessionTask, error: Error, attempts: Int) async throws -> Bool {
try await delegate?.client(_apiClient, shouldRetry: task, error: error, attempts: attempts) ?? false
}
/// Constructs URL for the given request.
///
/// - parameters:
/// - client: The client that sends the request.
/// - url: The URL passed by the client.
/// - request: The request about to be sent.
///
/// - returns: The URL for the request. Return `nil` to use the default
/// logic used by client.
public func client(_ client: APIClient, makeURLFor url: String, query: [(String, String?)]?) throws -> URL? {
try delegate?.client(_apiClient, makeURLFor: url, query: query)
}
}
// MARK: Helpers
extension JellyfinClient {
/// Signs in a user given a username and password. On a successful authentication response the `accessToken` is set to the supplied access token.
/// Overrides the current access token if one was set prior.
///
/// - Parameters:
/// - username: username of the user
/// - password: password of the user
///
/// - Throws: `ClientError.noAccessTokenInResponse` if no access token was supplied in a successful authentication response
@discardableResult
public func signIn(username: String, password: String) async throws -> AuthenticationResult {
let authenticateUserRequest = Paths.authenticateUserByName(.init(password: password, pw: nil, username: "epippin"))
let response = try await send(authenticateUserRequest).value
if let accessToken = response.accessToken {
self.accessToken = accessToken
} else {
throw ClientError.noAccessTokenInResponse
}
return response
}
/// Signs out the current user with the server by revoking the current access token
///
/// - Throws: `ClientError.noAccessTokenSet` if no access token has currently been set
public func signOut() async throws {
guard let accessToken = self.accessToken else { throw ClientError.noAccessTokenSet }
let revokeKeyRequest = Paths.revokeKey(key: accessToken)
try await send(revokeKeyRequest)
self.accessToken = nil
}
}
// MARK: ClientError
extension JellyfinClient {
enum ClientError: Error {
case noAccessTokenInResponse
case noAccessTokenSet
var localizedDescription: String {
switch self {
case .noAccessTokenInResponse:
return "No access token in authenticated response"
case .noAccessTokenSet:
return "No access token currently set"
}
}
}
}