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>
155 lines
5.5 KiB
Swift
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)
|
|
}
|
|
}
|