Commit Graph
11 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 93695dc332 Beim Umpacken wird nach der Tonspur gefragt, nicht geraten
Mein Fehler, und ein vermeidbarer. Beim Umpacken legt der Server genau eine
Tonspur in die Datei, und welche, hat bisher allein die Abwägung entschieden
— bei einer Aufnahme mit Audiodeskription die falsche. Nach dem Herunterladen
gibt es nichts mehr zu korrigieren; drei Gigabyte Datenvolumen waren weg.

Wo eine Spur verlorengeht, wird jetzt gefragt. Nur dort: bei einer Datei, die
der Systemplayer ohnehin öffnet, kommt alles mit, und eine Frage ohne Folgen
ist nur ein Klick mehr.

**Der Vorschlag kommt aus dem, was schon bekannt ist.** Die gemerkte Sprache
der Serie zuerst, dann die Serieneinstellung, dann die allgemeine — dieselbe
Abwägung wie beim Abspielen (`AudioTrackChooser`), über denselben Weg. Sie
ist jetzt Vorschlag statt Entscheidung und steht als „· empfohlen" dabei.

Beim Stapel aus der Mehrfachauswahl wird einmal gefragt, nicht je Folge: die
Folgen einer Staffel sind gleich aufgebaut, und zwanzigmal dieselbe Frage
wäre keine Wahl, sondern eine Prüfung der Geduld.

**Was am Server gemessen wurde, und warum es nicht besser geht:**

- `static=true` liefert die Datei unverändert — alle Spuren, aber Matroska;
  die Magie am Dateianfang ist `1a45dfa3`, auch wenn im Pfad `.mp4` steht.
- `stream.mp4` bildet genau eine Tonspur ab.
- Die HLS-Liste des Servers führt **eine** Variante und **keine**
  `EXT-X-MEDIA`-Tonspuren.

Mehrere Tonspuren offline gibt dieser Server auf keinem seiner Wege her. Das
liegt nicht an MP4 — das kann mehrere — und nicht an AVFoundation, sondern am
Transcoder, der je Sitzung eine Spur abbildet.

iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 10:14:14 +02:00
ScarriffleandClaude Opus 5 a432592271 Beim Umpacken fällt die Spurwahl an — also wird sie dort getroffen
Dein Befund an „Notruf Hafenkante": keine Spuren, nichts auswählbar. Die
technischen Angaben derselben Folge sagen, warum:

    Container    MKV
    Tonspur      AAC - Stereo - Standard
    Tonspur      Deutsch - German - AAC - Stereo - Standard
    Untertitel   German - Standard - SUBRIP

Die Quelle ist Matroska. AVFoundation öffnet das nicht, also lief der
Download bisher über die zweite Engine — und die meldete die Spurliste leer.
Offline wählen ging damit nie.

**Umpacken allein hätte es nicht gelöst.** Jellyfin bildet in die neue Hülle
genau **eine** Tonspur ab und übernimmt den Untertitel nur, wenn man ihn
nennt. Ohne Angabe nimmt der Server seine eigene Vorgabe — und das ist bei
deutschen Aufnahmen regelmäßig die Spur mit Audiodeskription.

Also fällt die Wahl jetzt beim Herunterladen, mit derselben Abwägung wie beim
Abspielen (`AudioTrackChooser`, der für genau diesen Fall gebaut wurde: die
als deutsch ausgezeichnete Spur ist die mit Audiodeskription). Der Untertitel
geht als `subtitleMethod=Embed` mit — als **Spur**, nicht ins Bild gebrannt,
sonst liesse er sich nicht abschalten.

Die Mediathek bleibt bei der Abwägung aussen vor: sie nachzuschlagen kostet
eine Anfrage je Folge, und Serie und allgemeine Einstellung tragen den Wunsch
ohnehin.

Der Offline-Abzug behauptet nicht mehr, alle Sprachen seien dabei — beim
Umpacken bleibt je eine übrig, und alle zu notieren wäre ein Versprechen, das
die Datei nicht hält.

