stremio/apps/ios/VMC/Data/Models.swift

774 lines
24 KiB
Swift

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]?
let positionMs: Int64?
let durationMs: Int64?
let completed: Bool?
var preferredMediaFileId: String? {
mediaFiles?.first(where: { $0.nodeOnline != false })?.id ?? mediaFiles?.first?.id
}
var resumeMs: Int64 {
if completed == true { return 0 }
return max(0, positionMs ?? 0)
}
}
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?
let live: Bool?
enum CodingKeys: String, CodingKey {
case streamId, name, logoUrl, streamUrl, format
case fallbackStreamUrl, fallbackFormat, drm, watchSessionId
case eventId, live
}
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)
live = try c.decodeIfPresent(Bool.self, forKey: .live)
}
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 sportBucket: String?
let sportLabel: 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() }
var resolvedSportBucket: String {
let b = (sportBucket ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return b.isEmpty ? "other" : b
}
var resolvedSportLabel: String {
if let s = sportLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !s.isEmpty {
return s
}
return EventSport.label(for: resolvedSportBucket)
}
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 sportBucket, sportLabel
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)
sportBucket = try c.decodeIfPresent(String.self, forKey: .sportBucket)
sportLabel = try c.decodeIfPresent(String.self, forKey: .sportLabel)
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?,
sportBucket: String? = nil,
sportLabel: String? = nil,
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.sportBucket = sportBucket
self.sportLabel = sportLabel
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,
sportBucket: sportBucket,
sportLabel: sportLabel,
imageLandscape: landscape ?? imageLandscape,
imageBackdrop: backdrop ?? imageBackdrop,
imagePortrait: portrait ?? imagePortrait,
logoUrl: logo ?? logoUrl,
images: imgs ?? images,
audioLanguages: audioLanguages
)
}
}
enum EventSport {
static let order = [
"football", "racing", "tennis", "golf", "darts", "snooker",
"basketball", "waterpolo", "sailing", "hockey", "cycling", "other",
]
static func label(for bucket: String) -> String {
switch bucket.lowercased() {
case "football": return "Voetbal"
case "racing": return "Racing"
case "tennis": return "Tennis"
case "golf": return "Golf"
case "darts": return "Darts"
case "snooker": return "Snooker"
case "basketball": return "Basketbal"
case "waterpolo": return "Waterpolo"
case "sailing": return "Zeilen"
case "hockey": return "Hockey"
case "cycling": return "Wielrennen"
default: return "Overig"
}
}
}
/// API streamId kan String (`"123"` / `"custom:uuid"`) of legacy Int zijn.
func decodeFlexibleString<K: CodingKey>(_ c: KeyedDecodingContainer<K>, 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?
}
// MARK: - Event hubs (F1 / Viaplay)
struct EventHub: Decodable, Identifiable, Hashable {
let id: String
let label: String
let subtitle: String?
}
struct F1Meeting: Decodable, Identifiable, Hashable {
var id: String { meetingKey }
let meetingKey: String
let title: String
let meetingNumber: Int?
let location: String?
let startMs: Double?
let endMs: Double?
let imageUrl: String?
enum CodingKeys: String, CodingKey {
case meetingKey, title, meetingNumber, location, startMs, endMs, imageUrl
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
meetingKey = try decodeFlexibleString(c, forKey: .meetingKey)
title = try c.decodeIfPresent(String.self, forKey: .title) ?? ""
meetingNumber = try? c.decodeIfPresent(Int.self, forKey: .meetingNumber)
location = try c.decodeIfPresent(String.self, forKey: .location)
startMs = try? c.decodeIfPresent(Double.self, forKey: .startMs)
endMs = try? c.decodeIfPresent(Double.self, forKey: .endMs)
imageUrl = try c.decodeIfPresent(String.self, forKey: .imageUrl)
}
}
struct F1ReplayGroup: Decodable, Identifiable, Hashable {
var id: String { meetingKey }
let meetingKey: String
let title: String
let location: String?
let meetingNumber: Int?
let imageUrl: String?
let latestStart: String?
let sessions: [ScheduleEvent]
enum CodingKeys: String, CodingKey {
case meetingKey, title, location, meetingNumber, imageUrl, latestStart, sessions
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
meetingKey = try decodeFlexibleString(c, forKey: .meetingKey)
title = try c.decodeIfPresent(String.self, forKey: .title) ?? ""
location = try c.decodeIfPresent(String.self, forKey: .location)
meetingNumber = try? c.decodeIfPresent(Int.self, forKey: .meetingNumber)
imageUrl = try c.decodeIfPresent(String.self, forKey: .imageUrl)
latestStart = try c.decodeIfPresent(String.self, forKey: .latestStart)
sessions = try c.decodeIfPresent([ScheduleEvent].self, forKey: .sessions) ?? []
}
}
struct F1HubPayload: Decodable {
let live: [ScheduleEvent]
let upcoming: [ScheduleEvent]
let recentReplays: [ScheduleEvent]
let replayGroups: [F1ReplayGroup]
let meetings: [F1Meeting]
let nextMeeting: F1Meeting?
enum CodingKeys: String, CodingKey {
case live, upcoming, recentReplays, replayGroups, meetings, nextMeeting
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
live = try c.decodeIfPresent([ScheduleEvent].self, forKey: .live) ?? []
upcoming = try c.decodeIfPresent([ScheduleEvent].self, forKey: .upcoming) ?? []
recentReplays = try c.decodeIfPresent([ScheduleEvent].self, forKey: .recentReplays) ?? []
replayGroups = try c.decodeIfPresent([F1ReplayGroup].self, forKey: .replayGroups) ?? []
meetings = try c.decodeIfPresent([F1Meeting].self, forKey: .meetings) ?? []
nextMeeting = try c.decodeIfPresent(F1Meeting.self, forKey: .nextMeeting)
}
}
struct F1MeetingDetail: Decodable {
let meeting: F1Meeting?
let sessions: [ScheduleEvent]
let replays: [ScheduleEvent]
let upcoming: [ScheduleEvent]
let live: [ScheduleEvent]
enum CodingKeys: String, CodingKey {
case meeting, sessions, replays, upcoming, live
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
meeting = try c.decodeIfPresent(F1Meeting.self, forKey: .meeting)
sessions = try c.decodeIfPresent([ScheduleEvent].self, forKey: .sessions) ?? []
replays = try c.decodeIfPresent([ScheduleEvent].self, forKey: .replays) ?? []
upcoming = try c.decodeIfPresent([ScheduleEvent].self, forKey: .upcoming) ?? []
live = try c.decodeIfPresent([ScheduleEvent].self, forKey: .live) ?? []
}
}
struct ViaplaySport: Decodable, Identifiable, Hashable {
let id: String
let label: String
let count: Int?
}
struct ViaplayHubItem: Decodable, Identifiable, Hashable {
let id: String
let guid: String
let title: String
let subtitle: String?
let imageUrl: String?
let kind: String?
let openAs: String?
let playable: Bool?
let durationLabel: String?
let start: String?
let badge: String?
let pageUrl: String?
var opensAsEvent: Bool { (openAs ?? "").lowercased() == "event" }
}
struct ViaplayHubRail: Decodable, Identifiable, Hashable {
let id: String
let title: String
let items: [ViaplayHubItem]
enum CodingKeys: String, CodingKey { case id, title, items }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
title = try c.decodeIfPresent(String.self, forKey: .title) ?? ""
items = (try? c.decodeIfPresent([ViaplayHubItem].self, forKey: .items)) ?? []
}
}
struct ViaplayHubPayload: Decodable {
let live: [ScheduleEvent]
let today: [ScheduleEvent]
let weekend: [ScheduleEvent]
let later: [ScheduleEvent]
let catchup: [ScheduleEvent]
let sports: [ViaplaySport]
let events: [ScheduleEvent]
let rails: [ViaplayHubRail]
enum CodingKeys: String, CodingKey {
case live, today, weekend, later, catchup, sports, events, rails
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
live = try c.decodeIfPresent([ScheduleEvent].self, forKey: .live) ?? []
today = try c.decodeIfPresent([ScheduleEvent].self, forKey: .today) ?? []
weekend = try c.decodeIfPresent([ScheduleEvent].self, forKey: .weekend) ?? []
later = try c.decodeIfPresent([ScheduleEvent].self, forKey: .later) ?? []
catchup = try c.decodeIfPresent([ScheduleEvent].self, forKey: .catchup) ?? []
sports = try c.decodeIfPresent([ViaplaySport].self, forKey: .sports) ?? []
events = try c.decodeIfPresent([ScheduleEvent].self, forKey: .events) ?? []
rails = try c.decodeIfPresent([ViaplayHubRail].self, forKey: .rails) ?? []
}
}