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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f98b995050
commit
a7cf6c5def
@@ -0,0 +1,232 @@
|
||||
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("/"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user