**Nebenbei richtiggestellt:** die technischen Angaben rechneten mit
`.directPlayOnly` und zeigten deshalb „Direkt (Systemplayer)" direkt über
„iOS öffnet den Container MKV nicht" — zwei Angaben, die sich widersprechen.
Das war früher richtig (ohne Serverhilfe übernahm die zweite Engine und hiess
auch so); jetzt wird mit Serverhilfe gerechnet.

NOCH NICHT AN EINEM ECHTEN SERVER GEPRÜFT.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:32:41 +02:00
ScarriffleandClaude Opus 5 ef2596e2bb Was heruntergeladen wird, muss der Systemplayer öffnen können
**Vorbereitung dafür, dass VLC verschwindet.** Online ist der Fall längst
gelöst: steht nur die Schachtel im Weg, packt der Server um — dieselben
Ströme, eine MP4-Hülle. Offline gibt es diesen Ausweg nicht. Was einmal als
Matroska auf dem Gerät liegt, kann hinterher niemand mehr umpacken, und ohne
zweite Engine bliebe die Datei schwarz.

Also entscheidet sich das beim Herunterladen. Fragt jemand das Original an und
AVFoundation könnte die Datei nicht öffnen, wird `remuxDownloadURL` benutzt:
`/Videos/{id}/stream.mp4` mit `allowVideoStreamCopy` und
`allowAudioStreamCopy`. Der Server reicht durch, was passt, und rechnet nur
um, was nicht passt — das Bild bleibt das Original, nur DTS oder TrueHD
werden zu AC3 oder AAC.

Drei Dinge hängen daran und sind mitgezogen:

- Die **Größe** ist dann keine Zusage mehr: dieselben Ströme in einer anderen
  Hülle wiegen anders. `expectedIsExact` fällt auf falsch, und es gilt wieder
  die weiche Prüfung.
- Der **Container** im Offline-Abzug ist „mp4", nicht die alte Schachtel.
  Sonst entschiede der Player anhand einer Datei, die es so nicht mehr gibt.
- Der **Tonkodierer** bleibt leer. Ob aus DTS dann AAC oder AC3 wird, sagt der
  Server vorher nicht; eine Behauptung wäre schlechter als die Lücke, und die
  Spuren liest der Player ohnehin aus der Datei.

**Die Frage „kann das der Systemplayer?" steht jetzt an einer Stelle.**
`DeviceCapabilities.opens(container:videoCodec:audioCodec:)` — dieselbe Liste,
nach der auch beim Abspielen entschieden wird. Was der Server nicht nennt,
gilt als passend: vorsichtshalber umzupacken hiesse, aus einer tadellosen MP4
eine neu gerechnete Datei zu machen. Fünf Tests.

NOCH NICHT AN EINEM ECHTEN SERVER GEPRÜFT: ob Jellyfin das Kopieren hier
tatsächlich wählt, verrät erst die Dateigröße des ersten Downloads.

140 Tests in FlixyPlayback, iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 07:54:46 +02:00
ScarriffleandClaude Opus 5 261cdee2f2 Mehrere Folgen ankreuzen und in einem Zug laden
Je Folge einzeln zu tippen ist bei einer halb gesehenen Staffel die falsche
Arbeit. „Auswählen" im Kopf der Folgenliste schaltet um: jede Zeile bekommt
einen Kreis, unten erscheint „N Folgen laden".

**Der Knopf steht bei dem, was er auswählt** — in der Zeile mit „Folgen",
nicht oben in der Werkzeugleiste. Dort wäre er weiter von der Liste entfernt
als von allem anderen auf der Seite.

**Nur bei Folgen.** Eine Staffelliste zeigt ihn nicht; für ganze Staffeln
gibt es den Knopf oben im Kopf.

**Die Qualität wird einmal gefragt, nicht je Folge.** Dafür werden die
Dateiangaben **einer** Folge nachgeholt — die Folgen einer Staffel sind in
derselben Fassung kodiert, und zwanzig Anfragen für eine Frage, die einmal
gestellt wird, wären zwanzig zu viel. Geladen wird danach jede Folge
einzeln; dort zählt wieder ihre eigene Größe.

