import Foundation import PlexKit /// Die Regeln, nach denen Downloads von selbst entstehen und verschwinden. /// /// Bewusst als reine Rechnung ohne Zugriff auf Datenbank, Dateien oder Netz: /// sie bekommt den Bestand hereingereicht und gibt zurück, was zu tun wäre. /// Dadurch lässt sich das Verhalten prüfen, ohne etwas zu löschen — und beim /// automatischen Löschen von Filmen ist das keine akademische Vorsicht. public struct DownloadRules: Sendable, Equatable { /// Gesehene Titel nach dieser Frist entfernen. `nil` schaltet es ab. /// /// Eine Frist statt sofortigem Löschen: wer eine Folge zu Ende sieht und /// gleich noch einmal die letzte Szene sucht, soll sie nicht schon /// verloren haben. public var deleteWatchedAfter: Duration? /// Wie viele ungesehene Folgen je Serie vorgehalten werden. `nil` /// schaltet das Nachladen ab. public var keepUnwatchedEpisodes: Int? public init(deleteWatchedAfter: Duration? = nil, keepUnwatchedEpisodes: Int? = nil) { self.deleteWatchedAfter = deleteWatchedAfter self.keepUnwatchedEpisodes = keepUnwatchedEpisodes } public static let disabled = DownloadRules() /// Welche Einträge nach den Regeln zu entfernen sind. public func itemsToDelete( from items: [DownloadItem], now: Date = Date() ) -> [DownloadItem] { guard let deleteWatchedAfter else { return [] } return items.filter { item in // Nur Fertiges: ein laufender Download hat nichts, was gesehen // worden sein könnte. guard item.state == .completed, let watchedAt = item.watchedAt else { return false } return now.timeIntervalSince(watchedAt) >= deleteWatchedAfter.seconds } } /// Wie viele Folgen je Serie noch fehlen, um den Vorrat zu füllen. /// /// Gezählt wird, was offline und ungesehen ist. Gesehene Folgen zählen /// nicht mit — sonst bliebe der Vorrat bei drei gesehenen Folgen stehen /// und es käme nie etwas Neues nach. public func missingEpisodeCount( forSeries seriesKey: String, in items: [DownloadItem] ) -> Int { guard let keepUnwatchedEpisodes else { return 0 } let unwatchedOffline = items.filter { $0.seriesKey == seriesKey && $0.watchedAt == nil // Wartende und laufende zählen mit: sonst würde bei jedem // Aufruf erneut nachgelegt, bis die ganze Serie auf dem Gerät // liegt. && $0.state != .failed }.count return max(0, keepUnwatchedEpisodes - unwatchedOffline) } /// Die Serien, für die nachgeladen werden müsste. public func seriesNeedingRefill(in items: [DownloadItem]) -> [String: Int] { guard keepUnwatchedEpisodes != nil else { return [:] } let seriesKeys = Set(items.compactMap(\.seriesKey)) var result: [String: Int] = [:] for key in seriesKeys { let missing = missingEpisodeCount(forSeries: key, in: items) if missing > 0 { result[key] = missing } } return result } } extension Duration { /// Gängige Fristen fürs automatische Löschen. public static let immediately = Duration.zero public static let afterOneDay = Duration.seconds(24 * 60 * 60) public static let afterOneWeek = Duration.seconds(7 * 24 * 60 * 60) }