Mediatheken stehen sofort da statt sich jedes Mal neu zu laden

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>
This commit is contained in:
Scarriffle
2026-09-28 15:59:15 +02:00
co-authored by Claude Opus 5
parent 18958aadbc
commit 2dbe260901
10 changed files with 573 additions and 24 deletions
+44 -10
View File
@@ -293,9 +293,9 @@ final class FlixySession {
let server = PlexServer(resource: resource, connection: connection, identity: identity)
else { return false }
adopt(server: server)
let plex = adopt(server: server)
do {
libraries = try await server.libraries().filter(\.isSupported).map(\.asMediaLibrary)
libraries = try await loadLibraries(from: plex)
phase = .ready
return true
} catch {
@@ -416,6 +416,10 @@ final class FlixySession {
forgetJellyfin()
defaults.removeObject(forKey: Key.serverKind)
clearActiveServer()
// Titel, Zusammenfassungen und Sehfortschritt sind persönlich. Was
// einem abgemeldeten Konto gehörte, hat auf der Platte nichts mehr
// verloren.
Task { await LibraryCache.shared.clearAll() }
phase = .signedOut
}
@@ -471,11 +475,11 @@ final class FlixySession {
return
}
adopt(server: server)
let plex = adopt(server: server)
remember(resource: resource, connection: connection)
do {
libraries = try await server.libraries().filter(\.isSupported).map(\.asMediaLibrary)
libraries = try await loadLibraries(from: plex)
phase = .ready
} catch {
phase = .failed(message: ErrorText.describe(error))
@@ -541,7 +545,7 @@ final class FlixySession {
self.backend = backend
do {
libraries = try await backend.libraries().filter(\.isSupported)
libraries = try await loadLibraries(from: backend)
phase = .ready
} catch {
phase = .failed(message: ErrorText.describe(error))
@@ -565,7 +569,7 @@ final class FlixySession {
isLocalConnection = await backend.isInLocalNetwork()
do {
libraries = try await backend.libraries().filter(\.isSupported)
libraries = try await loadLibraries(from: backend)
} catch {
self.backend = nil
jellyfinAccount = nil
@@ -613,7 +617,7 @@ final class FlixySession {
isLocalConnection = await backend.isInLocalNetwork()
do {
libraries = try await backend.libraries().filter(\.isSupported)
libraries = try await loadLibraries(from: backend)
phase = .ready
return true
} catch MediaBackendError.unauthorized {
@@ -648,12 +652,42 @@ final class FlixySession {
// MARK: - Der aktive Server
/// Setzt Plex als aktiven Server.
private func adopt(server: PlexServer) {
/// Setzt Plex als aktiven Server und gibt die Hülle zurück.
///
/// Zurückgegeben, damit der Aufrufer sie benutzen kann, ohne sich ein
/// zweites Mal eine zu bauen — die trüge eine eigene Live-TV-Sitzung mit
/// sich, und dann wären zwei im Umlauf, die sich nichts voneinander
/// erzählen.
@discardableResult
private func adopt(server: PlexServer) -> PlexBackend {
let hülle = PlexBackend(server: server)
kind = .plex
self.server = server
backend = PlexBackend(server: server)
backend = hülle
jellyfinAccount = nil
return hülle
}
/// Holt die Mediathekenliste und merkt sie sich.
///
/// **Der Speicher ist hier nur ein Rettungsanker**, kein Beschleuniger:
/// die Liste ist eine einzige kleine Anfrage, und sie sofort zu zeigen
/// hiesse, eine Sitzung für fertig zu erklären, bevor feststeht, dass der
/// Token noch gilt. Antwortet der Server dagegen **nicht**, ist eine
/// Seitenleiste von gestern besser als eine leere — im Zug ist das der
/// Unterschied zwischen „das Heruntergeladene ansehen" und „gar nichts".
private func loadLibraries(from backend: any MediaBackend) async throws -> [MediaLibrary] {
let kennung = backend.identity
do {
let geladen = try await backend.libraries().filter(\.isSupported)
await LibraryCache.shared.storeLibraries(geladen, server: kennung)
return geladen
} catch {
guard let gemerkt = await LibraryCache.shared.libraries(server: kennung),
!gemerkt.isEmpty
else { throw error }
return gemerkt
}
}
private func clearActiveServer() {
+50 -2
View File
@@ -66,7 +66,14 @@ struct LibraryView: View {
.sheet(isPresented: $showsFilters) {
LibraryFilterSheet(query: $query, libraryKind: library.kind)
}
.task(id: library.id) { await reload() }
.task(id: library.id) {
// Beim Wechsel auf eine andere Mediathek zuerst leeren: sonst hält
// `reload()` die Titel der vorigen für die eigenen und fragt den
// Speicher gar nicht erst.
items = []
page = nil
await reload()
}
// Jede Änderung an Filter oder Sortierung verwirft die bisherige
// Liste — Nachladen mit altem Versatz würde sonst Einträge aus zwei
// verschiedenen Abfragen vermischen.
@@ -241,19 +248,60 @@ struct LibraryView: View {
// MARK: - Laden
/// Lädt die erste Seite — erst aus dem Speicher, dann vom Server.
///
/// **Der gespeicherte Stand zuerst, ohne Ladekreis.** Die Plakate liegen
/// ohnehin schon auf der Platte; was fehlte, war die Liste, die sagt,
/// welche davon hierhergehören. Mit ihr steht die Mediathek sofort da.
///
/// **Dann der Server, stillschweigend.** Kein zweiter Ladekreis über einer
/// Liste, die schon zu sehen ist — die Titel werden ersetzt, sobald die
/// Antwort da ist, und in aller Regel sieht man davon nichts, weil sich
/// nichts geändert hat.
///
/// **Und wenn der Server nicht antwortet**, bleibt der gespeicherte Stand
/// stehen. Eine Liste von gestern ist im Zug brauchbarer als ein
/// Fehlerbildschirm.
private func reload() async {
guard let server = session.backend else { return }
state = .loading
let schlüssel = query.cacheKey
let kennung = server.identity
// Nur wenn nichts dasteht. Beim Herunterziehen und beim Filterwechsel
// ist das Gezeigte aktueller als jeder Abzug.
if items.isEmpty,
let stand = await LibraryCache.shared.snapshot(
server: kennung, library: library.id, queryKey: schlüssel
) {
items = stand.items
page = MediaPage(items: stand.items, totalCount: stand.totalCount)
state = .loaded(())
} else if items.isEmpty {
state = .loading
}
do {
let first = try await server.items(inLibrary: library, query: query, offset: 0, limit: 60)
items = first.items
page = first
loadMoreFailed = false
state = .loaded(())
await LibraryCache.shared.store(
LibraryCache.Snapshot(
queryKey: schlüssel, items: first.items, totalCount: first.totalCount
),
server: kennung,
library: library.id
)
} catch {
// Siehe `LoadState.load`: eine abgebrochene Netzanfrage meldet
// sich als Übertragungsfehler, nicht als Abbruch.
if Task.isCancelled { return }
// Steht schon etwas da — aus dem Speicher oder von vorhin —, dann
// bleibt es. Den Fehler über eine brauchbare Liste zu legen
// nähme dem Nutzer etwas weg, statt ihm etwas zu sagen.
guard items.isEmpty else { return }
state = .failed(message: ErrorText.describe(error))
}
}