Jeder Aufruf einer Mediathek war ein Serverabruf mit Ladekreis davor. Die Plakate lagen längst auf der Platte; was fehlte, war die Liste, die sagt, welche davon hierhergehören. Jetzt liegt die erste Seite als Abzug daneben: beim Aufrufen steht sie sofort da, der Server wird still nachgefragt, und man sieht in aller Regel nichts davon. Antwortet er nicht, bleibt der Abzug stehen — eine Liste von gestern ist im Zug brauchbarer als ein Fehlerbildschirm. Bewusst nur die erste Seite. Stünden 240 gespeicherte Titel da und der Server antwortet mit 60, müsste entweder die Liste vor den Augen schrumpfen oder der Versatz fürs Nachladen wäre falsch. Gespeichert wird ausserdem nur, was eine Kachel zeigt. Ein Titel aus einer Listenabfrage bringt jede Spur jeder Datei mit — im Raster steht davon nichts, und ausgepackt werden müsste es bei jedem Aufrufen, also genau da, wo der Abzug Zeit sparen soll. Die Mediathekenliste selbst wird nur als Rettungsanker gemerkt, nicht als Beschleuniger: sie sofort zu zeigen hiesse, eine Sitzung für fertig zu erklären, bevor feststeht, dass der Token noch gilt. Abmelden räumt alles weg. Titel, Zusammenfassungen und Sehfortschritt sind persönlich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
233 lines
9.3 KiB
Swift
233 lines
9.3 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import MediaKit
|
|
|
|
@Suite("Gespeicherte Mediatheken")
|
|
struct LibraryCacheTests {
|
|
|
|
/// Jeder Test bekommt ein eigenes Verzeichnis. Ein gemeinsames hiesse,
|
|
/// dass die Reihenfolge der Tests über ihr Ergebnis mitentscheidet.
|
|
private func frischerSpeicher() -> LibraryCache {
|
|
let ort = URL.temporaryDirectory.appending(path: "flixy-cache-\(UUID().uuidString)")
|
|
return LibraryCache(root: ort)
|
|
}
|
|
|
|
private func titel(_ id: String, _ name: String) -> MediaItem {
|
|
MediaItem(id: id, kind: .movie, title: name, year: 1999, thumb: "/bild/\(id)")
|
|
}
|
|
|
|
@Test("Was hineingeht, kommt heraus")
|
|
func roundTrip() async {
|
|
let speicher = frischerSpeicher()
|
|
let abzug = LibraryCache.Snapshot(
|
|
queryKey: "titleAscending||||||",
|
|
items: [titel("1", "Matrix"), titel("2", "Fight Club")],
|
|
totalCount: 412
|
|
)
|
|
|
|
await speicher.store(abzug, server: "jellyfin:daheim", library: "filme")
|
|
let zurück = await speicher.snapshot(
|
|
server: "jellyfin:daheim", library: "filme", queryKey: "titleAscending||||||"
|
|
)
|
|
|
|
#expect(zurück?.items.map(\.title) == ["Matrix", "Fight Club"])
|
|
// Ohne die Gesamtzahl wüsste die Ansicht nicht, ob noch etwas kommt.
|
|
#expect(zurück?.totalCount == 412)
|
|
#expect(zurück?.items.first?.thumb == "/bild/1")
|
|
}
|
|
|
|
/// Der Fall, der stillschweigend falsche Listen zeigen würde: derselbe
|
|
/// Ort, andere Sortierung.
|
|
@Test("Ein Abzug zu einer anderen Abfrage gilt nicht")
|
|
func wrongQueryIsNoAnswer() async {
|
|
let speicher = frischerSpeicher()
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "titleAscending||||||", items: [titel("1", "A")], totalCount: 1),
|
|
server: "s", library: "l"
|
|
)
|
|
|
|
#expect(await speicher.snapshot(server: "s", library: "l", queryKey: "recentlyAdded||||||") == nil)
|
|
}
|
|
|
|
@Test("Ohne Abzug kommt nichts")
|
|
func emptyIsEmpty() async {
|
|
#expect(await frischerSpeicher().snapshot(server: "s", library: "l", queryKey: "x") == nil)
|
|
}
|
|
|
|
@Test("Zwei Mediatheken kommen sich nicht ins Gehege")
|
|
func librariesAreSeparate() async {
|
|
let speicher = frischerSpeicher()
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("1", "Film")], totalCount: 1),
|
|
server: "s", library: "filme"
|
|
)
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("2", "Serie")], totalCount: 1),
|
|
server: "s", library: "serien"
|
|
)
|
|
|
|
#expect(await speicher.snapshot(server: "s", library: "filme", queryKey: "k")?.items.first?.title == "Film")
|
|
#expect(await speicher.snapshot(server: "s", library: "serien", queryKey: "k")?.items.first?.title == "Serie")
|
|
}
|
|
|
|
/// Dieselbe Mediathek auf zwei Servern hat dieselbe Kennung. Ohne Trennung
|
|
/// nach Server zeigte der eine die Titel des anderen.
|
|
@Test("Zwei Server kommen sich nicht ins Gehege")
|
|
func serversAreSeparate() async {
|
|
let speicher = frischerSpeicher()
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("1", "Daheim")], totalCount: 1),
|
|
server: "jellyfin:daheim", library: "1"
|
|
)
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("1", "Fremd")], totalCount: 1),
|
|
server: "jellyfin:fremd", library: "1"
|
|
)
|
|
|
|
#expect(await speicher.snapshot(server: "jellyfin:daheim", library: "1", queryKey: "k")?.items.first?.title == "Daheim")
|
|
}
|
|
|
|
@Test("Mehr als eine Seite wird nicht behalten")
|
|
func onlyOnePage() async {
|
|
let speicher = frischerSpeicher()
|
|
let viele = (0..<500).map { titel(String($0), "Titel \($0)") }
|
|
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: viele, totalCount: 500),
|
|
server: "s", library: "l"
|
|
)
|
|
|
|
let zurück = await speicher.snapshot(server: "s", library: "l", queryKey: "k")
|
|
#expect(zurück?.items.count == LibraryCache.pageLimit)
|
|
// Die Gesamtzahl bleibt, sonst hielte die Ansicht sechzig für alles.
|
|
#expect(zurück?.totalCount == 500)
|
|
}
|
|
|
|
/// Was eine Kachel nicht zeigt, wird nicht gespeichert — sonst wäre jeder
|
|
/// Abzug das Zwanzigfache gross und müsste beim Aufrufen ausgepackt
|
|
/// werden, also genau da, wo er Zeit sparen soll.
|
|
@Test("Gespeichert wird nur, was eine Kachel zeigt")
|
|
func storesOnlyWhatATileShows() async {
|
|
let speicher = frischerSpeicher()
|
|
let voll = MediaItem(
|
|
id: "1",
|
|
kind: .movie,
|
|
title: "Matrix",
|
|
summary: "Ein langer Text, der im Raster nirgends steht.",
|
|
duration: 120_000,
|
|
thumb: "/bild/1",
|
|
resumeMilliseconds: 60_000,
|
|
versions: [MediaVersion(id: "v1", videoCodec: "hevc", width: 3_840, height: 2_160)],
|
|
genres: ["Science-Fiction"],
|
|
cast: [MediaPerson(id: "p1", name: "Jemand")]
|
|
)
|
|
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [voll], totalCount: 1),
|
|
server: "s", library: "l"
|
|
)
|
|
let zurück = try! #require(
|
|
await speicher.snapshot(server: "s", library: "l", queryKey: "k")
|
|
).items[0]
|
|
|
|
// Was die Kachel braucht, ist da.
|
|
#expect(zurück.title == "Matrix")
|
|
#expect(zurück.thumb == "/bild/1")
|
|
#expect(zurück.watchProgress == 0.5)
|
|
// Was sie nicht braucht, ist weg.
|
|
#expect(zurück.versions.isEmpty)
|
|
#expect(zurück.cast.isEmpty)
|
|
#expect(zurück.genres.isEmpty)
|
|
#expect(zurück.summary == nil)
|
|
}
|
|
|
|
@Test("Abmelden räumt alles weg")
|
|
func signOutClears() async {
|
|
let speicher = frischerSpeicher()
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("1", "A")], totalCount: 1),
|
|
server: "s", library: "l"
|
|
)
|
|
await speicher.storeLibraries([MediaLibrary(id: "l", title: "Filme", kind: .movie)], server: "s")
|
|
|
|
await speicher.clearAll()
|
|
|
|
#expect(await speicher.snapshot(server: "s", library: "l", queryKey: "k") == nil)
|
|
#expect(await speicher.libraries(server: "s") == nil)
|
|
}
|
|
|
|
@Test("Einen Server vergessen lässt den anderen stehen")
|
|
func clearIsPerServer() async {
|
|
let speicher = frischerSpeicher()
|
|
for server in ["a", "b"] {
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("1", server)], totalCount: 1),
|
|
server: server, library: "l"
|
|
)
|
|
}
|
|
|
|
await speicher.clear(server: "a")
|
|
|
|
#expect(await speicher.snapshot(server: "a", library: "l", queryKey: "k") == nil)
|
|
#expect(await speicher.snapshot(server: "b", library: "l", queryKey: "k") != nil)
|
|
}
|
|
|
|
@Test("Die Mediathekenliste überlebt")
|
|
func libraryListRoundTrip() async {
|
|
let speicher = frischerSpeicher()
|
|
let listen = [
|
|
MediaLibrary(id: "1", title: "Filme", kind: .movie, thumb: "/a"),
|
|
MediaLibrary(id: "2", title: "Serien", kind: .show),
|
|
]
|
|
|
|
await speicher.storeLibraries(listen, server: "s")
|
|
|
|
#expect(await speicher.libraries(server: "s") == listen)
|
|
}
|
|
|
|
// MARK: - Der Schlüssel
|
|
|
|
/// **Nicht `hashValue`.** Swift streut Hashes je Programmlauf; ein damit
|
|
/// benannter Abzug wäre nach jedem Neustart unauffindbar, ohne dass
|
|
/// irgendetwas kaputt aussähe.
|
|
@Test("Derselbe Filter ergibt denselben Schlüssel")
|
|
func keyIsStable() {
|
|
let a = MediaQuery(sort: .recentlyAdded, unwatchedOnly: true, genre: "Krimi")
|
|
let b = MediaQuery(sort: .recentlyAdded, unwatchedOnly: true, genre: "Krimi")
|
|
#expect(a.cacheKey == b.cacheKey)
|
|
}
|
|
|
|
@Test("Jeder Unterschied ändert den Schlüssel")
|
|
func keySeparatesQueries() {
|
|
let grund = MediaQuery()
|
|
#expect(MediaQuery(sort: .recentlyAdded).cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(unwatchedOnly: true).cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(favouritesOnly: true).cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(genre: "Krimi").cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(year: 1999).cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(contentRating: "FSK 16").cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(resolution: "4k").cacheKey != grund.cacheKey)
|
|
#expect(MediaQuery(searchText: "Matrix").cacheKey != grund.cacheKey)
|
|
}
|
|
|
|
/// Kennungen kommen vom Server — Jellyfin-GUIDs, und in der
|
|
/// Serverkennung steckt eine Adresse mit Schrägstrichen.
|
|
@Test("Kennungen mit Schrägstrichen ergeben brauchbare Dateinamen")
|
|
func nameIsFilesystemSafe() async {
|
|
let speicher = frischerSpeicher()
|
|
let wild = "jellyfin:https://medien.beispiel.tld:8096/:a+b/c"
|
|
|
|
await speicher.store(
|
|
LibraryCache.Snapshot(queryKey: "k", items: [titel("1", "A")], totalCount: 1),
|
|
server: wild, library: "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
|
|
)
|
|
|
|
#expect(
|
|
await speicher.snapshot(
|
|
server: wild, library: "f81d4fae-7dec-11d0-a765-00a0c91e6bf6", queryKey: "k"
|
|
)?.items.count == 1
|
|
)
|
|
#expect(!LibraryCache.safeName(wild).contains("/"))
|
|
}
|
|
}
|