**Was zur Wahl steht, steht jetzt an einer Stelle.** Die beiden Rechnungen
— welche Stufen angeboten werden dürfen und wie sie beschriftet sind —
lagen im Knopf einer einzelnen Folge. Mit der Leiste gäbe es sie zweimal,
und die zweite Kopie wäre die mit dem Fehler. Sie sind jetzt
`DownloadQualityChoice`, ohne eine Zeile SwiftUI darin.

`DownloadService.downloadAll` und der neue Weg teilen sich dieselbe
Schleife; „alle Folgen" ist nur noch „diese Folgen" mit einer anderen Liste.

Im Simulator geprüft: Serienseite ohne Knopf, Staffelseite mit; zwei Folgen
angekreuzt → „2 Folgen laden". iOS und tvOS bauen, 489 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 21:57:53 +02:00
ScarriffleandClaude Opus 5 e86c0a6e95 Ein Download riss ab, galt als fertig — und setzte nie fort
Dein Bericht: bei rund 3 GB abgebrochen, beim Fortsetzen wieder bei null,
und später „plötzlich bei 60 Prozent fertig". Beide Hälften standen so im
Code.

**Fertig war sie nicht, sie war halb.**

    if expected > 0, Double(written) < Double(expected) * 0.5 { verwerfen }

Alles über der Hälfte galt als vollständig. Diese Regel war nie für diesen
Fall gedacht, sondern für die **hochgerechnete** Größe der Transcoder-Stufen
(Bitrate × Dauer) — dort ist Abweichung normal. Beim Original nennt der
Server die Dateigröße in den Metadaten, und da ist jedes fehlende Byte ein
Abriss.

Das unterscheidet jetzt `expectedIsExact`, gesetzt beim Planen und
mitgeführt am Eintrag. Dazu merkt sich der Koordinator, für welche Titel der
Server eine `Content-Length` geschickt hat — auch das ist eine Zusage.

**„Fortsetzen" war eine Zusage, die der Code nicht halten konnte.**
`NSURLSessionDownloadTaskResumeData` kam im ganzen Projekt nicht vor; den
Wiederaufnahme-Datensatz gab es nur beim Pausieren von Hand. Nach einem
Netzfehler war er weg, und der Knopf daneben hieß trotzdem „Fortsetzen".
Jetzt wird er aus dem Fehler gerettet, und wo es ihn nicht gibt, heißt der
Knopf „Erneut versuchen".

**Und es merkte niemand, wenn das WLAN zurückkam.** `RootView` reicht das
jetzt weiter — nur für Einträge mit Wiederaufnahme-Datensatz, damit ein 404
sich nicht im Kreis versucht.

**Rate und Restzeit** stehen in der Zeile: „12 MB / 20 MB · 3,4 MB/s ·
gleich fertig". `DownloadRate` ist ein reiner Wertetyp mit Uhr von außen —
geglättet, weil rohe Differenzen um ein Vielfaches springen, und ohne Zahl,
solange nichts gemessen ist. Neun Tests.

**Zwei Dinge, die erst die Messung gezeigt hat** — und die beide nicht in
der App liegen:

Erstens: reißt eine Übertragung ab, **die eine `Content-Length` angekündigt
hatte**, wiederholt die Hintergrundsitzung von iOS sie selbst — stumm, ohne
den Delegaten zu fragen, und **jedes Mal ab Byte null**. Gemessen: sieben
Anläufe, jeder die vollen 12 MB. Das ist genau dein „fing wieder bei null
an", und dagegen hilft kein Wiederaufnahme-Datensatz.

Zweitens: ein Fortsetzen braucht eine Prüfmarke vom Server. Ohne `ETag`
oder `Last-Modified` kann iOS sein `If-Range` nicht stellen und lädt alles
noch einmal — gemessen, 20 MB zweimal. Mit Marke kam die zweite Anfrage als
`Range: bytes=5636096-` und holte nur den Rest.

Die Attrappe kann das jetzt alles nachstellen: eine beliebig große Datei,
die auf Wunsch mittendrin abreißt, mit oder ohne angekündigte Länge, und
gedrosselt. Ohne sie ist nichts davon beweisbar — und genau daran ist der
Fehler vorbeigekommen.

