import Foundation import SwiftData /// Ein heruntergeladener oder herunterzuladender Titel. /// /// Liegt in SwiftData, weil Downloads die App überleben: iOS überträgt im /// Hintergrund weiter, auch wenn die App beendet ist, und beim nächsten Start /// muss aus dem gespeicherten Zustand hervorgehen, was zu tun ist. @Model public final class DownloadItem { /// Der Schlüssel des Titels auf dem Server. Eindeutig — derselbe Titel /// soll nicht zweimal in der Warteschlange stehen. @Attribute(.unique) public var ratingKey: String /// Der Zustand als Zeichenkette. SwiftData kann Aufzählungen speichern, /// aber eine Zeichenkette bleibt bei Umbenennungen im Code lesbar — und /// ein unlesbarer Zustand hieße: laufende Downloads sind verloren. public var stateRaw: String public var title: String public var subtitle: String? /// Zu welcher Serie und Staffel der Titel gehört. Gruppiert die /// Downloads-Ansicht und ist Grundlage für „halte N Folgen offline". public var seriesKey: String? public var seriesTitle: String? public var seasonKey: String? public var seasonIndex: Int? public var episodeIndex: Int? public var totalBytes: Int64 public var receivedBytes: Int64 /// Dateiname im Download-Ordner. Nicht der volle Pfad: der Container der /// App bekommt bei jedem Systemupdate eine neue Kennung, ein gespeicherter /// absoluter Pfad zeigt danach ins Leere. public var localFileName: String? public var artworkFileName: String? public var addedAt: Date public var completedAt: Date? /// Wann der Titel durchgesehen wurde — Grundlage fürs automatische Löschen. public var watchedAt: Date? public var failureMessage: String? /// Woher geladen wird. Enthält den Token, deshalb liegt er im geschützten /// App-Container und nicht in den Voreinstellungen. public var sourceURLString: String public var durationMilliseconds: Int? /// Was der Player offline über die Datei wissen muss: Container, Codecs, /// Spuren, Marken. Als JSON, weil die Form von Plex vorgegeben ist und /// sich eigene SwiftData-Modelle dafür nicht lohnen. public var offlineMediaData: Data? /// Zum Fortsetzen nach dem Pausieren. iOS liefert diesen Datensatz beim /// Abbrechen; ohne ihn beginnt ein pausierter Download wieder bei null. public var resumeData: Data? public init( ratingKey: String, title: String, subtitle: String? = nil, sourceURLString: String, state: DownloadState = .queued, totalBytes: Int64 = 0, seriesKey: String? = nil, seriesTitle: String? = nil, seasonKey: String? = nil, seasonIndex: Int? = nil, episodeIndex: Int? = nil, durationMilliseconds: Int? = nil, offlineMediaData: Data? = nil ) { self.ratingKey = ratingKey self.title = title self.subtitle = subtitle self.sourceURLString = sourceURLString self.stateRaw = state.rawValue self.totalBytes = totalBytes self.receivedBytes = 0 self.seriesKey = seriesKey self.seriesTitle = seriesTitle self.seasonKey = seasonKey self.seasonIndex = seasonIndex self.episodeIndex = episodeIndex self.durationMilliseconds = durationMilliseconds self.offlineMediaData = offlineMediaData self.addedAt = Date() } public var state: DownloadState { get { DownloadState(rawValue: stateRaw) ?? .failed } set { stateRaw = newValue.rawValue } } /// 0…1, oder `nil` solange die Gesamtgröße unbekannt ist. /// /// Ein Balken bei null Prozent, der sich nie bewegt, sieht aus wie ein /// hängender Download — ohne Gesamtgröße ist ein unbestimmter Fortschritt /// die ehrlichere Anzeige. public var progress: Double? { guard totalBytes > 0 else { return nil } return min(Double(receivedBytes) / Double(totalBytes), 1) } public var isComplete: Bool { state == .completed } /// Wie viel Platz dieser Eintrag gerade belegt. public var occupiedBytes: Int64 { state.occupiesStorage ? max(receivedBytes, 0) : 0 } } /// Was der Player offline über eine Datei wissen muss. /// /// Ein Abzug der Serverangaben zum Zeitpunkt des Downloads. Offline lässt sich /// nichts nachfragen — und der `PlaybackDecider` braucht Container und Codecs, /// um zwischen die Engines zu wählen. public struct OfflineMedia: Sendable, Codable, Equatable { public var container: String? public var videoCodec: String? public var audioCodec: String? public var videoResolution: String? public var bitrate: Int? public var audioLanguages: [String] public var subtitleLanguages: [String] /// Intro und Abspann, in Millisekunden. public var markers: [OfflineMarker] public init( container: String? = nil, videoCodec: String? = nil, audioCodec: String? = nil, videoResolution: String? = nil, bitrate: Int? = nil, audioLanguages: [String] = [], subtitleLanguages: [String] = [], markers: [OfflineMarker] = [] ) { self.container = container self.videoCodec = videoCodec self.audioCodec = audioCodec self.videoResolution = videoResolution self.bitrate = bitrate self.audioLanguages = audioLanguages self.subtitleLanguages = subtitleLanguages self.markers = markers } } public struct OfflineMarker: Sendable, Codable, Equatable { public var type: String public var startMilliseconds: Int public var endMilliseconds: Int public init(type: String, startMilliseconds: Int, endMilliseconds: Int) { self.type = type self.startMilliseconds = startMilliseconds self.endMilliseconds = endMilliseconds } }