Files
ScarriffleandClaude Opus 5 a7cf6c5def Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine
eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt
so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner
Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging
daneben.

**Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den
deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche
Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in
Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store
der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden
frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler.

Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen
ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim
letzten Mal stattfand.

---

2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der
Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`,
`Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen
`FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und
das Image.

**Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen
Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes
Ersetzen hätte den sonst zerlegt.

`docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte
Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das
Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden
in die Irre führt.

**Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag:
`com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat
jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe.

308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS
bauen, das Image baut, und ein frischer Container läuft durch Einrichtung
und Anmeldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:38:27 +02:00

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