Im Simulator gegen die Attrappe: 12 von 20 MB → „Fehlgeschlagen:
Übertragung unvollständig", nicht Haken. Vollständig → Haken. iOS und tvOS
bauen, 489 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 21:05:34 +02:00
ScarriffleandClaude Opus 5 5696254034 Jellyfin: der Anmeldebildschirm bietet jetzt beides an
Der Anmeldebildschirm kannte nur Plex. JellyfinKit war fertig, aber von der
App aus nicht erreichbar — „ich sehe nix von Jellyfin".

Quick Connect steht vorn: der Server nennt einen sechsstelligen Code, den man
in einem bereits angemeldeten Client bestätigt. Das Passwort erreicht Flixy
dabei nie, dieselbe Zusage wie bei Plex. Benutzername und Passwort sind der
Rückfall für Server, auf denen Quick Connect abgeschaltet ist; das Passwort
wird dann für die Dauer einer Anfrage gehalten und danach weggeworfen.

Die Adresse wird nicht erraten, sondern durchprobiert. „192.168.1.50" kann
alles Mögliche heißen, und die Reihenfolge ist der Kern von JellyfinAddress:
eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein Domänenname fast
immer. Andersherum wartet man bei jedem Server ohne Zertifikat erst auf eine
Zeitüberschreitung.

Die Sitzung hält von hier an einen Server hinter dem neutralen Vertrag, nicht
mehr einen Plex-Server. Die Ansichten haben das nicht gemerkt — genau dafür
war MediaKit da. Dabei fällt ab, was Jellyfin kann und Plex nicht: Favoriten
stehen im Aktionsmenü, sobald der Server sie kennt, Sterne nur dort, wo es
sie gibt.

Was noch nicht geht und deshalb auch nicht behauptet wird: Wiedergabe und
Live-TV sprechen weiter unmittelbar mit Plex. Bei Jellyfin verschwindet der
Live-TV-Eintrag aus der Seitenleiste, und der Player sagt, dass die
Wiedergabe der nächste Schritt ist, statt „Server nicht erreichbar" zu
melden — er ist ja erreichbar.

Nebenbei zwei Stellen, die der Übersetzer aus Plex hinterlassen hat:
`catch is CancellationError` griff seit den getippten Fehlern nie mehr, weil
eine abgebrochene Netzanfrage als Übertragungsfehler zurückkommt; und das
Ersatz-Hintergrundbild einer Folge war toter Code, weil die linke Seite des
`??` gar nicht optional war.

TokenStore wandert nach MediaKit — Tokens sind nichts Plex-Eigenes. Die
Konten sind nach Server getrennt, damit ein späteres Umschalten nicht jedes
Mal eine neue Anmeldung kostet; „Abmelden" räumt trotzdem beide ab.

248 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 13:22:09 +02:00
ScarriffleandClaude Opus 5 fe2d730c00 Die Ansichten sprechen nicht mehr mit Plex
Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit
`MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für
einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre.

Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle`
wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und
`TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um
davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten.

`Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung
gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die
konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein
zweites Mal falsch — zu schreiben.

Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie
waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
Bildschirm eine andere Erklärung.

Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die
fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist
aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört
nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das
jetzt ausdrücklich, damit es niemand später „aufräumt".

Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit
Plex. Sie folgen als Nächstes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:46:52 +02:00
GuidoandClaude Opus 5 6150b018ec Vier Punkte: Player-Hänger, kaputte Downloads, Stufenliste, Kopfleiste
Player hing sich auf
  Die Qualitätsauswahl war ein Menu. Schloss man es durch einen Tipp ins
  Bild, blieb eine unsichtbare Ebene zurück, die alle weiteren Tipps
  schluckte — die Bedienelemente kamen nicht mehr, pausieren ging nicht
  mehr. Jetzt ein confirmationDialog, der sich selbst wegräumt. Nach dem
  Schließen werden die Bedienelemente eingeblendet und die Ausblendfrist
  neu gestartet.

