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:
co-authored by
Claude Opus 5
parent
c05004f586
commit
a44d55e71c
@@ -106,8 +106,18 @@ struct PlayerView: View {
|
||||
// Jetzt rechnet das Layout. Der schwarze Rand ist schlicht
|
||||
// der Hintergrund dahinter, und wer ihn anklickt, klickt
|
||||
// nicht aufs Bild — das muss niemand mehr feststellen.
|
||||
// **Im Vollbild über alles, sonst nicht unter die
|
||||
// Insel.**
|
||||
//
|
||||
// Bei 21∶9 auf einem Telefon reicht das Bild über die
|
||||
// ganze Breite — und damit hinter die Dynamic Island, die
|
||||
// links einen Streifen davon verdeckt. Wer Vollbild
|
||||
// wählt, hat sich fürs Beschneiden entschieden; dort ist
|
||||
// Rand an Rand folgerichtig. Im normalen Modus ist der
|
||||
// Anspruch gerade, dass nichts fehlt — dann darf auch
|
||||
// nichts verdeckt sein. Plex macht es genauso.
|
||||
Color.clear
|
||||
.ignoresSafeArea()
|
||||
.modifier(Randlos(an: model.videoFit == .fill))
|
||||
.overlay {
|
||||
surface(for: model)
|
||||
.contentShape(.rect)
|
||||
@@ -1049,3 +1059,16 @@ extension Duration {
|
||||
return String(format: "%d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lässt eine Fläche über die Sicherheitsränder hinausreichen — oder nicht.
|
||||
///
|
||||
/// **Ein eigener Modifikator, weil `ignoresSafeArea` keinen Schalter hat.**
|
||||
/// Ein `if` im Baum wäre zwei verschiedene Ansichten, und SwiftUI baut die
|
||||
/// beim Umschalten neu auf — die Wiedergabe begänne von vorn.
|
||||
private struct Randlos: ViewModifier {
|
||||
let an: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if an { content.ignoresSafeArea() } else { content }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
{
|
||||
"sourceLanguage": "en",
|
||||
"strings": {
|
||||
"downloads.failed.notReady": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Der Server packt diese Datei noch um. Versuch es in ein paar Minuten noch einmal."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The server is still repackaging this file. Try again in a few minutes."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads.failed.status %lld": {
|
||||
"comment": "Der Server hat die Download-Anfrage abgelehnt",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Der Server hat den Download mit Fehler %lld abgelehnt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The server refused the download with error %lld."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads.failed.tooSmall %@": {
|
||||
"comment": "Die heruntergeladene Datei ist zu klein, um ein Video zu sein",
|
||||
"localizations": {
|
||||
@@ -34,23 +68,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads.failed.status %lld": {
|
||||
"comment": "Der Server hat die Download-Anfrage abgelehnt",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Der Server hat den Download mit Fehler %lld abgelehnt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The server refused the download with error %lld."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "1.0"
|
||||
|
||||
Reference in New Issue
Block a user