import Foundation 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? } struct Events: Decodable { let enabled: Bool? } let iptv: Iptv? let events: Events? let profile: ViewerProfile? let deviceId: String? var iptvEnabled: Bool { iptv?.enabled == true } var eventsEnabled: Bool { events?.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 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: String { streamId } let streamId: String let name: String let logoUrl: String? let number: Int? let categoryId: String? let nowTitle: String? enum CodingKeys: String, CodingKey { case streamId, name, logoUrl, number, categoryId, nowTitle } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) streamId = try decodeFlexibleString(c, forKey: .streamId) name = try c.decode(String.self, forKey: .name) logoUrl = try c.decodeIfPresent(String.self, forKey: .logoUrl) number = try c.decodeIfPresent(Int.self, forKey: .number) categoryId = try c.decodeIfPresent(String.self, forKey: .categoryId) nowTitle = try c.decodeIfPresent(String.self, forKey: .nowTitle) } } struct ClearKeyEntry: Decodable, Hashable { let kid: String let key: String } struct DrmInfo: Decodable, Hashable { let type: String let keys: [ClearKeyEntry] enum CodingKeys: String, CodingKey { case type, keys } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) type = try c.decodeIfPresent(String.self, forKey: .type) ?? "clearkey" keys = try c.decodeIfPresent([ClearKeyEntry].self, forKey: .keys) ?? [] } } struct IptvPlayInfo: Decodable { let streamId: String let name: String let logoUrl: String? let streamUrl: String let format: String? let fallbackStreamUrl: String? let fallbackFormat: String? let drm: DrmInfo? let watchSessionId: String? enum CodingKeys: String, CodingKey { case streamId, name, logoUrl, streamUrl, format case fallbackStreamUrl, fallbackFormat, drm, watchSessionId case eventId } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) if let sid = try? decodeFlexibleString(c, forKey: .streamId) { streamId = sid } else if let eid = try c.decodeIfPresent(String.self, forKey: .eventId) { streamId = eid } else { streamId = "" } name = try c.decode(String.self, forKey: .name) logoUrl = try c.decodeIfPresent(String.self, forKey: .logoUrl) streamUrl = try c.decode(String.self, forKey: .streamUrl) format = try c.decodeIfPresent(String.self, forKey: .format) fallbackStreamUrl = try c.decodeIfPresent(String.self, forKey: .fallbackStreamUrl) fallbackFormat = try c.decodeIfPresent(String.self, forKey: .fallbackFormat) drm = try c.decodeIfPresent(DrmInfo.self, forKey: .drm) watchSessionId = try c.decodeIfPresent(String.self, forKey: .watchSessionId) } var clearKeyEntries: [ClearKeyEntry] { guard let drm, drm.type.lowercased() == "clearkey", !drm.keys.isEmpty else { return [] } return drm.keys } var needsClearKey: Bool { !clearKeyEntries.isEmpty } } struct EpgProgram: Decodable, Hashable { let title: String let start: String? let end: String? } struct IptvEpg: Decodable, Hashable { let streamId: String let channelName: String let logoUrl: String? let now: EpgProgram? let next: EpgProgram? enum CodingKeys: String, CodingKey { case streamId, channelName, logoUrl, now, next } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) streamId = (try? decodeFlexibleString(c, forKey: .streamId)) ?? "" channelName = try c.decode(String.self, forKey: .channelName) logoUrl = try c.decodeIfPresent(String.self, forKey: .logoUrl) now = try c.decodeIfPresent(EpgProgram.self, forKey: .now) next = try c.decodeIfPresent(EpgProgram.self, forKey: .next) } } struct ScheduleEventsPayload: Decodable { let generatedAt: String? let events: [ScheduleEvent] let providers: [String]? enum CodingKeys: String, CodingKey { case generatedAt, events, providers } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) generatedAt = try c.decodeIfPresent(String.self, forKey: .generatedAt) events = try c.decodeIfPresent([ScheduleEvent].self, forKey: .events) ?? [] providers = try c.decodeIfPresent([String].self, forKey: .providers) } } struct ScheduleEvent: Decodable, Identifiable, Hashable { let id: String let name: String let description: String? let provider: String let category: String? let eventType: String? let subtitle: String? let channel: String? let network: String? let isLive: Bool? let phase: String? let playable: Bool? let status: String? let start: String let stop: String let startLocal: String? let stopLocal: String? let imageLandscape: String? let imageBackdrop: String? let imagePortrait: String? let logoUrl: String? let images: [String]? let audioLanguages: [String]? var isPlayable: Bool { playable == true } var phaseValue: String { (phase ?? "upcoming").lowercased() } enum CodingKeys: String, CodingKey { case id, name, description, provider, category, eventType, subtitle case channel, network, isLive, phase, playable, status case start, stop, startLocal, stopLocal case imageLandscape, imageBackdrop, imagePortrait, logoUrl, images, audioLanguages } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) name = try c.decode(String.self, forKey: .name) description = try c.decodeIfPresent(String.self, forKey: .description) provider = try c.decodeIfPresent(String.self, forKey: .provider) ?? "unknown" category = try c.decodeIfPresent(String.self, forKey: .category) eventType = try c.decodeIfPresent(String.self, forKey: .eventType) subtitle = try c.decodeIfPresent(String.self, forKey: .subtitle) channel = try c.decodeIfPresent(String.self, forKey: .channel) network = try c.decodeIfPresent(String.self, forKey: .network) isLive = try c.decodeIfPresent(Bool.self, forKey: .isLive) phase = try c.decodeIfPresent(String.self, forKey: .phase) playable = try c.decodeIfPresent(Bool.self, forKey: .playable) status = try c.decodeIfPresent(String.self, forKey: .status) start = try c.decodeIfPresent(String.self, forKey: .start) ?? "" stop = try c.decodeIfPresent(String.self, forKey: .stop) ?? "" startLocal = try c.decodeIfPresent(String.self, forKey: .startLocal) stopLocal = try c.decodeIfPresent(String.self, forKey: .stopLocal) imageLandscape = try c.decodeIfPresent(String.self, forKey: .imageLandscape) imageBackdrop = try c.decodeIfPresent(String.self, forKey: .imageBackdrop) imagePortrait = try c.decodeIfPresent(String.self, forKey: .imagePortrait) logoUrl = try c.decodeIfPresent(String.self, forKey: .logoUrl) images = try c.decodeIfPresent([String].self, forKey: .images) audioLanguages = try c.decodeIfPresent([String].self, forKey: .audioLanguages) } init( id: String, name: String, description: String?, provider: String, category: String?, eventType: String?, subtitle: String?, channel: String?, network: String?, isLive: Bool?, phase: String?, playable: Bool?, status: String?, start: String, stop: String, startLocal: String?, stopLocal: String?, imageLandscape: String?, imageBackdrop: String?, imagePortrait: String?, logoUrl: String?, images: [String]?, audioLanguages: [String]? ) { self.id = id self.name = name self.description = description self.provider = provider self.category = category self.eventType = eventType self.subtitle = subtitle self.channel = channel self.network = network self.isLive = isLive self.phase = phase self.playable = playable self.status = status self.start = start self.stop = stop self.startLocal = startLocal self.stopLocal = stopLocal self.imageLandscape = imageLandscape self.imageBackdrop = imageBackdrop self.imagePortrait = imagePortrait self.logoUrl = logoUrl self.images = images self.audioLanguages = audioLanguages } func withImages( landscape: String? = nil, backdrop: String? = nil, portrait: String? = nil, logo: String? = nil, imgs: [String]? = nil, channel: String? = nil, network: String? = nil ) -> ScheduleEvent { ScheduleEvent( id: id, name: name, description: description, provider: provider, category: category, eventType: eventType, subtitle: subtitle, channel: channel ?? self.channel, network: network ?? self.network, isLive: isLive, phase: phase, playable: playable, status: status, start: start, stop: stop, startLocal: startLocal, stopLocal: stopLocal, imageLandscape: landscape ?? imageLandscape, imageBackdrop: backdrop ?? imageBackdrop, imagePortrait: portrait ?? imagePortrait, logoUrl: logo ?? logoUrl, images: imgs ?? images, audioLanguages: audioLanguages ) } } /// API streamId kan String (`"123"` / `"custom:uuid"`) of legacy Int zijn. func decodeFlexibleString(_ c: KeyedDecodingContainer, forKey key: K) throws -> String { if let s = try? c.decode(String.self, forKey: key) { return s } if let i = try? c.decode(Int.self, forKey: key) { return String(i) } if let d = try? c.decode(Double.self, forKey: key) { return String(Int(d)) } throw DecodingError.dataCorruptedError(forKey: key, in: c, debugDescription: "Verwacht string of int id") } 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? }