Heruntergeladenes lag auf dem Gerät und war nirgends zu sehen

Downloads gab es nur im Downloads-Bereich. Wer wissen wollte, was im
Flugzeug läuft, musste zwei Listen vergleichen: die Mediathek und die
Downloads.

Jetzt sind sie eine Reihe auf der Startseite, tragen in jeder Kachel
eine Marke, und in der Mediathek gibt es „Nur Heruntergeladene" — der
Filter fragt den Server gar nicht erst, weil der nicht weiss, was auf
diesem Gerät liegt.

Dabei kam heraus, dass Downloads nie ein Bild mitbrachten: der
Dateiname dafür stand im Modell, gesetzt hat ihn niemand. Neue holen es
mit, alten wird es beim nächsten Start nachgereicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-12 19:28:45 +02:00
co-authored by Claude Opus 5
parent fb8ad1c831
commit 77593105de
11 changed files with 335 additions and 14 deletions
@@ -422,6 +422,40 @@ public final class DownloadCoordinator {
return fileStore.url(forFileName: fileName)
}
/// Das lokale Plakat eines Downloads — `nil`, wenn keines geholt wurde.
///
/// **Weil eine Reihe ohne Bilder keine Reihe ist.** Ohne Verbindung gibt
/// es keine Adresse, von der ein Plakat nachzuladen wäre; was nicht beim
/// Herunterladen mitkam, bleibt eine graue Fläche.
public func artworkURL(forRatingKey key: String) -> URL? {
guard let name = items.first(where: { $0.ratingKey == key })?.artworkFileName,
fileStore.exists(fileName: name)
else { return nil }
return fileStore.url(forFileName: name)
}
/// Holt das Plakat zu einem Eintrag nach und legt es ab.
///
/// **Beim Anlegen, nicht erst beim Fertigwerden.** Die Liste zeigt den
/// Eintrag, sobald er in der Warteschlange steht; bis dahin wäre sonst
/// nichts zu sehen. Es sind ein paar Kilobyte.
public func fetchArtwork(for ratingKey: String, from url: URL) async {
guard let item = items.first(where: { $0.ratingKey == ratingKey }),
item.artworkFileName == nil
else { return }
guard let (daten, antwort) = try? await URLSession.shared.data(from: url),
(antwort as? HTTPURLResponse).map({ (200..<300).contains($0.statusCode) }) ?? true,
!daten.isEmpty
else { return }
let name = fileStore.artworkFileName(forRatingKey: ratingKey)
guard fileStore.store(daten, as: name) != nil else { return }
item.artworkFileName = name
save()
}
public func offlineMedia(forRatingKey key: String) -> OfflineMedia? {
guard let data = items.first(where: { $0.ratingKey == key })?.offlineMediaData else { return nil }
return try? JSONDecoder().decode(OfflineMedia.self, from: data)
@@ -58,6 +58,29 @@ public struct DownloadFileStore: Sendable {
return "\(key).\(ext)"
}
/// Der Name des Plakats zu einem Titel.
///
/// **Eine eigene Datei, kein Anhängsel am Film.** Sie ist ein paar
/// Kilobyte gross und wird gebraucht, solange der Eintrag steht — auch
/// während der Film noch lädt und noch gar keine Datei hat.
public func artworkFileName(forRatingKey key: String) -> String { "\(key).bild" }
/// Legt ein geholtes Plakat ab. Schlägt es fehl, gibt es eben keines.
///
/// **Wirft nicht.** Ein Download ohne Bild ist ein Download; ein
/// Download, der am Bild scheitert, wäre keiner.
@discardableResult
public func store(_ data: Data, as name: String) -> URL? {
let ziel = url(forFileName: name)
do {
try createRootIfNeeded()
try data.write(to: ziel, options: .atomic)
return ziel
} catch {
return nil
}
}
/// Verschiebt die fertige Datei aus dem temporären Ordner an ihren Platz.
///
/// Verschieben, nicht kopieren: iOS löscht die temporäre Datei, sobald der