163 Byte als Film verbucht, in einer Sekunde heruntergeladen

Muss der Server eine Datei umpacken, antwortet er mit **202** und einem
kurzen Text: noch nicht, komm wieder. Die Prüfung im Download verwarf
alles ausserhalb von 200 bis 299 — und 202 liegt mittendrin. Der
Antworttext lag danach als Film auf dem Gerät.

Jetzt ist 202 ein eigener Fall: der Eintrag geht zurück in die
Warteschlange und fragt alle fünfzehn Sekunden erneut, bis zu fünf
Minuten lang. Es ist nichts schiefgegangen, es ist nur noch nicht fertig
— und im Sekundentakt nachzufragen hiesse, dieselbe Datei hundertmal
anzufangen und wegzuwerfen.

Gezählt wird im Arbeitsspeicher, nicht im Eintrag: nach einem Neustart
beginnt das Zählen von vorn, und das ist richtig — der Server hat
inzwischen vermutlich fertig gepackt.

Dazu, aus derselben Meldung: bei 21∶9 auf einem Telefon reichte das Bild
hinter die Dynamic Island. Im Vollbild bleibt das so — wer es wählt, hat
sich fürs Beschneiden entschieden. Im normalen Modus nicht mehr: dort ist
der Anspruch gerade, dass nichts fehlt, und dann darf auch nichts
verdeckt sein.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-11 20:33:32 +02:00
co-authored by Claude Opus 5
parent c05004f586
commit a44d55e71c
3 changed files with 120 additions and 18 deletions
@@ -458,6 +458,13 @@ public final class DownloadCoordinator {
}
}
/// Wie oft je Eintrag schon auf ein Umpacken gewartet wurde.
///
/// **Im Arbeitsspeicher und nicht im Eintrag.** Nach einem Neustart
/// beginnt das Zählen von vorn — und das ist richtig: der Server hat
/// inzwischen vermutlich fertig gepackt.
private var wartend: [String: Int] = [:]
private func start(_ item: DownloadItem) {
guard let url = URL(string: item.sourceURLString) else {
fail(item, message: "Ungültige Adresse")
@@ -526,6 +533,7 @@ public final class DownloadCoordinator {
}
func handleCompletion(ratingKey: String, temporaryFile: URL) {
wartend[ratingKey] = nil
guard let item = items.first(where: { $0.ratingKey == ratingKey }),
let sourceURL = URL(string: item.sourceURLString)
else { return }
@@ -608,6 +616,45 @@ public final class DownloadCoordinator {
}
/// Der Server hat die Anfrage abgelehnt.
/// Wie oft auf ein Umpacken gewartet wird, bevor aufgegeben wird.
///
/// **Grosszügig, und das mit Absicht.** Ein Dreistünder in 4K braucht auf
/// einem NAS Minuten; abzubrechen, während die Arbeit läuft, wäre die
/// eine Antwort, die niemandem hilft. Zwanzig Versuche im Abstand von
/// fünfzehn Sekunden sind fünf Minuten.
private static let hoechstensWarten = 20
private static let wartezeit = Duration.seconds(15)
/// Der Server packt noch um — später noch einmal fragen.
///
/// **Nicht sofort.** Ein Umpacken dauert Minuten, und im Sekundentakt
/// nachzufragen hiesse, dieselbe Datei hundertmal anzufangen und wieder
/// wegzuwerfen.
func handleNotReady(ratingKey: String) {
guard let item = items.first(where: { $0.ratingKey == ratingKey }) else { return }
let versuche = (wartend[ratingKey] ?? 0) + 1
wartend[ratingKey] = versuche
guard versuche <= Self.hoechstensWarten else {
wartend[ratingKey] = nil
fail(item, message: String(localized: "downloads.failed.notReady", bundle: .module))
Task { await startNextIfPossible() }
return
}
// **Zurück in die Warteschlange, nicht in den Fehler.** Es ist
// nichts schiefgegangen; es ist nur noch nicht fertig.
item.state = .waiting
save()
Task { [weak self] in
try? await Task.sleep(for: Self.wartezeit)
guard let self else { return }
await self.resume(item)
}
}
func handleServerRejection(ratingKey: String, status: Int) {
guard let item = items.first(where: { $0.ratingKey == ratingKey }) else { return }
fail(item, message: String(localized: "downloads.failed.status \(status)", bundle: .module))
@@ -740,6 +787,21 @@ final class DownloadSessionDelegate: NSObject, URLSessionDownloadDelegate, @unch
) {
guard let key = downloadTask.taskDescription else { return }
// **202 heisst: noch nicht, komm wieder.**
//
// Der Server packt eine Datei um, die so nicht ausgeliefert werden
// kann, und antwortet währenddessen mit 202 und einem kurzen Text.
// Das ist ein Erfolgscode, also ging er durch die Prüfung darunter
// glatt hindurch: 163 Byte Antworttext lagen danach als Film auf dem
// Gerät, „in einer Sekunde heruntergeladen".
if let response = downloadTask.response as? HTTPURLResponse,
response.statusCode == 202 {
Task { @MainActor [weak coordinator] in
coordinator?.handleNotReady(ratingKey: key)
}
return
}
// `URLSessionDownloadTask` gilt auch bei 404 oder 500 als erfolgreich
// und legt die Fehlerseite als Datei ab. Ohne diese Prüfung wurde eine
// zwei Kilobyte große HTML-Seite als fertiger Film verbucht — in einer