Files
Flaro/Packages/FlixyKit/Tests/FlixyDownloadsTests/DownloadRulesTests.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

155 lines
5.5 KiB
Swift

import Foundation
import PlexKit
import Testing
@testable import FlixyDownloads
/// Baut einen Eintrag im gewünschten Zustand, ohne Datenbank.
@MainActor
private func item(
_ key: String,
state: DownloadState = .completed,
watchedAgo: Duration? = nil,
series: String? = nil
) -> DownloadItem {
let item = DownloadItem(
ratingKey: key,
title: "Titel \(key)",
sourceURLString: "http://server/\(key)",
state: state,
seriesKey: series
)
if let watchedAgo {
item.watchedAt = Date().addingTimeInterval(-watchedAgo.seconds)
}
return item
}
@MainActor
@Suite("Download-Regeln")
struct DownloadRulesTests {
// MARK: - Automatisches Löschen
@Test("Ohne Regel wird nichts gelöscht")
func disabledRulesDeleteNothing() {
// Die wichtigste Zusicherung im ganzen Regelwerk: wer nichts
// eingestellt hat, verliert nichts.
let items = [item("1", watchedAgo: .afterOneWeek)]
#expect(DownloadRules.disabled.itemsToDelete(from: items).isEmpty)
}
@Test("Ungesehene Titel werden nie gelöscht")
func unwatchedIsNeverDeleted() {
let rules = DownloadRules(deleteWatchedAfter: .immediately)
let items = [item("1"), item("2"), item("3")]
#expect(rules.itemsToDelete(from: items).isEmpty)
}
@Test("Nach Ablauf der Frist wird gelöscht")
func deletesAfterGracePeriod() {
let rules = DownloadRules(deleteWatchedAfter: .afterOneDay)
let items = [
item("alt", watchedAgo: .afterOneWeek),
item("frisch", watchedAgo: .seconds(60)),
]
let doomed = rules.itemsToDelete(from: items).map(\.ratingKey)
#expect(doomed == ["alt"])
}
@Test("Die Frist schützt frisch Gesehenes")
func gracePeriodProtectsRecent() {
// Wer eine Folge zu Ende sieht und gleich noch einmal die letzte
// Szene sucht, soll sie nicht schon verloren haben.
let rules = DownloadRules(deleteWatchedAfter: .afterOneDay)
let items = [item("1", watchedAgo: .seconds(3_600))]
#expect(rules.itemsToDelete(from: items).isEmpty)
}
@Test("Unfertige Downloads werden nicht gelöscht")
func incompleteDownloadsSurvive() {
// Ein laufender Download hat nichts, was gesehen worden sein könnte —
// ein alter watchedAt-Wert aus einem früheren Durchlauf darf ihn
// nicht abräumen.
let rules = DownloadRules(deleteWatchedAfter: .immediately)
let running = item("1", state: .downloading, watchedAgo: .afterOneWeek)
#expect(rules.itemsToDelete(from: [running]).isEmpty)
}
// MARK: - Vorrat an Folgen
@Test("Ohne Regel wird nichts nachgeladen")
func noRefillWithoutRule() {
#expect(DownloadRules.disabled.seriesNeedingRefill(in: [item("1", series: "s")]).isEmpty)
}
@Test("Ein leerer Vorrat verlangt die volle Zahl")
func emptyStockNeedsFullCount() {
let rules = DownloadRules(keepUnwatchedEpisodes: 3)
#expect(rules.missingEpisodeCount(forSeries: "s", in: []) == 3)
}
@Test("Gesehene Folgen zählen nicht zum Vorrat")
func watchedEpisodesDoNotCount() {
// Sonst bliebe der Vorrat bei drei gesehenen Folgen stehen und es käme
// nie etwas Neues nach — genau das Gegenteil des Gewünschten.
let rules = DownloadRules(keepUnwatchedEpisodes: 3)
let items = [
item("1", watchedAgo: .seconds(10), series: "s"),
item("2", watchedAgo: .seconds(10), series: "s"),
item("3", series: "s"),
]
#expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 2)
}
@Test("Wartende Downloads zählen zum Vorrat")
func queuedEpisodesCount() {
// Sonst würde bei jedem Durchlauf erneut nachgelegt, bis die ganze
// Serie auf dem Gerät liegt.
let rules = DownloadRules(keepUnwatchedEpisodes: 3)
let items = [
item("1", state: .completed, series: "s"),
item("2", state: .queued, series: "s"),
item("3", state: .downloading, series: "s"),
]
#expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 0)
}
@Test("Gescheiterte Downloads zählen nicht zum Vorrat")
func failedEpisodesDoNotCount() {
let rules = DownloadRules(keepUnwatchedEpisodes: 2)
let items = [
item("1", state: .completed, series: "s"),
item("2", state: .failed, series: "s"),
]
#expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 1)
}
@Test("Ein voller Vorrat verlangt nichts")
func fullStockNeedsNothing() {
let rules = DownloadRules(keepUnwatchedEpisodes: 2)
let items = [item("1", series: "s"), item("2", series: "s"), item("3", series: "s")]
#expect(rules.missingEpisodeCount(forSeries: "s", in: items) == 0)
}
@Test("Serien werden getrennt gezählt")
func seriesAreCountedSeparately() {
let rules = DownloadRules(keepUnwatchedEpisodes: 2)
let items = [
item("a1", series: "a"),
item("a2", series: "a"),
item("b1", series: "b"),
]
let refill = rules.seriesNeedingRefill(in: items)
#expect(refill["a"] == nil)
#expect(refill["b"] == 1)
}
@Test("Filme ohne Serie lösen kein Nachladen aus")
func moviesDoNotTriggerRefill() {
let rules = DownloadRules(keepUnwatchedEpisodes: 3)
#expect(rules.seriesNeedingRefill(in: [item("1"), item("2")]).isEmpty)
}
}