stremio/apps/ios/VMCTV/Data/Models.swift
Jos Vooges | STH 3e2549c33a Fix iOS Live TV streamId String decode and add Events + ClearKey path.
Restore channel list decoding for string/custom IDs, URL-encode play paths, parse DRM/heartbeat, and ship Events tab (0.5.0/12) with best-effort VLC ClearKey.
2026-09-19 03:59:51 +02:00

421 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: String { streamId }
let streamId: String
let name: String
let logoUrl: String?
let number: Int?
let categoryId: String?
let nowTitle: String?
let nextTitle: String?
enum CodingKeys: String, CodingKey {
case streamId, name, logoUrl, number, categoryId, nowTitle, nextTitle
}
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)
nextTitle = try c.decodeIfPresent(String.self, forKey: .nextTitle)
}
}
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
}
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)
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)
}
}
struct ClearKeyEntry: Decodable, Hashable {
let kid: String
let key: String
}
struct DrmInfo: Decodable, Hashable {
let type: String
let keys: [ClearKeyEntry]
}
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: String
let channelName: String
let logoUrl: String?
let now: EpgProgram?
let next: EpgProgram?
enum CodingKeys: String, CodingKey {
case streamId, channelName, logoUrl, now, next
}
init(streamId: String, channelName: String, logoUrl: String?, now: EpgProgram?, next: EpgProgram?) {
self.streamId = streamId
self.channelName = channelName
self.logoUrl = logoUrl
self.now = now
self.next = 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)
}
}
/// 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")
}
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
}