Downloads galten als fertig, waren aber leer
  Zwei Fehler übereinander. Erstens gilt ein URLSessionDownloadTask auch
  bei 404 oder 500 als erfolgreich und legt die Fehlerseite als Datei ab.
  Zweitens überschrieb ich danach die erwartete Größe mit der empfangenen
  — womit die Anzeige auf hundert Prozent stand und der Beweis weg war.
  873 MB waren in einer Sekunde „fertig", und beim Abspielen kam nur
  „Wiedergabe wurde unterbrochen".

  Jetzt wird der Statuscode geprüft, die erwartete Größe bleibt stehen,
  und eine unplausible Datei wird verworfen statt verbucht: unter 100 KB
  ist keine Videodatei, unter der Hälfte der angekündigten Größe ist eine
  abgebrochene Übertragung. Sechs Tests dazu.

Stufenliste ergab keinen Sinn
  Für einen 720p-Inhalt standen Original, Full HD und HD mit derselben
  geschätzten Größe untereinander — das sah aus, als wäre die Schätzung
  kaputt. Sie war es nicht, die Stufen waren es: hochrechnen bringt nur
  eine größere Datei. Angeboten wird jetzt nur noch, was unter der
  Auflösung der Datei liegt, und beim Original steht dabei, was es ist:
  „Original · 720p · 873 MB".

Kopfleiste
  Kein Hintergrund mehr statt eines weichgezeichneten — die Knöpfe
  bringen ihre eigene Glasfläche mit. Der Titel ist weg: der Name der App
  gehört auf den Homescreen, nicht über jeden Bildschirm.

145 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:32:19 +02:00
GuidoandClaude Opus 5 ac0c27c026 Fünf Punkte aus dem Praxistest
iPad meldete sich nie an
  Die Ursache ist unangenehm speziell: Wer im Browser schon bei Plex
  angemeldet ist, wird von app.plex.tv/auth sofort auf watch.plex.tv
  weitergeschickt — OHNE dass der Code verknüpft wird. Man sieht sich
  angemeldet, die App wartet vergeblich, und es gibt keinerlei Hinweis.

  Jetzt wird plex.tv/link mit eingetragenem Code geöffnet. Diese Seite
  tut nur eine Sache: den Code bestätigen. Die Gerätekennung steckt
  ohnehin schon im PIN.

  Dazu ein Ausweg: bleibt es nach fünfzehn Sekunden ohne Token, erscheint
  der Code im Anmeldebildschirm zum Abtippen. Schließt man das Fenster
  ohne Erfolg, bleibt er stehen statt abzumelden — er gilt weiter.

Download fragte nicht nach der Qualität
  Jetzt Original, Full HD, HD oder Sparsam, jeweils mit geschätzter
  Größe. Die Größe ist das eigentliche Entscheidungskriterium — „Full HD"
  allein sagt nicht, ob es aufs Telefon passt.
  Die komprimierten Stufen laufen über den Universal-Transcoder und sind
  weiterhin NICHT am Server geprüft; steht so im Quelltext.

Schwarzes Bild ohne Anzeige
  Der Player kannte nur „spielt" und „spielt nicht". Wartete er auf
  Daten, stand man vor einem Standbild ohne Hinweis. Es gibt jetzt einen
  eigenen Pufferzustand mit Anzeige.

Aufmacher war beschnitten
  Er stand auf 3:4, Plex liefert als `art` aber 16:9 — mit scaledToFill
  ging links und rechts je ein Viertel verloren, ohne dass man es dem
  Bild ansah. Die Seitenverhältnisse liegen jetzt an einer Stelle in
  FlixyAspectRatio.

Schwarze Leiste über dem Aufmacher
  .ultraThinMaterial statt undurchsichtigem Grund: das Bild läuft
  unscharf hinter der Leiste weiter, statt an einer harten Kante zu
  enden. Weichgezeichnet, nicht abgedunkelt.

139 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:06:51 +02:00
GuidoandClaude Opus 5 bf0a63a924 Vier Punkte aus dem Praxistest
Download-Knopf tat nichts
  DownloadService.plan gab still nil zurück, wenn es keinen Auftrag
  bauen konnte, und download() reichte das als nil weiter. Der Knopf sah
  aus, als wäre er kaputt, dabei war der Grund jedes Mal bekannt. Es
  gibt jetzt einen eigenen Ablehnungsgrund mit vier unterschiedlichen
  Begründungen — keine Verbindung, keine Datei, kein abrufbarer Teil,
  keine Adresse. Eine Funktion, die kommentarlos nichts tut, ist selbst
  der Fehler.

