Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
Some checks failed
release / build-release (push) Has been cancelled
Some checks failed
release / build-release (push) Has been cancelled
- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped) - IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the AES-128 key is fetched server-side with the session cookie, segments stay on the CDN - delivery/info cached per token lifetime and throttled (~2/min) - config page: paste sails.sid + validate, extra creators, quality, search test - scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow - research docs, HAR analysis (HAR itself gitignored), install-pi5.sh Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
119
doc/research/reference-sources/wf_Wasserflug_tvOSApp.swift
Normal file
119
doc/research/reference-sources/wf_Wasserflug_tvOSApp.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
import SwiftUI
|
||||
import FloatplaneAPIClient
|
||||
import Vapor
|
||||
import os
|
||||
import Logging
|
||||
import OAuthKit
|
||||
|
||||
@main
|
||||
struct Wasserflug_tvOSApp: App {
|
||||
static var logger: Logging.Logger {
|
||||
var logger = Logging.Logger(label: Bundle.main.bundleIdentifier!)
|
||||
#if DEBUG
|
||||
// For debugging, log at a lower level to get more information.
|
||||
logger.logLevel = .info
|
||||
#else
|
||||
// For release mode, only log important items.
|
||||
logger.logLevel = .notice
|
||||
#endif
|
||||
return logger
|
||||
}
|
||||
|
||||
static var networkLogger: Logging.Logger {
|
||||
var logger = Logging.Logger(label: Bundle.main.bundleIdentifier!)
|
||||
logger.logLevel = .info
|
||||
logger[metadataKey: "category"] = "network"
|
||||
return logger
|
||||
}
|
||||
|
||||
var oauth: OAuth = .init()
|
||||
let vaporApp = Vapor.Application(.production, .singleton)
|
||||
let fpApiService: FPAPIService = DefaultFPAPIService()
|
||||
let authViewModel: AuthViewModel
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
||||
init() {
|
||||
// Set custom user agent for network requests. This is particularly
|
||||
// required in order to pass the login phase with bypassing the captcha.
|
||||
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "<unknown>"
|
||||
let bundleVersion = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? "<unknown>"
|
||||
Wasserflug_tvOSApp.setHttp(header: "User-Agent", value: "Wasserflug tvOS App version \(appVersion)-\(bundleVersion), CFNetwork")
|
||||
|
||||
oauth = .init(providers: [.init(
|
||||
id: "https://auth.floatplane.com/realms/floatplane",
|
||||
authorizationURL: URL(string: "https://auth.floatplane.com/realms/floatplane/protocol/openid-connect/auth")!,
|
||||
accessTokenURL: URL(string: "https://auth.floatplane.com/realms/floatplane/protocol/openid-connect/token")!,
|
||||
deviceCodeURL: URL(string: "https://auth.floatplane.com/realms/floatplane/protocol/openid-connect/auth/device")!,
|
||||
clientID: "wasserflug",
|
||||
clientSecret: nil,
|
||||
encodeHttpBody: true,
|
||||
customUserAgent: "Wasserflug tvOS App version \(appVersion)-\(bundleVersion), CFNetwork",
|
||||
debug: true,
|
||||
)], options: [
|
||||
.autoRefresh: true,
|
||||
])
|
||||
|
||||
// Attempt to use any previous authentication cookies, so the user does
|
||||
// not need to login on every app start.
|
||||
// FloatplaneAPIClientAPI.loadAuthenticationCookiesFromStorage()
|
||||
FloatplaneAPIClientAPI.basePath = "https://www.floatplane.com"
|
||||
|
||||
// Use FP's date format for JSON encoding/decoding.
|
||||
let fpDateFormatter = DateFormatter()
|
||||
fpDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
fpDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
Configuration.contentConfiguration.use(encoder: JSONEncoder.custom(dates: .formatted(fpDateFormatter)), for: .json)
|
||||
Configuration.contentConfiguration.use(decoder: JSONDecoder.custom(dates: .formatted(fpDateFormatter)), for: .json)
|
||||
|
||||
// Bootstrap core logging.
|
||||
LoggingSystem.bootstrap({ label -> LogHandler in
|
||||
var loggingLogger = OSLoggingLogger(label: label, category: "Wasserflug")
|
||||
loggingLogger.logLevel = .debug
|
||||
return MultiplexLogHandler([
|
||||
loggingLogger,
|
||||
])
|
||||
})
|
||||
|
||||
// Create and store in @State the main view model.
|
||||
authViewModel = AuthViewModel(fpApiService: fpApiService)
|
||||
}
|
||||
|
||||
func setup() {
|
||||
// Bootstrap API/Network logging.
|
||||
Configuration.apiClient = vaporApp.client
|
||||
.logging(to: Wasserflug_tvOSApp.networkLogger)
|
||||
Configuration.apiWrapper = { clientRequest in
|
||||
switch oauth.state {
|
||||
case let .authorized(provider, auth):
|
||||
clientRequest.headers.bearerAuthorization = .init(token: auth.token.accessToken)
|
||||
default:
|
||||
break
|
||||
}
|
||||
Wasserflug_tvOSApp.networkLogger.info("Sending \(clientRequest.method) request to \(clientRequest.url)")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(viewModel: authViewModel)
|
||||
.environment(\.fpApiService, fpApiService)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environment(oauth)
|
||||
.onAppear {
|
||||
setup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func setHttp(header: String, value: String) {
|
||||
FloatplaneAPIClientAPI.customHeaders.replaceOrAdd(name: header, value: value)
|
||||
if var headers = URLSession.shared.configuration.httpAdditionalHeaders {
|
||||
headers[header] = value
|
||||
URLSession.shared.configuration.httpAdditionalHeaders = headers
|
||||
} else {
|
||||
URLSession.shared.configuration.httpAdditionalHeaders = [
|
||||
header: value,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user