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 Foundation
import LuminateCore
public struct ServerSetupView: View {
@ -22,10 +22,12 @@ public struct ServerSetupView: View {
icon: .custom(name: "dev.bscubed.Luminate"),
description: "Enter your Jellyfin server details"
) {
VStack {
VStack(spacing: 16) {
PreferencesGroup("Server configuration") {
EntryRow("Server URL", text: $serverURL)
EntryRow("Username", text: $username)
PasswordEntryRow("Password", text: $password)
}
Button("Connect") {
connect()
}
@ -44,7 +46,12 @@ public struct ServerSetupView: View {
}
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"
return
}
@ -55,15 +62,16 @@ public struct ServerSetupView: View {
let client = JellyfinClient(serverURL: url)
let result = try await client.authenticate(username: username, password: password)
let userId = result.User?.value1.Id ?? ""
await MainActor.run {
isLoading = false
onLogin(client, userId)
}
} catch (JellyfinError.httpError(let code)) {
isLoading = false
self.error = "Failed to login. HTTP code \(code)"
} catch {
await MainActor.run {
isLoading = false
self.error = error.localizedDescription
}
print(error.localizedDescription)
}
}
}

View file

@ -3,8 +3,47 @@ import OpenAPIRuntime
import OpenAPIURLSession
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 {
let token: String
let token: String?
func intercept(
_ request: HTTPRequest,
@ -14,7 +53,7 @@ struct AuthMiddleware: ClientMiddleware {
next: (HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?)
) async throws -> (HTTPResponse, OpenAPIRuntime.HTTPBody?) {
var request = request
request.headerFields[.authorization] = "MediaBrowser Token=\"\(token)\""
request.headerFields[.authorization] = mediaBrowserHeader(token: token)
return try await next(request, body, baseURL)
}
}
@ -32,18 +71,24 @@ public actor JellyfinClient {
private var token: String?
private var client: Client
private let clientConfig: Configuration
public init(serverURL: URL) {
self.serverURL = serverURL
self.token = nil
self.clientConfig = Configuration(dateTranscoder: JellyfinDateTranscoder())
self.client = Client(
serverURL: serverURL,
transport: URLSessionTransport()
configuration: clientConfig,
transport: URLSessionTransport(),
middlewares: [AuthMiddleware(token: nil)]
)
}
private func makeClient(token: String) -> Client {
Client(
serverURL: serverURL,
configuration: clientConfig,
transport: URLSessionTransport(),
middlewares: [AuthMiddleware(token: token)]
)