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:
@@ -0,0 +1,125 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import SwiftUI
|
||||
import FloatplaneAPIClient
|
||||
|
||||
extension ErrorModel: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
let message = self.message ?? self.errors.first?.message ?? self.errors.first?.name ?? "Error message unretrievable."
|
||||
let separator: String
|
||||
if message.last != "." {
|
||||
separator = ". "
|
||||
} else {
|
||||
separator = " "
|
||||
}
|
||||
return message + separator + "Error ID: " + self.id
|
||||
}
|
||||
|
||||
public var failureReason: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
public var recoverySuggestion: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
public var helpAnchor: String? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoAttachmentModel: Identifiable { }
|
||||
|
||||
extension PictureAttachmentModel: Identifiable { }
|
||||
|
||||
extension AudioAttachmentModel: Identifiable { }
|
||||
|
||||
extension BlogPostModelV3: Identifiable { }
|
||||
|
||||
extension UserSubscriptionModel: Identifiable {
|
||||
public var id: String {
|
||||
self.creator
|
||||
}
|
||||
}
|
||||
|
||||
protocol ImageModelShared {
|
||||
var width: Int { get set }
|
||||
var height: Int { get set }
|
||||
var path: String { get set }
|
||||
var childImages: [ChildImageModel]? { get set }
|
||||
}
|
||||
|
||||
extension ImageModelShared {
|
||||
var aspectRatio: CGFloat {
|
||||
return CGFloat(self.width) / CGFloat(self.height)
|
||||
}
|
||||
|
||||
func bestImage(for geometrySize: CGSize?) -> URL? {
|
||||
guard let geometrySize else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let geometryMagnitude = Int(geometrySize.width * geometrySize.height)
|
||||
var difference = (self.width * self.height) - geometryMagnitude
|
||||
var path = self.path
|
||||
|
||||
for childImage in self.childImages ?? [] {
|
||||
let childDifference = (childImage.width * childImage.height) - geometryMagnitude
|
||||
|
||||
if childDifference >= 0 && childDifference < difference {
|
||||
difference = childDifference
|
||||
path = childImage.path
|
||||
}
|
||||
}
|
||||
|
||||
return URL(string: path)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageModel: ImageModelShared {
|
||||
}
|
||||
|
||||
extension Optional where Wrapped == ImageModelShared {
|
||||
var pathUrlOrNil: URL? {
|
||||
if let thumbnail = self {
|
||||
return URL(string: thumbnail.path)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func bestImage(for geometrySize: CGSize?) -> URL? {
|
||||
if let thumbnail = self {
|
||||
return thumbnail.bestImage(for: geometrySize)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BlogPostModelV3Channel {
|
||||
var asChannelModel: ChannelModel? {
|
||||
switch self {
|
||||
case let .typeChannelModel(channelModel):
|
||||
return channelModel
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BlogPostModelV3 {
|
||||
var firstVideoAttachmentId: String? {
|
||||
return self.attachmentOrder.filter({ self.videoAttachments?.contains($0) == true }).first
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentPostV3Response {
|
||||
var firstVideoAttachmentId: String? {
|
||||
return self.attachmentOrder.filter({ self.videoAttachments?.lazy.map(\.id).contains($0) == true }).first
|
||||
}
|
||||
|
||||
var firstVideoAttachment: VideoAttachmentModel? {
|
||||
return self.videoAttachments?.first(where: { $0.id == firstVideoAttachmentId })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user