Live-TV galt als „nicht eingerichtet"
  Plex legt die Empfängerliste unter dem Schlüssel `Dvr` ab, die Hülle
  kannte aber nur Metadata, Directory und Hub. Sie las eine leere Liste
  und zog daraus die falsche Schlussfolgerung — auf einem Server, auf
  dem ein Empfänger eingerichtet war. Dasselbe galt für die
  Anbieterliste unter `MediaProvider`, ohne die es keinen
  Programmführer gibt.

  Beim Umbau wäre beinahe die Fehlerdiagnose verloren gegangen: mit
  `try?` über alle Schlüssel verschwände auch ein echter Dekodierfehler
  und ergäbe stillschweigend eine leere Liste. Aus „ein Feld heißt jetzt
  anders" würde „die Mediathek ist leer". Jetzt wird erst geprüft, ob
  der Schlüssel da ist, und dann ohne try? dekodiert. Fünf Tests halten
  beides fest.

Qualität im Player wählbar
  Automatisch, Original, 1080p, 720p, 480p. Der Eingriff wird genau dann
  gebraucht, wenn es ruckelt — dafür will niemand den Film verlassen.
  Ein Wechsel baut den Strom neu auf (die Stufe steckt in der Adresse),
  merkt sich die Stelle und fährt sie wieder an. Der alte Transcoder
  wird vorher angehalten, sonst rechnet der Server zwei Ströme für einen
  Zuschauer.

AVPlayer-Anzeige entfernt
  Sie war eine Prüfhilfe aus Phase 1. Dieselbe Angabe steht weiterhin
  unter Einstellungen → Diagnose, wo sie hingehört.

137 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:44:20 +02:00
GuidoandClaude Opus 5 62158303a2 Phase 3: Download-Engine und Offline-Wiedergabe
Engine
- Hintergrund-URLSession: läuft weiter, wenn die App zu ist. Der ganze
  Zustand liegt in SwiftData, weil es nach einem Neustart kein anderes
  Gedächtnis gibt.
- Pausieren über cancel(byProducingResumeData:) — nur so lässt sich
  weiterladen statt neu zu beginnen. Bei 40 GB der Unterschied
  zwischen brauchbar und unbrauchbar.
- Zwei Übertragungen gleichzeitig, Folgen in Staffel-/Folgenreihenfolge.
  Alles parallel zu laden hieße: keine Folge wird fertig.
- Speicherlimit und Geräteplatz werden vor dem Annehmen geprüft, ein
  Gigabyte bleibt als Puffer frei.
- Verwaiste Dateien werden beim Start entfernt; sie entstehen, wenn die
  App genau beim Abschluss beendet wird.
- Downloads liegen in Application Support, nicht in Caches (iOS räumt
  den eigenmächtig leer) und sind von der iCloud-Sicherung ausgenommen.

Oberfläche
- Download-Knopf auf Detailseiten mit vier Zuständen in einem Knopf.
- „Alle Folgen laden" für Staffel und Serie; ein abgelehnter Titel
  stoppt den Stapel nicht.
- Downloads-Ansicht mit Belegung, WLAN-Schalter, Pausieren, Fortsetzen,
  Löschen — nach Serie gruppiert.

Offline
- Der Player bevorzugt die lokale Datei, auch ohne erreichbaren Server.
- Beim Herunterladen wird ein Abzug der Serverangaben mitgesichert:
  offline lässt sich nichts nachfragen, und der PlaybackDecider braucht
  Container und Codecs.

Ein Fehler, den die Tests aufgedeckt haben: der Koordinator hielt nur
den ModelContext, nicht den ModelContainer. Ein ModelContext hält seinen
Container nicht am Leben — wird der anderswo freigegeben, stürzt jeder
Zugriff auf einen Eintrag ab, und die Meldung zeigt auf ModelContext.reset
statt auf die Ursache. Jetzt liegt die Zuständigkeit beim Koordinator.

102 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:34:57 +02:00