Files
Flaro/Packages/FlixyKit/Sources/FlixyDownloads/Engine/DownloadRules.swift
T
GuidoandClaude Opus 5 af23372997 Phase 4 und Teile von Phase 5
Download-Regeln
- Automatisches Löschen nach dem Ansehen, mit Frist statt sofort: wer
  die letzte Szene noch einmal sucht, soll sie nicht schon verloren
  haben. „Nie" ist die Voreinstellung.
- „Halte N ungesehene Folgen offline": wartende und laufende Downloads
  zählen mit, sonst würde bei jedem Durchlauf nachgelegt, bis die ganze
  Serie auf dem Gerät liegt.
- Beides als reine Rechnung ohne Zugriff auf Datenbank oder Dateien —
  bei einer Regel, die Filme löscht, ist Prüfbarkeit keine Formsache.
  13 Tests.

Qualitätswahl beim Download
- transcodedDownloadURL über den Universal-Transcoder mit protocol=http
  statt über die umständliche Sync-API. NOCH NICHT AM SERVER GEPRÜFT:
  offen ist, ob Plex eine Content-Length mitschickt (sonst kein
  Fortschritt und kein Fortsetzen) und ob die Datei durchsuchbar ist.
  Steht so im Quelltext; Original bleibt Voreinstellung.

Erscheinungsbild
- Akzentfarbe: zehn kuratierte Kacheln plus freie Hex-Eingabe. Bei einer
  eigenen Farbe greift die Kontrastkorrektur und sagt es auch — sonst
  wirkt sie wie ein Fehler.
- Vorschaufläche mit Knopf, Symbol und Fortschrittsbalken.

Serveraktionen
- Gesehen-Status umschalten und Sterne setzen über ein Menü auf der
  Detailseite.
- Suchlauf der Mediathek aus der Werkzeugleiste.

115 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:40:10 +02:00

87 lines
3.4 KiB
Swift

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)
}