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("/")) } }