Wire FAAB schedule-events into master-api and admin IPTV settings, add Android TV Events UI, fix mobile ClearKey/stream playback (DASH + bump 1.0.23), and scaffold the Apple TV (VMCTV) client with docs/scripts.
347 lines
8.1 KiB
Swift
347 lines
8.1 KiB
Swift
import Foundation
|
||
import CoreGraphics
|
||
|
||
struct DeviceCodeResponse: Decodable {
|
||
let code: String
|
||
let expiresAt: String?
|
||
let pollIntervalSeconds: Int?
|
||
let linkUrl: String?
|
||
let qrPayload: String?
|
||
}
|
||
|
||
enum PollResult {
|
||
case pending
|
||
case expired
|
||
case alreadyLinked
|
||
case linked(refreshToken: String, viewerName: String)
|
||
}
|
||
|
||
struct HomePayload: Decodable {
|
||
let continueWatching: [MediaItem]
|
||
let spotlight: [MediaItem]?
|
||
let shelves: [ShelfRow]
|
||
let favorites: [MediaItem]?
|
||
}
|
||
|
||
struct ShelfRow: Decodable, Identifiable {
|
||
let id: String
|
||
let title: String
|
||
let name: String?
|
||
let kind: String?
|
||
let slug: String?
|
||
let items: [MediaItem]
|
||
}
|
||
|
||
struct MediaItem: Codable, Identifiable, Hashable {
|
||
let type: String
|
||
let id: String
|
||
let title: String
|
||
let subtitle: String?
|
||
let year: Int?
|
||
let posterUrl: String?
|
||
let backdropUrl: String?
|
||
let logoUrl: String?
|
||
let mediaFileId: String?
|
||
let seriesId: String?
|
||
let positionMs: Int64?
|
||
let durationMs: Int64?
|
||
let progressId: String?
|
||
let badge: String?
|
||
}
|
||
|
||
struct PlayResult: Decodable {
|
||
let streamUrl: String
|
||
let sessionId: String?
|
||
let mediaFileId: String?
|
||
let defaultSubtitle: DefaultSubtitle?
|
||
}
|
||
|
||
struct DefaultSubtitle: Decodable {
|
||
let fileId: Int?
|
||
let source: String?
|
||
let language: String?
|
||
let fileName: String?
|
||
let format: String?
|
||
let contentBase64: String
|
||
let label: String?
|
||
}
|
||
|
||
struct ViewerProfile: Decodable, Identifiable, Hashable {
|
||
let id: String
|
||
let name: String
|
||
let avatarId: String?
|
||
let isOwner: Bool?
|
||
let isKids: Bool?
|
||
let hasPin: Bool?
|
||
let showAvatar: Bool?
|
||
let hasCustomAvatar: Bool?
|
||
let avatarUrl: String?
|
||
|
||
var showsAvatar: Bool { showAvatar ?? true }
|
||
var hasCustomPhoto: Bool { hasCustomAvatar == true }
|
||
}
|
||
|
||
struct ProfilesPayload: Decodable {
|
||
let profiles: [ViewerProfile]
|
||
let activeProfileId: String?
|
||
let canManage: Bool?
|
||
}
|
||
|
||
struct ClientMe: Decodable {
|
||
struct Iptv: Decodable { let enabled: Bool? }
|
||
let iptv: Iptv?
|
||
let profile: ViewerProfile?
|
||
let deviceId: String?
|
||
|
||
var iptvEnabled: Bool { iptv?.enabled == true }
|
||
}
|
||
|
||
struct LinkedDevice: Decodable, Identifiable, Hashable {
|
||
let id: String
|
||
let name: String
|
||
let platform: String?
|
||
let lastSeenAt: String?
|
||
let createdAt: String?
|
||
let current: Bool?
|
||
}
|
||
|
||
struct DevicesPayload: Decodable {
|
||
let devices: [LinkedDevice]
|
||
}
|
||
|
||
struct ApproveDeviceResult: Decodable {
|
||
let ok: Bool?
|
||
let deviceName: String?
|
||
let platform: String?
|
||
}
|
||
|
||
struct MediaFileInfo: Decodable, Identifiable, Hashable {
|
||
let id: String
|
||
let label: String?
|
||
let resolution: String?
|
||
let videoCodec: String?
|
||
let audioCodec: String?
|
||
let hdrType: String?
|
||
let nodeOnline: Bool?
|
||
let nodeName: String?
|
||
}
|
||
|
||
struct MovieDetail: Decodable {
|
||
let id: String
|
||
let title: String
|
||
let overview: String?
|
||
let year: Int?
|
||
let posterUrl: String?
|
||
let backdropUrl: String?
|
||
let logoUrl: String?
|
||
let runtime: Int?
|
||
let genres: [String]?
|
||
let rating: String?
|
||
let cast: [String]?
|
||
let director: [String]?
|
||
let mediaFiles: [MediaFileInfo]?
|
||
|
||
var preferredMediaFileId: String? {
|
||
mediaFiles?.first(where: { $0.nodeOnline != false })?.id ?? mediaFiles?.first?.id
|
||
}
|
||
}
|
||
|
||
struct SeriesDetail: Decodable {
|
||
let id: String
|
||
let title: String
|
||
let overview: String?
|
||
let year: Int?
|
||
let posterUrl: String?
|
||
let backdropUrl: String?
|
||
let logoUrl: String?
|
||
let genres: [String]?
|
||
let rating: String?
|
||
let cast: [String]?
|
||
let director: [String]?
|
||
let seasons: [Season]?
|
||
}
|
||
|
||
struct Season: Decodable, Identifiable {
|
||
var id: Int { seasonNumber }
|
||
let seasonNumber: Int
|
||
let episodes: [Episode]?
|
||
}
|
||
|
||
struct Episode: Decodable, Identifiable {
|
||
let id: String
|
||
let title: String?
|
||
let episodeNumber: Int?
|
||
let seasonNumber: Int?
|
||
let overview: String?
|
||
let stillUrl: String?
|
||
let mediaFileId: String?
|
||
let available: Bool?
|
||
let runtime: Int?
|
||
let completed: Bool?
|
||
let positionMs: Int64?
|
||
let durationMs: Int64?
|
||
|
||
var progressFraction: CGFloat {
|
||
if completed == true { return 1 }
|
||
guard let dur = durationMs, dur > 0, let pos = positionMs else { return 0 }
|
||
return CGFloat(pos) / CGFloat(dur)
|
||
}
|
||
|
||
var resumeMs: Int64 {
|
||
if completed == true { return 0 }
|
||
return max(0, positionMs ?? 0)
|
||
}
|
||
}
|
||
|
||
struct NextEpisode: Decodable, Identifiable, Hashable {
|
||
let id: String
|
||
let seriesId: String
|
||
let seasonNumber: Int?
|
||
let episodeNumber: Int?
|
||
let title: String?
|
||
let seriesTitle: String?
|
||
let subtitle: String?
|
||
let mediaFileId: String
|
||
let posterUrl: String?
|
||
}
|
||
|
||
struct SearchPayload: Decodable {
|
||
let results: [MediaItem]
|
||
}
|
||
|
||
struct BrowsePayload: Decodable {
|
||
let id: String?
|
||
let title: String?
|
||
let kind: String?
|
||
let total: Int?
|
||
let offset: Int?
|
||
let items: [MediaItem]
|
||
}
|
||
|
||
struct SubtitleHit: Decodable, Identifiable, Hashable {
|
||
var id: String { "\(source ?? "x")-\(fileId)" }
|
||
let fileId: Int
|
||
let language: String?
|
||
let release: String?
|
||
let fileName: String?
|
||
let downloadCount: Int?
|
||
let hearingImpaired: Bool?
|
||
let source: String?
|
||
let uploader: String?
|
||
}
|
||
|
||
struct SubtitleSearchResult: Decodable {
|
||
let configured: Bool?
|
||
let imdbId: String?
|
||
let title: String?
|
||
let results: [SubtitleHit]
|
||
}
|
||
|
||
struct FetchedSubtitle: Decodable {
|
||
let fileId: Int?
|
||
let source: String?
|
||
let fileName: String?
|
||
let format: String?
|
||
let contentBase64: String
|
||
}
|
||
|
||
struct IptvCategory: Decodable, Identifiable, Hashable {
|
||
let id: String
|
||
let name: String
|
||
let channelCount: Int?
|
||
}
|
||
|
||
struct IptvChannel: Decodable, Identifiable, Hashable {
|
||
var id: Int { streamId }
|
||
let streamId: Int
|
||
let name: String
|
||
let logoUrl: String?
|
||
let number: Int?
|
||
let categoryId: String?
|
||
let nowTitle: String?
|
||
let nextTitle: String?
|
||
}
|
||
|
||
struct IptvPlayInfo: Decodable {
|
||
let streamId: Int
|
||
let name: String
|
||
let logoUrl: String?
|
||
let streamUrl: String
|
||
let format: String?
|
||
let fallbackStreamUrl: String?
|
||
let fallbackFormat: String?
|
||
}
|
||
|
||
struct EpgProgram: Decodable, Hashable {
|
||
let title: String
|
||
let start: String?
|
||
let end: String?
|
||
|
||
private static func parseISO(_ raw: String?) -> Date? {
|
||
guard let raw, !raw.isEmpty else { return nil }
|
||
if let d = ISO8601DateFormatter.vmcFractional.date(from: raw) { return d }
|
||
return ISO8601DateFormatter.vmcBasic.date(from: raw)
|
||
}
|
||
|
||
var progress: CGFloat {
|
||
guard let s = Self.parseISO(start), let e = Self.parseISO(end) else { return 0 }
|
||
let total = e.timeIntervalSince(s)
|
||
guard total > 0 else { return 0 }
|
||
return CGFloat(min(1, max(0, Date().timeIntervalSince(s) / total)))
|
||
}
|
||
|
||
var timeRangeLabel: String? {
|
||
guard let s = Self.parseISO(start), let e = Self.parseISO(end) else { return nil }
|
||
let f = DateFormatter()
|
||
f.locale = Locale(identifier: "nl_NL")
|
||
f.dateFormat = "HH:mm"
|
||
return "\(f.string(from: s)) – \(f.string(from: e))"
|
||
}
|
||
}
|
||
|
||
struct IptvEpg: Decodable, Hashable {
|
||
let streamId: Int
|
||
let channelName: String
|
||
let logoUrl: String?
|
||
let now: EpgProgram?
|
||
let next: EpgProgram?
|
||
}
|
||
|
||
extension ISO8601DateFormatter {
|
||
static let vmcFractional: ISO8601DateFormatter = {
|
||
let f = ISO8601DateFormatter()
|
||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||
return f
|
||
}()
|
||
|
||
static let vmcBasic: ISO8601DateFormatter = {
|
||
let f = ISO8601DateFormatter()
|
||
f.formatOptions = [.withInternetDateTime]
|
||
return f
|
||
}()
|
||
}
|
||
|
||
struct ShelfPref: Codable, Identifiable, Hashable {
|
||
var id: String { shelfId }
|
||
let shelfId: String
|
||
let name: String
|
||
let slug: String?
|
||
let kind: String?
|
||
var visible: Bool
|
||
var sortOrder: Int
|
||
}
|
||
|
||
struct FavoriteToggleResult: Decodable {
|
||
let favorited: Bool
|
||
let items: [MediaItem]?
|
||
}
|
||
|
||
struct ApiErrorBody: Decodable {
|
||
struct ErrorObj: Decodable { let message: String? }
|
||
let error: ErrorObj?
|
||
}
|
||
|
||
struct PlayerTrack: Identifiable, Hashable {
|
||
let id: Int32
|
||
let name: String
|
||
}
|