Add required MediaBrowser auth header and Jellyfin date format transcoder

This commit is contained in:
Brendan Szymanski 2026-06-05 04:27:45 -04:00
parent 3a16773092
commit e6d44ac1ea
2 changed files with 70 additions and 17 deletions

View file

@ -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)
}
} }
} }
} }

View file

@ -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)]
) )