Add required MediaBrowser auth header and Jellyfin date format transcoder
This commit is contained in:
parent
3a16773092
commit
e6d44ac1ea
2 changed files with 70 additions and 17 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import Foundation
|
|
||||||
import Adwaita
|
import Adwaita
|
||||||
|
import Foundation
|
||||||
import LuminateCore
|
import LuminateCore
|
||||||
|
|
||||||
public struct ServerSetupView: View {
|
public struct ServerSetupView: View {
|
||||||
|
|
@ -22,10 +22,12 @@ public struct ServerSetupView: View {
|
||||||
icon: .custom(name: "dev.bscubed.Luminate"),
|
icon: .custom(name: "dev.bscubed.Luminate"),
|
||||||
description: "Enter your Jellyfin server details"
|
description: "Enter your Jellyfin server details"
|
||||||
) {
|
) {
|
||||||
VStack {
|
VStack(spacing: 16) {
|
||||||
EntryRow("Server URL", text: $serverURL)
|
PreferencesGroup("Server configuration") {
|
||||||
EntryRow("Username", text: $username)
|
EntryRow("Server URL", text: $serverURL)
|
||||||
PasswordEntryRow("Password", text: $password)
|
EntryRow("Username", text: $username)
|
||||||
|
PasswordEntryRow("Password", text: $password)
|
||||||
|
}
|
||||||
Button("Connect") {
|
Button("Connect") {
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +46,12 @@ public struct ServerSetupView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func connect() {
|
private func connect() {
|
||||||
guard let url = URL(string: serverURL), !username.isEmpty else {
|
var urlString = serverURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !urlString.hasPrefix("http://") && !urlString.hasPrefix("https://") {
|
||||||
|
urlString = "https://" + urlString
|
||||||
|
}
|
||||||
|
urlString = urlString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||||
|
guard let url = URL(string: urlString), !username.isEmpty else {
|
||||||
error = "Please enter a valid server URL and username"
|
error = "Please enter a valid server URL and username"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -55,15 +62,16 @@ public struct ServerSetupView: View {
|
||||||
let client = JellyfinClient(serverURL: url)
|
let client = JellyfinClient(serverURL: url)
|
||||||
let result = try await client.authenticate(username: username, password: password)
|
let result = try await client.authenticate(username: username, password: password)
|
||||||
let userId = result.User?.value1.Id ?? ""
|
let userId = result.User?.value1.Id ?? ""
|
||||||
await MainActor.run {
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
onLogin(client, userId)
|
onLogin(client, userId)
|
||||||
}
|
} catch (JellyfinError.httpError(let code)) {
|
||||||
|
isLoading = false
|
||||||
|
self.error = "Failed to login. HTTP code \(code)"
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
isLoading = false
|
||||||
isLoading = false
|
self.error = error.localizedDescription
|
||||||
self.error = error.localizedDescription
|
print(error.localizedDescription)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,47 @@ import OpenAPIRuntime
|
||||||
import OpenAPIURLSession
|
import OpenAPIURLSession
|
||||||
import HTTPTypes
|
import HTTPTypes
|
||||||
|
|
||||||
|
struct JellyfinDateTranscoder: DateTranscoder {
|
||||||
|
func encode(_ date: Date) throws -> String {
|
||||||
|
ISO8601DateFormatter().string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ dateString: String) throws -> Date {
|
||||||
|
let withoutExtraDigits = dateString.replacingOccurrences(
|
||||||
|
of: #"\.(\d{3})\d+"#,
|
||||||
|
with: ".$1",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
if let date = formatter.date(from: withoutExtraDigits) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
formatter.formatOptions = [.withInternetDateTime]
|
||||||
|
if let date = formatter.date(from: withoutExtraDigits) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
.init(codingPath: [], debugDescription: "Expected date string to be ISO8601-formatted: \(dateString)")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let clientName = "Luminate"
|
||||||
|
private let deviceName = "Desktop"
|
||||||
|
private let deviceId = "luminate-001"
|
||||||
|
private let clientVersion = "1.0.0"
|
||||||
|
|
||||||
|
func mediaBrowserHeader(token: String? = nil) -> String {
|
||||||
|
if let token {
|
||||||
|
"MediaBrowser Token=\"\(token)\", Client=\"\(clientName)\", Device=\"\(deviceName)\", DeviceId=\"\(deviceId)\", Version=\"\(clientVersion)\""
|
||||||
|
} else {
|
||||||
|
"MediaBrowser Client=\"\(clientName)\", Device=\"\(deviceName)\", DeviceId=\"\(deviceId)\", Version=\"\(clientVersion)\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AuthMiddleware: ClientMiddleware {
|
struct AuthMiddleware: ClientMiddleware {
|
||||||
let token: String
|
let token: String?
|
||||||
|
|
||||||
func intercept(
|
func intercept(
|
||||||
_ request: HTTPRequest,
|
_ request: HTTPRequest,
|
||||||
|
|
@ -14,7 +53,7 @@ struct AuthMiddleware: ClientMiddleware {
|
||||||
next: (HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?)
|
next: (HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?)
|
||||||
) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?) {
|
) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?) {
|
||||||
var request = request
|
var request = request
|
||||||
request.headerFields[.authorization] = "MediaBrowser Token=\"\(token)\""
|
request.headerFields[.authorization] = mediaBrowserHeader(token: token)
|
||||||
return try await next(request, body, baseURL)
|
return try await next(request, body, baseURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,18 +71,24 @@ public actor JellyfinClient {
|
||||||
private var token: String?
|
private var token: String?
|
||||||
private var client: Client
|
private var client: Client
|
||||||
|
|
||||||
|
private let clientConfig: Configuration
|
||||||
|
|
||||||
public init(serverURL: URL) {
|
public init(serverURL: URL) {
|
||||||
self.serverURL = serverURL
|
self.serverURL = serverURL
|
||||||
self.token = nil
|
self.token = nil
|
||||||
|
self.clientConfig = Configuration(dateTranscoder: JellyfinDateTranscoder())
|
||||||
self.client = Client(
|
self.client = Client(
|
||||||
serverURL: serverURL,
|
serverURL: serverURL,
|
||||||
transport: URLSessionTransport()
|
configuration: clientConfig,
|
||||||
|
transport: URLSessionTransport(),
|
||||||
|
middlewares: [AuthMiddleware(token: nil)]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeClient(token: String) -> Client {
|
private func makeClient(token: String) -> Client {
|
||||||
Client(
|
Client(
|
||||||
serverURL: serverURL,
|
serverURL: serverURL,
|
||||||
|
configuration: clientConfig,
|
||||||
transport: URLSessionTransport(),
|
transport: URLSessionTransport(),
|
||||||
middlewares: [AuthMiddleware(token: token)]
|
middlewares: [AuthMiddleware(token: token)]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue