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 }