Commit Graph
406 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 156c740330 Halo-Server, Stufe 1: Benutzer, Anmeldung, Einrichtung
Der Anfang eines eigenen Medienservers — C#, .NET 10, SQLite, im Container für
amd64 und arm64. Stufe 1 von fünf: er spielt noch keinen Film ab, aber ein
Fremder kann ihn in Betrieb nehmen, und genau daran scheitern solche Projekte
sonst als Erstes.

**Was läuft, gegen den laufenden Container gemessen** — nicht gegen einen
Prüfstand: Gesundheitsfrage, Einrichtungsassistent, zweiter Einrichtungsversuch
abgelehnt (409), Anmeldung, falsches Passwort abgelehnt (401), und der ganze
Weg der Code-Anmeldung vom Bildschirm bis zum Token.

**Drei Entscheidungen, die beim Lesen Fragen aufwerfen:**

*Zeitpunkte liegen als Unix-Millisekunden in der Datenbank.* SQLite hat keinen
Zeittyp; EF legt einen `DateTimeOffset` sonst als Text mitsamt Zonenversatz ab,
und dann steht `…T12:00+02:00` neben `…T11:00Z` — derselbe Augenblick, als Text
falsch herum sortiert. EF weigert sich deshalb, `ExpiresAt > jetzt` überhaupt zu
übersetzen; das war der erste echte Fehlschlag und ist jetzt eine
Typumwandlung für das ganze Modell statt je Eigenschaft.

*Token sind keine JWT.* Der Server schlägt sie nach, statt sie zu prüfen — nur
so lässt sich ein verlorenes Telefon sofort aussperren. In der Datenbank steht
allein der Fingerabdruck.

*Der Code am Fernseher holt nie selbst ein Token ab.* Dafür gibt es den
Abholschein, den nur das anfragende Gerät kennt. Ohne ihn wären sechsstellige
Codes ein Türschloss mit einer Million Schlüsseln — in Minuten durchprobiert.
Dass ein erratener Schein nichts bekommt, ist die wichtigste der 55 Prüfungen.

**Zwei Fehler, die erst der echte Container zeigte:**

`EnsureExists` legte nur an und prüfte nicht, ob sich schreiben lässt — bei
einem vorhandenen, aber gesperrten Verzeichnis meldete es Erfolg, und der
Server starb zehn Zeilen später an „unable to open database file". Diese
Meldung nennt weder das Verzeichnis noch verrät sie, dass es um Dateirechte
geht. Jetzt wird tatsächlich geschrieben, und die Meldung nennt Variable,
Pfad, die eigene Kennung und den nächsten Schritt.

Und: Docker legt den Einhängepunkt eines frischen benannten Volumes als `root`
an, wenn das Verzeichnis im Image fehlt. Das traf jeden, der ohne Bind Mount
startet. Die drei Verzeichnisse werden jetzt im Image angelegt und übereignet.

**Die Lizenz ist eine Bauvorschrift, keine Fussnote.** GPL-2.0-only verträgt
sich nicht mit Apache-2.0 — dessen Patentklausel gilt unter Abschnitt 6 als
„weitere Einschränkung". Darum steht in `LICENSES.md`, dass vor jedem
`PackageReference` die Lizenz zu prüfen ist, und darum ist das Testwerk NUnit
(MIT) und nicht xUnit (Apache-2.0). Alle vier Pakete sind geprüft MIT.

Die Apps stehen unter MPL-2.0 statt GPL, weil GPL und App Store sich nicht
vertragen — Apple entfernte 2011 VLC aus dem Store, nachdem ein Rechteinhaber
sich auf Abschnitt 6 berufen hatte.

55 Tests grün. Image baut für linux/amd64 und linux/arm64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 12:13:47 +02:00
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit,
damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind.

712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25
Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit
`HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`,
`JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben.

`docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind
ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben
hiesse, die Vergangenheit zu glätten.

**Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis:
`com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte
Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container;
Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu
laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen.

**Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der
Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr
heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter
`…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur
anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim
Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor
dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet.

Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht
statt umbenannt — sie steht ohnehin nicht im Repo.

Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist
ein Handgriff ausserhalb des Projekts.

368 Tests grün. iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:12:32 +02:00
ScarriffleandClaude Opus 5 2fcf6821c8 Plex ist raus — und nahm zwei stille Fehler mit
Vollständig entfernt: das Paket `PlexKit` samt 108 Tests, der
Web-Authenticator, die Anmeldung über plex.tv, die Serverauswahl und alles,
was daran hing. 43 Dateien gelöscht, 68 Stellen in 28 weiteren bereinigt.

**Zwei Dinge hätten beim blossen Streichen still das Falsche getan.**

`playbackUnavailableMessage` deckte im Else-Zweig zwei Lagen gleichzeitig ab
— „es ist Plex" und „niemand ist angemeldet". Wer die Serverart naiv
herausstreicht, bekommt als nicht angemeldeter Nutzer „Jellyfin-Wiedergabe
steht aus" statt „Server nicht erreichbar". Die Unterscheidung hängt jetzt
daran, ob überhaupt ein Server da ist.

Die Schlüsselbund-Einträge `plex.account` und `plex.server` wurden allein
über den Authenticator aus dem gelöschten Paket entfernt. Danach stünde
`plex.account` in keiner Zeile dieses Projekts mehr: wer sich nie abgemeldet
hat, trüge ein gültiges Token am fremden Konto bis in alle Ewigkeit mit sich,
ohne dass irgendein Weg es noch löschen könnte. `purgePlexLeftovers()` räumt
beide Token und fünf Voreinstellungen einmalig ab — und fasst nichts an, wo
nichts liegt, damit kein Jellyfin-Nutzer dafür seinen Mediathekenspeicher
verliert.

**Was Nutzer sehen.** Die Berechtigungsfrage fürs lokale Netz sagte wörtlich,
die App brauche es, „um deinen Plex-Server zu finden" — das stand so im
Systemdialog. Der Anmeldebildschirm hatte zwei Knöpfe, und der hervorgehobene
war der für Plex; der verbliebene erbt die Hervorhebung, denn ein Knopf, der
aussieht wie die zweite Wahl, lässt nach der ersten suchen. 14 verwaiste
Textbausteine sind weg, vier umformuliert.

Darunter einer, der schlicht nicht mehr stimmte: `signIn.privacy` versprach,
das Passwort erreiche diese App nie. Beim Jellyfin-Rückfall auf das Passwort
tut es das sehr wohl. Die Zusage, die weiterhin gilt, ist die über fremde
Dienste — und nur die steht jetzt da.

**Was ausdrücklich bleibt:** `MediaBackend`. Diese Abstraktion hat zwei
Server gleichzeitig getragen, und der Steckplatz, den Plex räumt, ist genau
der, auf den Halo kommt. `ServerKind` fällt dagegen weg — mit einem Fall ist
es ein Bool, das `backend != nil` schon beantwortet.

Nebenbei zog die README nach. Sie beschrieb noch einen „nativen Plex-Client"
mit zwei Playern und VLCKit, beides seit Längerem nicht mehr wahr.

368 Tests grün (von 476 minus die 108 aus PlexKitTests, kein anderer).
iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:10:20 +02:00
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 24d80e5c95 Offline fragte die Spurauswahl den Server — den es dort nicht gibt
Deine Beobachtung, Schritt für Schritt nachgemessen: der Untertitel-Knopf war
da, das Sheet zeigte nur „Aus".

**Zwei Quellen für dieselbe Frage.** Der Knopf richtete sich nach den Spuren,
die der Player in der Datei findet (`subtitleTracks`); die Liste nach den
Spuren, die der Server gemeldet hat (`subtitleStreams`). Die kommen aus
`chooseAudioStream`, und das läuft nur auf dem Online-Weg. Offline war die
Liste deshalb leer — Knopf da, nichts dahinter.

Offline listet das Sheet jetzt die Spuren der Datei und schaltet sie
unmittelbar in der Engine um. Online bleibt es beim bisherigen Weg: dort
heisst Umstellen „neu bestellen", weil ein umgerechneter Stream genau eine
Spur mitbringt.

**Der Tonspur-Knopf verschwindet, wo es nichts zu wählen gibt.** Eine Spur
ist keine Wahl — ein Angebot, das nach dem Antippen nichts anbietet. Bei den
Untertiteln genügt dagegen eine: „an oder aus" ist auch eine Wahl.

**Belegt, dass die Spur wirklich ankommt.** Die heruntergeladene Datei direkt
mit AVFoundation gelesen:

    Spuren in der Datei: 3
      vide / soun / sbtl   (sbtl = tx3g, also mov_text)
    Gruppe AVMediaCharacteristicLegible: 2 Auswahlmöglichkeiten

Der Untertitel ist drin — er wurde nur nie angeboten.

**Was dabei auffällt und offen bleibt:** alle Spuren der umgepackten Datei
tragen die Sprache `und`. Jellyfin reicht die Sprachkennung beim Umpacken
nicht durch, also heissen sie „Unknown language". Das ist eine eigene Sache;
die App weiss, welche Sprache sie bestellt hat, und könnte sie beschriften.

iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 09:07:04 +02:00
ScarriffleandClaude Opus 5 25e6ada0ce Herunterladen steht jetzt im Kontextmenü
Bisher führte der Weg über die Detailseite — zwei Schritte für etwas, das man
beim Vorbeiscrollen entscheidet. Langes Drücken auf eine Kachel bietet es
jetzt direkt an, samt derselben Qualitätsauswahl.

Die Kachel kennt nur eine Zusammenfassung ohne Dateiangaben; geholt wird
deshalb erst beim Tippen. Eine Reihe aus zwanzig Kacheln wären sonst zwanzig
Anfragen für ein Menü, das selten aufgeht.

**Nur bei Filmen und Folgen.** Bei einer Serie oder Staffel lädt man nicht
*den* Titel, sondern seine Folgen — dafür gibt es auf der Seite „Alle Folgen
laden". Und nicht bei dem, was schon in der Warteschlange steht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:46:02 +02:00
ScarriffleandClaude Opus 5 6b6bc6ad02 Der Server lieferte nichts — drei Angaben fehlten in der Adresse
Am echten Server gemessen, nachdem der erste Versuch mit „Fehler 500"
abgelehnt wurde. Drei Befunde, alle reproduziert:

**`deviceId` und `playSessionId` fehlten.** Ohne sie antwortet Jellyfin mit
`200` und `Content-Length: 0` — kein Fehler, keine Datei, nichts. Mit ihnen
kamen in fünfzehn Sekunden 66 MB.

Das betraf **auch die verkleinerten Fassungen**: `compressedDownloadURL`
hatte sie nie. „In 480p herunterladen" war damit die ganze Zeit folgenlos —
ein Fehler, der älter ist als alles von heute.

**Die Sitzungskennung muss je Auftrag neu sein.** Zuerst hatte ich sie an die
Titelkennung gehängt, damit ein Wiederholversuch dieselbe Sitzung bleibt.
Zwei Anfragen unter derselben Kennung hielt der Server aber für dieselbe
Sitzung und antwortete der zweiten mit einer leeren Datei. Jetzt eine UUID
beim Anlegen des Auftrags — eindeutig je Download, gleich über alle
Wiederholversuche, weil die Adresse gespeichert ist.

**`subtitleCodec=mov_text` muss dabeistehen.** Das war der Fehler 500. Mit
`subtitleMethod=Embed` allein lehnt der Server ab; derselbe Aufruf ohne
Untertitel liefert 61 MB, mit Untertitel und `mov_text` 42 MB samt Spur. MP4
kennt kein SubRip — wer den Untertitel hineinlegen will, muss sagen, als was.

Nachgewiesen im Simulator gegen den echten Server: „100,0 MB / 917,3 MB ·
6,1 MB/s · noch 2 Min · 11 %".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:46:02 +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 6b927c21b0 VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er
nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und
Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an
einer Stelle, `PlaybackDecider.fallback`.

Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket.

**Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt,
sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind`
fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy`
kennt kein `allowsFallbackEngine`.

**Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am
Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern,
ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer
nicht an. Statt der zweiten Engine rechnet der Server jetzt neu
(`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer
schwarzen Wand mit einer Fehlernummer.

**Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der
einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird,
kommt schon in einer Hülle, die der Systemplayer öffnet.

Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten:
Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten
auf dem Apple TV, wo es VLC ohnehin nie gab.

Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist
geblieben, sein Ziel nicht.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:04:23 +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 b1973c6890 Bild-in-Bild gab es nur beim Weggehen — jetzt auch auf Zuruf
Du hast keinen Knopf gefunden, weil es keinen gab: `AVPictureInPictureController`
stand da, aber nur mit `canStartPictureInPictureAutomaticallyFromInline`. Das
greift, wenn man die App verlässt — und auf dem Mac gar nicht.

Der Knopf erscheint nur, wenn es wirklich ginge. Das ist nicht immer so:
`attachSurface` steigt für alles ausser `AVPlayerEngine` sofort aus, und
`AVPictureInPictureController` braucht eine `AVPlayerLayer`. Läuft ein Titel
über VLC, gibt es kein Bild-in-Bild — nur Ton im Hintergrund. Genau das war
deine Beobachtung.

**Beobachtet, nicht abgefragt.** `isPictureInPicturePossible` steht erst fest,
wenn der Player ein Bild hat; wer beim Zeichnen fragt, bekommt `false` und
zeichnet den Knopf nie wieder. Also meldet es sich über KVO — vom
Input-Thread, deshalb der Sprung auf den Hauptakteur, wie bei den übrigen
Beobachtungen.

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 915bdcd277 Die Downloadzeile stand nicht still — und sagte nicht, wie weit sie ist
Aus deiner Aufnahme: „181,2 MB … 3,4 MB/s" wird „190,1 MB … 5,8 MB/s", und
dabei wandert alles dahinter mit. In einer Proportionalschrift ist die 1
schmal und die 8 breit; zweimal je Sekunde ergibt das ein Zappeln.

Drei Dinge halten die Zeile jetzt still:

**Ziffern in fester Breite** (`monospacedDigit`) — damit kostet jede Ziffer
gleich viel Platz, egal welche.

**Eine feste Nachkommastelle.** `.byteCount` lässt die Null hinter dem Komma
weg: aus „186,0 MB" wird „186 MB", und die Zeile ist zwei Zeichen kürzer.
Gemessen an sechs aufeinanderfolgenden Sekunden war das die einzige, die
noch sprang. Die Zahlen kommen deshalb über `Measurement` mit fester Stelle.

**Die Prozente am rechten Rand.** Dort wachsen sie nach innen und schieben
nichts; die Zeile links wächst nach rechts und schiebt ebenfalls nichts.

Gemessen an acht aufeinanderfolgenden Sekunden: „343.1 / 347.0 / 350.9 /
355.0 / 359.1 / 363.5 / 368.1 / 373.0 MB" — jede Spalte beginnt auf
demselben Punkt.

**Ein Umweg, der nicht blieb.** Beide Zahlen in das Mass des Ziels zu
zwingen hätte auch den Wechsel von MB auf GB beseitigt — machte den Anfang
aber unlesbar: bei 2 GB Ziel stand dort „0,03 GB / 2 GB". Der Wechsel kommt
einmal je Download; das ist der kleinere Preis.

**Und die Prozente**, nach denen man eigentlich sieht. „181,2 MB von 890,2
MB" sagt dasselbe, aber man muss es ausrechnen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 21:57:53 +02:00
ScarriffleandClaude Opus 5 ad556c9034 Ein Knopf an jeder Folge, und die Suche spielt nicht mehr sofort ab
**Staffelansicht: Download je Folge.** Für den Fall, den du beschrieben
hast — Staffel halb gesehen, Rest nachholen. Ein Tipp öffnet dieselbe
Qualitätsauswahl wie auf der Detailseite.

Der Knopf steht **neben** dem Link, nicht darin: ein `Button` im Label
eines `NavigationLink` löst auf iOS nie aus, und `.plain` lässt das
lautlos geschehen. Dieselbe Falle steht in `QuickPlayTile` schon
beschrieben. Als Geschwister in einer Reihe teilt das Layout den Platz
auf, und der Titel kann gar nicht erst unter dem Knopf verschwinden.

Die Kinderliste trägt keine Dateiangaben — ohne Nachladen wäre die
Qualitätsliste leer und die Prüfung auf freien Speicher übersprungen.
`DownloadButton` holt sie deshalb beim ersten Tipp nach (`loadsDetails`),
mit Kreisel und fester Breite, damit die Zeile nicht springt. Nicht beim
Erscheinen: zwanzig Zeilen wären zwanzig Anfragen für einen Knopf, den man
meistens nicht drückt.

Nur bei Folgen. Für eine ganze Staffel gibt es den Knopf oben schon.

**Suche: Antippen führt auf die Seite.** Die Suche baut dieselbe Reihe wie
die Startseite und war damit auch dieselbe Geste. Wer sucht, weiß aber
noch nicht, was er sehen will — der erste Treffer ist eine Vermutung des
Servers. Abspielen steht weiter im Menü unter langem Drücken.

Dabei aufgefallen: `SearchView` hinterlegte `flixyItemChanged` nicht,
anders als Startseite und Mediathek. „Als gesehen markieren" wirkte dort
am Server, aber die Kachel lud nie neu — es sah aus, als wäre nichts
passiert.

Im Simulator geprüft: Knopf an jeder Folge, Auswahl mit echten Größen
(„Original · HD · 906,9 MB"), Suchtreffer öffnet die Detailseite. iOS und
tvOS bauen, 489 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 21:05:49 +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 c37087fd34 Auf dem Telefon stand ein Plakat im Aufmacher — zweimal derselbe Name
Der Aufmacher auf dem iPhone hat nicht das Hintergrundbild geholt,
sondern das **Plakat**. Dafür gab es einen Grund, und er stimmte: ein
16:9-Bild in einem 2:3-Rahmen ist ein Streifen aus der Mitte.

Nur war der Rahmen das Problem, nicht das Bild. Ein Plakat trägt den
Titel selbst, und darüber legt der Aufmacher noch den freigestellten
Schriftzug — derselbe Name zweimal, einmal gesetzt und einmal gemalt.

**Jetzt ein Querbild in einem 4:3-Rahmen.** 16:9 ginge nicht: das sind
auf 440 Punkten Breite 248 Punkte Höhe, und darin sollen Schriftzug,
Zeile, ein Satz Beschreibung und zwei Knöpfe stehen. Sie standen schon
einmal übereinander, das ist der dokumentierte Grund für das Hochformat.
4:3 gibt 330 Punkte — gemessen passt der Text hinein; dafür fehlt dem
Bild links und rechts zusammen ein Viertel.

**Die Detailseite bleibt hochkant.** Dort hat man sich schon entschieden;
das Plakat ist das Bild dieses Films, und du wolltest den Kopf der Seite
ausdrücklich höher. Beide Seiten lasen bisher aus **derselben**
Funktion — eine Änderung für den Aufmacher hätte die Detailseite von 660
auf 330 Punkte gestutzt, ohne dass irgendwo ein Fehler aufgetaucht wäre.
`HeroArtwork` kennt deshalb jetzt den Verwendungszweck:

    HeroArtwork.aspectRatio(.detailHeader, compact: …)   // 2:3
    HeroArtwork.aspectRatio(compact: …)                  // 4:3 bzw. 16:9

Maße, Verhältnis und Reihenfolge der Bildpfade hängen alle an **einer**
privaten Abfrage — sie müssen zusammenpassen, sonst beschneidet der
Server anders als der Rahmen.

Die Maße schmal: 1200×900 statt 1920×1080. Der Rahmen ist dort höchstens
1320 Bildpunkte breit; das spart auf einer Mobilfunkverbindung ein
Drittel.

Gemessen im Simulator (iPhone 17 Pro Max): Aufmacher 4:3, Text passt,
Detailseite unverändert hochkant. iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 12:57:42 +02:00
ScarriffleandClaude Opus 5 2ab05ee8f6 Der Schriftzug gehörte zum vorigen Film — ein Zustand ohne Adresse
„Werner Beinhart" über dem Bild von John Wick. Von mir eingebaut, im
Commit, der die Überblendung umgestellt hat.

**Der Grund ist der Umbau selbst.** Vorher bekam der Aufmacher je Titel
eine neue Identität; sein Zustand begann damit jedes Mal von vorn. Seit
er bleibt und nur seine Angaben wechseln, überlebt auch der geladene
Schriftzug den Wechsel — und `availableLogo` gab ihn heraus, ohne zu
prüfen, ob er überhaupt zum aktuellen Titel gehört:

    if let logo { return logo }        // ← zu irgendeinem Film

Jetzt trägt der Zustand die Adresse mit, zu der er gehört, und wird nur
herausgegeben, wenn sie zur aktuellen passt. Passt sie nicht, gilt der
Zwischenspeicher — und wenn auch der nichts hat, der gesetzte Titel. Nie
der falsche Schriftzug.

**Dieselbe Falle steckte in `FlixyArtwork`**, nur unauffälliger: auch
dort hielt `loaded` ein Bild ohne Angabe, wozu es gehört, und `load()`
brach deswegen früh ab („ist ja schon eins da"). Solange jede Kachel ihre
eigene Identität hat, fällt das nicht auf. Der Aufmacher hat seit dem
Umbau keine mehr.

Die Regel dahinter gilt allgemein: **ein Zustand, der etwas
zwischenspeichert, muss sagen, wozu es gehört** — sonst gilt er für
alles.

Die Attrappe liefert jetzt Schriftzüge (`Logo`-Bildmarke). Ohne sie fällt
der Aufmacher auf den gesetzten Titel zurück, und dieser ganze Weg wäre
im Simulator gar nicht zu sehen — genau deshalb ist der Fehler erst beim
Nutzer aufgefallen.

iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 12:29:26 +02:00
ScarriffleandClaude Opus 5 37bb153dd8 Der Schriftzug wandert wieder mit — über dem Verlauf statt in ihm
Im Commit davor blendete nur noch das Bild über; Schriftzug, Zeile und
Knöpfe standen still und wechselten schlagartig. Das war zu viel
weggenommen: die Bewegung gehört dazu.

Jetzt wandert der Text wieder mit — aber an der richtigen Stelle im
Stapel. Der Aufmacher zeichnet seinen Inhalt während eines Wechsels
zweimal: den weichenden deckend, den kommenden mit demselben Einblenden
und demselben Hereinrutschen wie das Bild darunter. Beides liegt **über**
dem Verlauf, und dort kostet es ihn nichts.

Der Unterschied zum alten Aufbau ist genau diese Reihenfolge. Vorher
lagen zwei **vollständige** Aufmacher übereinander, jeder mit eigenem
Verlauf — und zwei halbdurchsichtige Verläufe ergeben zusammen keinen
ganzen. Jetzt gibt es einen Verlauf, einmal, in voller Stärke.

Nachgemessen über einen auf sechs Sekunden gestreckten Wechsel, an
derselben Stelle im Bild:

    ursprünglich   68 .. 154
    jetzt          63 ..  75

Dafür ist der Textsatz in `HeroBanner` eine eigene Funktion geworden —
es gibt ihn während eines Wechsels zweimal — und `HeroSection.metadata`
ist `static`, weil die Zeile auch für den weichenden Titel gebraucht
wird. `titleView` und `plainTitle` sind darin aufgegangen.

iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 12:18:58 +02:00
ScarriffleandClaude Opus 5 63c664e5a9 Der Aufmacher hellte beim Wechsel auf — zwei Verläufe ergeben keinen
Beim Wechsel im Karussell war der Verlauf nach unten für einen Moment
weg: das Bild stand bis unten hell da, und erst nach dem Übergang kam das
Abdunkeln zurück.

**Der Grund steckt in der Form der Überblendung.** Das Karussell legte
zwei vollständige Aufmacher übereinander und blendete den oberen ein. Nur
bringt jeder von beiden seinen eigenen Verlauf mit — und zwei
halbdurchsichtige Verläufe ergeben zusammen keinen ganzen.

Gemessen, an derselben Stelle im Bild, über einen auf sechs Sekunden
gestreckten Wechsel:

    Ruhezustand        68 .. 75
    Mitte des Wechsels      137        ← fast doppelt so hell

Zwei naheliegende Reparaturen habe ich davor versucht und verworfen,
weil die Messung sie widerlegt hat: dem weichenden Aufmacher die
Animation zu nehmen (wurde schlechter, 152) und beide Ebenen
gegenläufig zu blenden (brachte einen dunklen Einbruch auf 35 dazu).
Beide behandelten das Symptom, nicht die Form.

**Jetzt wechselt nur das Bild.** `HeroBanner` nimmt zusätzlich die
weichende Bildadresse entgegen und blendet **innen** über; Verlauf,
Schriftzug und Knöpfe liegen einmal darüber und ändern sich dabei nicht.
Damit kann der Verlauf gar nicht mehr schwächer werden — nicht weil es
jetzt richtig gerechnet wird, sondern weil es nur noch einen gibt.

Nachgemessen, derselbe Wechsel:

    vorher   68 .. 154
    jetzt    63 ..  75

Die Aufmacher-Ansicht behält dabei ihre Identität, statt je Titel eine
neue zu bekommen: eine neue hiesse Einfügen und Entfernen, und SwiftUI
blendete sie dann zusätzlich ein — noch eine Überblendung über der
gemeinten.

iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 11:11:22 +02:00
ScarriffleandClaude Opus 5 a665d79e6d Antippen spielt ab — und vier Dinge, die daneben lagen
**Antippen startet, langes Drücken führt auf die Detailseite.** Am Telefon
und am iPad; am Mac bleibt der Klick die Seite, denn dort gibt es den
Abspielknopf beim Überfahren und langes Drücken ist keine übliche Geste.

Die Hürde war nicht die Geste, sondern das Schieben: ein `NavigationLink`
schiebt selbst, ein Menüeintrag kann es nicht. Der Stapel ist deshalb ein
eigener Typ mit Pfad (`MediaStack`) und reicht eine Schiebefunktion über
die Umgebung nach innen — denselben Weg nimmt schon die Meldung, dass sich
am Server etwas geändert hat.

**Zwei Knöpfe übereinander auf der Detailseite.** Das Monogramm hing als
Überzug an `mediaStack` — also über **jedem** Bildschirm dieses Stapels,
auch den geschobenen, und dort über deren Drei-Punkte-Menü. Es gilt jetzt
für die Startseite und endet mit ihr.

**Der Kopf der Detailseite war viel zu flach.** Dort stand kein
Seitenverhältnis, also galt die Vorgabe 16∶9 — während `HeroArtwork` auf
einem schmalen Gerät ein **Plakat** anfordert, also hochkant. Zu sehen war
ein Streifen aus der Mitte. Der Aufmacher auf der Startseite macht es seit
Langem richtig; die beiden Detailseiten nicht.

**Die Mediathekenleiste stand linksbündig in einer halbleeren Kapsel.**
Jetzt ist sie so breit wie ihr Inhalt und mittig. `ViewThatFits` nimmt die
ungescrollte Reihe, solange sie passt, sonst die scrollbare — damit steht
nirgends eine Zahl, ab wann es kippt.

**Und der Grund, warum die Mediathekenauswahl beim Nutzer nie griff:**
Plex. Dessen Hubs sagten nicht, aus welcher Mediathek sie stammen, also
warf der Filter jede Reihe weg und fiel auf „alles" zurück — ein Schalter,
der nichts tut. `PlexHub` liest jetzt `librarySectionID`, als Zahl oder
als Zeichenkette, je nachdem was der Server liefert. Vier Tests dazu.

Das ist **kein** Wiederaufnehmen der Plex-Arbeit: es repariert eine
Funktion, die dort gelogen hat. Neues kommt weiterhin nicht dazu.

iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 10:39:45 +02:00
ScarriffleandClaude Opus 5 1fa8011094 Eine Mediathek fehlte ganz — „Heimvideos" lagen bei den Fotos
Jellyfin nennt „Heimvideos & Fotos" `collectionType: homevideos`. Die
Zuordnung legte das zusammen mit `photos` auf `.photo` — und damit fiel
die ganze Mediathek unter `MediaLibrary.isSupported` heraus: keine Reihe
auf der Startseite, kein Eintrag in der Mediathekenleiste, nicht einmal
eine Zeile in der Auswahl für den Aufmacher. Aus Sicht der App gab es sie
nicht.

Dass Fotos im Namen stehen, macht den Inhalt nicht zu Fotos. `photos`
bleibt bei den Fotos, `homevideos` ist jetzt `.other` — wie alles, was
der Server nicht einsortiert.

Die Zuordnung steht dafür nicht mehr mitten in `libraries()`, sondern als
eigene Funktion mit Tests. Sie entscheidet mehr, als sie aussieht: was
hier als `.photo` oder `.music` landet, ist in der App unsichtbar, und
das sieht aus wie ein Server, der nichts liefert — nicht wie eine
Zuordnung, die zu scharf ist.

Drei Tests halten es fest, darunter der Fall ohne Angabe: eine gemischte
Mediathek meldet gar keine Art und muss trotzdem bleiben.

iOS und tvOS bauen, 472 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 10:25:16 +02:00
ScarriffleandClaude Opus 5 0a59b8e82b Die Attrappe unterscheidet die Mediatheken — und der Aufmacher sagt, wenn eine Auswahl ins Leere greift
Gemessen, weil „es tut nichts" und „es tut etwas anderes" von aussen
gleich aussehen. Gegen die Attrappe, mit laufendem Mitschnitt:

    Auswahl ["lib-shows","lib-movies"] → übrig [latest.lib-movies, latest.lib-shows]
    Auswahl ["lib-shows"]              → übrig [latest.lib-shows]

Und der Aufmacher wechselte dabei **sofort** von „Nachtzug nach Genf"
(Film) zu „Küstenwache" (Serie) — ohne Neustart, mit offener App. Der
Weg stimmt also; was gemeldet wurde, muss eine andere Ursache haben.

Zwei Dinge, die diese Messung erst möglich gemacht haben:

**Die Attrappe hatte in jeder Mediathek dasselbe stehen.** „Neu in Filme",
„Neu in Serien" und „Neu in Gemischtes" lieferten dieselben Titel — damit
sieht ein wirkungsloser Filter genauso aus wie ein wirksamer. Jetzt gibt
es zwei Titel, die es **nur** in „Gemischtes" gibt, und jede Mediathek
führt, was zu ihr gehört.

**Ein Zwischenergebnis, das ich fast für den Fehler gehalten hätte:** ein
Neuinstallieren über `simctl install` löscht die Voreinstellungen nicht.
Die vermeintlich frische Auswahl war in Wahrheit die alte plus die neue —
zwei Messungen lang.

**Und der Punkt, der dem gemeldeten Verhalten am nächsten kommt:** trifft
die Auswahl keine einzige Reihe, gilt sie seit gestern als wirkungslos und
der Aufmacher zeigt alles. Das verhindert einen leeren Aufmacher, sieht
aber genau aus wie ein Schalter, der nichts tut. Jetzt steht es dort, wo
man es einstellt: ein Hinweis unter der Auswahl, sobald für die
angehakten Mediatheken keine eigene Reihe auf der Startseite existiert.

Welche Mediatheken überhaupt eine Reihe stellen, wird dafür aus den
geladenen Reihen gelesen, nicht angenommen.

iOS und tvOS bauen, 469 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 10:18:53 +02:00
ScarriffleandClaude Opus 5 7f380e5f4d Die Bibliothek beginnt jetzt in einer Mediathek
Vorher stand dort eine Liste aller Mediatheken: auf „Bibliothek" tippen,
dann auf „Filme", dann endlich ein Raster. In eine andere wechseln hiess
zurück und wieder hinein. Der Zwischenbildschirm hat nie etwas erzählt,
was eine Leiste nicht auch zeigen kann — er war ein Platzproblem, kein
Entwurf.

Jetzt steht man sofort in einer Mediathek, und darüber der unteren Leiste
liegt eine zweite mit den Mediatheken und Live-TV. Auf dem iPad und am Mac
ändert sich nichts: dort steht jede Mediathek einzeln in der Seitenleiste,
auf dem Fernseher nebeneinander in der oberen Leiste — es ist dieselbe
Idee in der Form, die ein Telefon hergibt.

**Warum eine zweite Leiste und nicht Plätze in der unteren.** Die hat
fünf, und alles darüber schiebt das System in ein „Mehr"-Verzeichnis,
dessen Einträge zwei Navigationsleisten übereinander tragen. Genau daran
ist der Zwischenbildschirm seinerzeit entstanden. Eine selbst gezeichnete
Leiste kennt diese Grenze nicht — und sie sieht aus wie die Insel auf dem
iPad, weil sie dieselbe Fläche benutzt: `FlixySidebar.surface(in:)` ist
ein Bauteil, kein nachgebautes Aussehen, und wird jetzt zum dritten Mal
gerufen.

Als Sicherheitsrand und nicht als Überzug, damit die Liste über der
Leiste endet statt dahinter zu verschwinden. Bei mehr als vier
Mediatheken wird sie waagerecht scrollbar.

**Das Monogramm steht jetzt in der Werkzeugleiste**, am rechten Ende, auf
derselben Höhe wie Suche, Filter und Ansicht — die rücken dafür nach
links. Als Überzug lag es über dem Inhalt, auf einem Raster also über
einem Plakat. Dass es dabei an den Anfang der Reihe rutschte, hat einen
Grund, der im Quelltext steht: SwiftUI reiht Werkzeugleisten von aussen
nach innen, ein von aussen angehängter Eintrag steht deshalb **vor** den
übrigen. Wo in der Reihe etwas steht, kann nur der entscheiden, der die
Reihe schreibt. Auf der Startseite bleibt es ein Überzug — dort gibt es
keine Navigationsleiste.

Dazu: die Zuordnung Mediathekenart → Symbol stand dreimal im Projekt, eine
davon falsch (jede Mediathek bekam „film", auch eine mit Serien). Jetzt
einmal, neben den übrigen Ableitungen aus den Modellen.

Geprüft im Simulator: „Bibliothek" öffnet sofort das Raster, die Leiste
wechselt zwischen Filme, Serien, Gemischtes und Live-TV, und das
Monogramm steht ganz rechts in der Werkzeugleiste.

iOS und tvOS bauen, 469 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 09:40:00 +02:00
ScarriffleandClaude Opus 5 0aa11e1c25 Die Einstellungen verlassen die Leiste — oben rechts steht jetzt das Konto
Auf dem iPhone belegten die Einstellungen einen von fünf Plätzen in der
unteren Leiste, gleichrangig neben Start und Bibliothek. Das unterstellt,
dass man zwischen ihnen hin und her wechselt, und das tut niemand.

Jetzt stehen dort drei Einträge — Start, Bibliothek, Downloads — und der
Weg in die Einstellungen führt über das Monogramm oben rechts. Auf dem
iPad und am Mac bleibt alles, wie es war: dort hat die Seitenleiste Platz,
und den Weg über das Konto gibt es ohnehin schon.

Drei Entscheidungen, die nicht offensichtlich sind:

**Ein Überzug, kein Werkzeugleisten-Knopf.** Die Startseite schaltet ihre
Navigationsleiste ganz ab — sie war leer und belegte trotzdem Höhe —, ein
`ToolbarItem` erschiene dort gar nicht. Der Knopf liegt deshalb einmal
über allen Bildschirmen statt dreimal in deren Leisten und sitzt überall
an derselben Stelle. Beim Scrollen blendet er sich aus, wie die Insel auf
dem iPad: über einem Film, den man gerade sucht, hat ein Kontoknopf
nichts verloren.

**Ein Blatt, kein Bereich.** `SettingsView` ist ein Formular aus
geschobenen Unterbildschirmen und braucht nur einen Navigationsstapel um
sich; die Medienziele aus `mediaStack` ruft es nie auf. Beim Schliessen
steht man wieder dort, wo man war.

**Ein Buchstabe, kein Bild.** Den Avatar gab es schon — er steckte in der
Kontozeile der Seitenleiste und wird jetzt von beiden benutzt. Ein
Serverbild führt das Konto heute nirgends mit, weder bei Jellyfin noch
bei Plex; eine graue Silhouette, die bei jedem gleich aussieht, sagt
weniger als der Anfangsbuchstabe.

Dazu gehört `.settings` in die Rückfallliste beim Grössenwechsel: wer am
iPad in den Einstellungen steht und das Fenster schmal zieht, stünde sonst
vor einem Bereich, den die Leiste gar nicht mehr führt.

Geprüft im Simulator: drei Einträge unten, Monogramm oben rechts, Tippen
öffnet die Einstellungen, Schliessen führt zurück.

iOS und tvOS bauen, 469 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 09:32:38 +02:00
ScarriffleandClaude Opus 5 19c9dc843c „Aus Weiterschauen entfernen" tut jetzt etwas — und man kann es sehen
Der Knopf war wirkungslos, und zwar aus zwei Gründen, die beide auf
dasselbe hinauslaufen: er war nie geprüft worden.

**Der Weg war falsch.** Die Methode las erst ein Benutzerdatenblatt über
`GET /Users/{id}/Items/{id}/UserData`, änderte es und schrieb es zurück.
Diese Stelle war die **einzige im ganzen Projekt**, die ein solches Blatt
eigens liest — überall sonst kommt es am Titel mit. Es gab also kein
zweites Beispiel, an dem der Weg je funktioniert hätte, und ein 404
beendete die Sache, bevor überhaupt etwas geschrieben wurde.

Jetzt wird das Blatt **gebaut**, nicht erfragt: „gesehen", „Favorit" und
die Zählung stehen am Titel, den der Aufrufer ohnehin in der Hand hat —
deshalb nimmt `removeFromContinueWatching` jetzt den Titel statt nur
seiner Kennung. Gelesen wird trotzdem noch, aber unverbindlich: gelingt
es, bleiben auch Felder erhalten, die diese App gar nicht kennt. Und
geschrieben wird über beide Wege — erst der gebündelte
`/UserItems/{id}/UserData`, bei einem 404 der ältere. Welcher gilt, hängt
an der Serverfassung.

**Und man konnte es nicht sehen.** Die Attrappe kannte die Route nicht;
ihr Auffangzweig antwortete auf jeden Schreibversuch mit Status 200, und
die Weiterschauen-Reihe war fest verdrahtet. Der Aufruf gelang also immer
und änderte nie etwas — genau das Bild eines kaputten Knopfes, nur aus
einem anderen Grund. Genau deshalb ist der Fehler durchgerutscht.

Sie führt jetzt echte Benutzerdatenblätter, beantwortet beide Wege und
liefert unter „Weiterschauen" nur noch, was eine gemerkte Stelle grösser
null hat. Unter `/__userdata` steht, was geschrieben wurde und wohin.

Gemessen im Simulator, nach einem Tipp auf den Eintrag:

    /UserItems/m2/UserData
    {"Played":false,"IsFavorite":false,"PlaybackPositionTicks":0,…}

Danach ist „Weiterschauen" leer und die Reihe verschwindet von der
Startseite.

**Zwei Dinge, die danach trotzdem auffallen können**, und beide sind
keine Fehler: Jellyfins „Als Nächstes" führt angefangene Folgen mit, eine
entfernte Folge kann dort wieder auftauchen. Und bei zusammengelegten
Reihen stehen auch Folgen ohne gemerkte Stelle — dort gibt es nichts zu
entfernen, und der Eintrag fehlt.

Plex bleibt unberührt: der Weg dort war in Ordnung und bekommt nur die
neue Form.

iOS und tvOS bauen, 469 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 09:30:20 +02:00
ScarriffleandClaude Opus 5 ac62e8bf19 Der Aufmacher war weg, weil die Mediathek unterwegs verlorenging
Zwei Schalter, die im selben Bildschirm untereinander stehen, ergaben
zusammen gar keinen Aufmacher mehr: „Gesehenes aus Neuzugängen
ausblenden" und eine angehakte Mediathek.

Der Grund steht in `HomeFeed.prepared`. Der Zweig für das Ausblenden baut
die betroffenen Reihen neu — und baute sie **ohne `libraryID`** neu, weil
der neue Parameter dort nicht mitgegeben war. Getroffen hat es dabei
ausgerechnet die Reihen `latest.<mediathek>`, die als einzige überhaupt
eine tragen. Danach warf der Filter des Aufmachers jede Reihe ohne
Mediathek weg, und übrig blieb nichts.

Dasselbe an zweiter Stelle: `combiningContinueRows` verlor `libraryID`
**und** `moreKey` — Letzteres ein eigener, älterer Fehler, durch den bei
zusammengelegten Weiterschauen-Reihen „Alle anzeigen" wegfiel.

**Dazu eine Regel, die ich zurücknehme.** Gestern stand hier: trifft die
Auswahl nichts, bleibt nichts. Das war als Ehrlichkeit gedacht und ist in
der Praxis der schlechtere Fehler — ein Aufmacher, der lautlos
verschwindet, wird überall gesucht, nur nicht in einer Einstellung zwei
Bildschirme weiter. Trifft die Auswahl keine einzige Reihe, gilt sie
jetzt als wirkungslos. Das deckt auch den Fall ab, dass eine gespeicherte
Kennung nach einem Serverwechsel ins Leere zeigt.

Und damit solche Kennungen nicht unsichtbar weiterwirken, werden sie beim
Öffnen des Bildschirms gegen die vorhandenen Mediatheken abgeglichen —
vorher standen sie in keiner Zeile und liessen sich nicht abwählen.

Geprüft im Simulator: „Gesehenes ausblenden" an, „Filme" angehakt — der
Aufmacher ist da und zeigt einen Film. Vorher: nichts.

iOS und tvOS bauen, 469 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 09:26:18 +02:00
ScarriffleandClaude Opus 5 3a6a6c4242 Intro und Abspann bei Jellyfin, und drei Dinge aus dem Gebrauch
**Intro überspringen gab es für Jellyfin nie.** Der Knopf, die Marken,
das automatische Überspringen, die Einstellungen dazu — alles stand seit
Langem im Player und auf dem Fernseher. Nur kam bei Jellyfin nie eine
Marke an: `JellyfinItem+Media.swift` setzte `markers: []`, weil Jellyfin
sie nicht am Titel liefert.

Seit 10.10 gibt es `/MediaSegments/{itemId}`, gefüllt von einem Anbieter
im Server wie dem Intro-Skipper. Die Anfrage läuft **neben** dem Abruf
des Titels, nicht danach — sonst wartete die Detailseite zweimal.

Zwei Dinge, die sonst eine Stunde kosten: `includeSegmentTypes` muss
**zweimal** stehen, nicht einmal mit Komma (mit Komma antwortet Jellyfin
mit null Treffern), und die Abfrage wirft nicht. Ein Server ohne Anbieter
antwortet leer, einer vor 10.10 mit einem 404 — und ein Film ohne Intro
ist kein Fehler, sondern der Normalfall.

Geprüft im Simulator: die Attrappe meldet jetzt Segmente, und im Player
steht „Intro überspringen".

---

**„Aus Weiterschauen entfernen".** Eine halbe Stunde gesehen und keine
Lust mehr — der Titel ist weder gesehen noch ungesehen, er soll nur nicht
ewig oben stehen. Beide Server können es, auf verschiedenen Wegen:

- **Plex** hat einen eigenen: `PUT /actions/removeFromContinueWatching`.
  **Nicht** `/:/unscrobble` — das setzt zugleich auf ungesehen.
- **Jellyfin** hat keinen. Was es gibt, ist das Benutzerdatenblatt; die
  Liste führt, was eine gemerkte Stelle grösser null hat. Also wird das
  Blatt gelesen, bekommt eine Null und geht so, wie es war, zurück.
  **Erst lesen, dann schreiben**, weil dort auch „gesehen", „Favorit" und
  die Bewertung stehen — wer seinen Favoriten verliert, weil er einen
  Film aus einer Liste genommen hat, sucht den Grund nie dort.

Der Eintrag erscheint nur, wenn es eine gemerkte Stelle gibt.

---

**Die Mediatheken für den Aufmacher taten zu wenig.** Die Regel „Reihen
ohne Mediathek bleiben immer drin" war freundlich gemeint und falsch: bei
„nur Filme" stand weiter eine Serie oben, weil sie aus „Weiterschauen"
kam. Gemessen und nicht vermutet — es lag nicht am Zeitfenster der
Auswahl, wie zuerst angenommen.

Jetzt gilt: ist etwas gewählt, speist sich der Aufmacher **nur** daraus.
Wer Weiterschauen oben haben will, wählt keine Mediathek. Der Hinweis
unter der Auswahl sagt das auch.

---

**Der „Bearbeiten"-Knopf stand über allem und tat dort nichts.** Er gilt
nur für die Reihen; jetzt steht er an deren Überschrift.

---

Die Attrappe kann drei Dinge mehr, sonst wäre nichts davon prüfbar:
Segmente, `parentId` bei den Neuzugängen (vorher stand in jeder Mediathek
dasselbe) und damit überhaupt eine Mediathekenauswahl, die man ansehen
kann.

iOS und tvOS bauen, 468 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 08:47:06 +02:00
ScarriffleandClaude Opus 5 dc93d6e22e Der Aufmacher lässt sich auf Mediatheken einschränken
Mehrfachauswahl in den Einstellungen unter „Startseite": welche
Mediatheken den Aufmacher speisen dürfen. Haken statt Auswahlliste —
eine Liste könnte nur eine, und „keine" wäre dort ein Eintrag, den man
versehentlich trifft.

Dafür musste eine Reihe sagen können, woher sie kommt: `MediaRowGroup`
trägt jetzt eine `libraryID`. Jellyfin setzt sie an den „Neu in …"-Reihen
— die baut die App ohnehin je Mediathek.

Zwei Regeln, und beide sind Entscheidungen, keine Nebenwirkungen:

**Leer heisst alle.** Wer nichts ankreuzt, meint „mir ist es gleich",
nicht „nimm nichts". Ein leerer Aufmacher wäre der Fehler, den niemand
mit dieser Einstellung in Verbindung brächte.

**Reihen ohne Mediathek bleiben immer drin.** Weiterschauen, Als Nächstes
und Favoriten gehen quer durch alle, und aus welcher Mediathek eine
angefangene Folge stammt, sagt der Server an keiner Stelle. Sie
wegzuwerfen hiesse, jemandem das Angefangene zu nehmen, weil er
„Musikvideos" abgewählt hat. Der Hinweis unter der Auswahl sagt das auch
dem, der nicht in den Quelltext sieht.

Eingeschränkt wird **nur der Aufmacher**, nicht die Startseite: die
Reihe selbst bleibt stehen. Wer sie ganz loswerden will, blendet sie aus
— das ist eine andere Frage und hat ihren eigenen Schalter.

Fünf Tests an `HeroSelection.rows(_:limitedTo:)` halten die Regeln fest,
darunter der Fall einer gespeicherten Kennung, die es nicht mehr gibt.

**Plex bleibt aussen vor:** dort kommen die Reihen als fertige Hubs vom
Server, und `librarySectionID` wird in `PlexHub` nicht gelesen. Die
Auswahl schränkt dort also nichts ein. Nachzuholen, wenn Plex wieder
dran ist.

iOS und tvOS bauen, 468 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 08:24:09 +02:00
ScarriffleandClaude Opus 5 3f680c7426 Gesehen und Favorit direkt an der Kachel — und die Pinch-Geste greift wieder
Drei Dinge aus dem Gebrauch.

**Das Kontextmenü einer Kachel trug genau einen Eintrag.** „Abspielen",
sonst nichts; der Kommentar daneben nannte es seit Längerem den
natürlichen Ort für „Als gesehen markieren". Jetzt steht es dort —
zusammen mit Favorit und Sternen, soweit der Server sie kennt. Es sind
dieselben Einträge wie im Menü der Detailseite: `ItemActionEntries` wird
von beiden benutzt, statt sie zweimal zu beschreiben.

Zwei Dinge daran sind nicht offensichtlich. Der Inhalt eines
Kontextmenüs lebt nur, solange das Menü offen ist — ein `@State` darin
könnte nichts anzeigen, was nach dem Tippen passiert. Ausgeführt wird
deshalb von der Kachel, die länger lebt, und dort hängt auch die
Fehlermeldung. Und danach muss die Seite neu laden: eine abgehakte Folge
gehört nicht mehr in „Weiterschauen", und der Fortschrittsbalken stimmte
sonst nicht mehr. Dafür gibt es `flixyItemChanged` in der Umgebung —
sonst müsste der Rückweg durch jede Zwischenebene gereicht werden.

**Die Pinch-Geste griff fast nie.** Sie hing an der Bildfläche, und über
der liegen die Sprungflächen — und sobald man einmal getippt hat, die
ganze Bedienung. Eine Zwei-Finger-Geste bildet sich aber nur, wenn
**beide** Finger auf der Ansicht landen, die sie trägt; in dem schmalen
Streifen dazwischen gelang das kaum. Sie hängt jetzt als
`simultaneousGesture` am ganzen Player: sie erkennt mit, statt
dazwischenzugehen, und Tippen, Doppeltippen und Scrubber bleiben
unberührt.

Gemessen im Simulator, Querformat auf einem Pro Max, mit einer echten
Zwei-Finger-Geste:

    auseinander   956 × 440 pt, Rand 0,0 / 0,0
    zusammen      782 × 440 pt, Rand 87,0 / 87,0

**Der Drehknopf zeigte ein Telefon.** Das las sich als Zustandsanzeige —
„so liegt es" — statt als Knopf. Zwei Pfeile im Kreis sagen, was
passiert.

iOS und tvOS bauen, 463 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 08:19:19 +02:00
ScarriffleandClaude Opus 5 46b0117984 Füllend heisst füllend — der schwarze Streifen unten ist weg
Im Füllmodus wurde die Bildebene unten um den Sicherheitsrand eingerückt,
damit die Untertitel nicht unter die Bildschirmkante geraten. Das stand
dort unbedingt — auch wenn gar kein Untertitel lief. Übrig blieb dann ein
schwarzer Streifen unter einem Bild, das gerade füllen soll: genau das
Gegenteil dessen, wofür der Knopf da ist.

Der Einzug hängt jetzt daran, ob überhaupt eine Untertitelspur gewählt
ist. Ohne Untertitel reicht das Bild bis an alle vier Kanten, mit
Untertiteln bleibt die Schrift gerettet.

Gemessen auf einem Pro Max, füllend, ohne Untertitel:

    hochkant    440 × 956 pt   Rand 0,0 / 0,0 / 0,0 / 0,0
    quer        956 × 440 pt   Rand 0,0 / 0,0 / 0,0 / 0,0

`PlayerModel.subtitleBottomInset` fällt weg. Die Eigenschaft beschrieb
diese Rechnung, las sie aber niemand — die Ansicht rechnete selbst. Eine
Beschreibung, die nichts beschreibt, zeigt beim nächsten Mal in die
falsche Richtung.

**Nicht geprüft:** wie es mit eingeschalteten Untertiteln aussieht. Die
Attrappe liefert keine Untertitelspur, und ohne eine lässt sich der Fall
im Simulator nicht herstellen. Der Weg ist unverändert geblieben, aber
belegt ist er damit nicht.

iOS und tvOS bauen, 463 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 07:55:50 +02:00
ScarriffleandClaude Opus 5 9e7f3393b4 DTS, TrueHD und Vorbis gehen jetzt an den Server statt an VLC
Bisher entschied der Decider: steht nur die Schachtel im Weg, wird
umgepackt — stimmt ein Codec nicht, geht es an VLC. Das war beim Ton zu
grob gedacht.

Beim Umpacken nennt das Geräteprofil dem Server, was ankommen darf:

    "VideoCodec": "h264,hevc"
    "AudioCodec": "aac,ac3,eac3,mp3"

Trifft ein Strom eine dieser Angaben, kopiert der Server ihn; trifft er
sie nicht, rechnet er **genau diesen einen** um. Eine Matroska mit HEVC
und DTS braucht also kein VLC: das Bild geht Byte für Byte durch, und nur
die Tonspur wird nach AC3 oder AAC kodiert. Das kostet Tonqualität und
sonst nichts.

Die Bedingung heisst deshalb nicht mehr „nur die Schachtel", sondern
„das Bild bleibt": Container und Ton dürfen im Weg stehen, der
Videocodec nicht. Dazu musste die Prüfung der Schachtel aufhören, sich
immer zu stellen — eine MP4 mit DTS stand mit einem reinen Tonhindernis
da und fiel trotzdem durch die Liste umpackbarer Container.

**Der grössere Gewinn liegt auf dem Apple TV.** Dort gibt es VLC nicht,
und bisher landete eine Datei mit DTS-Ton beim vollen Neurechnen — Bild
inbegriffen, obwohl mit dem Bild nichts war. Jetzt bleibt das Bild
unberührt. Auf iPhone, iPad und Mac verhält sich derselbe Film jetzt
gleich, und zwar über AVPlayer: mit Bild-in-Bild, AirPlay und
Sperrbildschirm, die es auf dem VLC-Weg nie gab.

Geprüft im Simulator gegen die Attrappe mit `--vlc` (meldet DTS): der
Film läuft, und in der Leiste steht jetzt der AirPlay-Knopf, der vorher
fehlte — der sichtbarste Beleg dafür, dass AVPlayer spielt und nicht VLC.

„Umgepackt" und „umgepackt, Ton umgerechnet" sind in den technischen
Infos und in der Diagnose auseinandergehalten. Ein Weg, der den Ton neu
kodiert, darf nicht „ohne Neukodierung" heissen.

Was bleibt bei VLC: nicht unterstützte **Bild**codecs, allen voran AV1,
und Dateien, die der Server noch nicht untersucht hat. Dafür gäbe es nur
das volle Neurechnen — eine eigene Entscheidung, nicht diese.

iOS und tvOS bauen, 463 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 07:46:39 +02:00
ScarriffleandClaude Opus 5 3ed682dd00 Der Sicherheitsrand am Bild war falsch — er schützte vor nichts
Eingepasst bekam das Bild ein Polster in Grösse des Sicherheitsrandes,
damit es nicht unter die Dynamic Island läuft. Nachgerechnet hält das
nicht.

Querformat auf einem Pro Max: 956 × 440 Punkt. Ein Bild in 16∶9 wird bei
voller Höhe 782 Punkt breit — es bleiben 87 Punkte Balken auf jeder
Seite, und die Island braucht 62. Das Bild kommt ihr von selbst nicht
nahe. Das Polster hat also nichts abgewehrt und dabei vierzig Punkte
Höhe gekostet, also jedes zehnte Bildpunktband:

    vorher   695,7 × 400 pt   Balken 130,0 / 130,3
    jetzt    765,3 × 440 pt   Balken  95,3 /  95,3

Beides über VLC gemessen, dieselbe Datei, dieselbe Lage.

Erst was breiter ist als der Schirm (2,17∶1) läuft an die Kanten — bei
21∶9 über die ganze Breite, und dann liegt ein Streifen hinter der
Island. Dafür ist der Zoomknopf da. Ein Film, der nie an den Rand darf,
wäre der schlechtere Tausch: er kostet bei *jeder* Datei Höhe, um bei
wenigen einen Streifen zu retten.

`EdgeInsets.symmetrical` fällt damit weg, samt seiner fünf Tests. Es
hatte genau eine Aufgabe — ein unsymmetrisches Polster mittig zu halten
—, und das Polster gibt es nicht mehr. Eine geprüfte Funktion, die
niemand ruft, ist trotzdem nur Ballast.

iOS und tvOS bauen, 460 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 07:33:18 +02:00
ScarriffleandClaude Opus 5 b0db4f8f5f Bei VLC sass das Bild nie richtig — und der Knopf wirkte nie
Zwei Fehler, die dieselbe Ursache haben: alles, was über VLC läuft, ging
an der Bildebene vorbei, an der die App gedreht hat.

**Das Bild sass nicht mittig.** Nicht ein paar Punkte daneben — gemessen
eine Anzeigefläche von 1200×480 Punkt in einer Zeichenfläche von 711×400,
unten rechts angeschlagen, im falschen Seitenverhältnis. Der Grund steht
im Vertrag von `VLCDrawable`: VLCKit fragt die Grösse **einmal** beim
Anlegen ab und überlässt seine Anzeigefläche danach der
Autoresizing-Maske. Eine Maske hält aber den Unterschied fest, nicht das
Verhältnis — was beim Anlegen zu gross war, bleibt für immer um denselben
Betrag zu gross. Und angelegt wird sie mitten in der Drehung ins
Querformat, während SwiftUI sie einblendet: also regelmässig falsch.

Die Zeichenfläche ist jetzt eine eigene Klasse, die ihrer Anzeigefläche
bei jedem Layout wieder die eigene Grösse gibt. Danach gemessen: 695,7 ×
400 Punkt, Rand links 130,0, rechts 130,3 — mittig.

**Der Knopf für einpassen/füllen tat nichts.** Er schrieb `videoGravity`
auf eine `AVPlayerLayer`, die bei VLC gar nicht im Baum hängt. Die
Einstellung geht deshalb jetzt durch die Engine: `setVideoFit` am
Protokoll, beim Systemplayer eine Voreinstellung, die nichts tut — dort
macht es die Bildebene —, bei VLC `videoFitMode`. `smaller` heisst bei
libvlc „fit inside", `larger` „fit outside".

Gemessen, im Querformat auf einem Pro Max, über VLC:

    eingepasst   695,7 × 400 pt   Rand l/r 130,0/130,3   o/u 20,0/20,0
    füllend      935,3 × 440 pt   Rand l/r  10,3/ 10,3   o/u  0,0/ 0,0

**Zurückgenommen:** im Kommentar an `videoFit` stand, ein `didSet` auf
einer `@Observable`-Klasse werde nicht gemeldet. Das stimmt nicht —
`HomeLayoutSettings` lebt davon. Ein Kommentar, der einen falschen
Mechanismus behauptet, schickt den nächsten in die falsche Richtung;
jetzt steht der belegte Grund dort.

Dazu, damit dieser Weg überhaupt prüfbar ist: die Attrappe meldet mit
`--vlc` DTS-Ton. AVFoundation kann das nicht, also fällt die App auf
VLCKit zurück. Die Beispieldatei bleibt dieselbe — geprüft wird der Weg,
nicht der Ton. Ohne diesen Schalter ist der VLC-Zweig im Simulator gar
nicht zu erreichen, und genau deshalb lagen beide Fehler so lange da.

Ebenfalls dabei: `EdgeInsets.symmetrical` mit fünf Tests und das Polster
im Player, das damit nicht mehr verschiebt statt einrückt.

iOS und tvOS bauen, 465 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 21:05:57 +02:00
ScarriffleandClaude Opus 5 51abcabb27 Die Rechnung nachgetragen — und eine falsche Erklärung zurückgenommen
Eine unabhängige Gegenprüfung (zweiunddreissig Agenten, jede Behauptung
von Skeptikern angegriffen) bestätigt die Ursache und rechnet alle vier
gemessenen Zahlen nach:

    scrollbarer Teil  421,0   (8 Zeilen à 36,33 + Überschrift 22,33
                               + 8 Zwischenräume à 12 + 12 unten)
    starrer Rest      137,6   (Kopfzeile 48,33 + Trenner 1
                               + Kontozeile 64,27 + 2 × 12 aussen)
    zusammen          558,6   gemessen 558,67
    zentriert         (420 − 558,67) / 2 = −69,335   gemessen −69,33

Die Rechnung steht jetzt im Quelltext, damit beim nächsten Eintrag in der
Leiste jemand nachsehen kann, wie viel Luft noch ist: starr bleiben 137,6
von 420.

**Und eine Berichtigung.** Ich hatte den Zwischenwert 620,67 einer
Rückkopplung zugeschrieben — gemessener Rand geht über `topInset` in
einen `safeAreaInset` und verändert den Rand. Das stimmt nicht. Die
Differenz von zweiundsechzig kommt schlicht aus
`.padding(.top, statusBarHeight)` an der Seitenleiste, das während der
Drehung noch den Hochformatwert trug.

Die Umstellung auf den Fensterwert bleibt richtig und nützlich — sie
spart eine Layoutrunde. Sie war nur nicht die Reparatur, und der
Kommentar sagte etwas anderes. Ein Kommentar, der einen falschen
Mechanismus behauptet, ist schlimmer als keiner: er schickt den nächsten
in die falsche Richtung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:32:56 +02:00
ScarriffleandClaude Opus 5 5f635c699a Die Seitenleiste war zu hoch — und der Player ging deshalb hinaus
Dreimal habe ich an den Rändern geschraubt, und dreimal war es nicht das.
Der Fehler sass in der Navigation und zeigte sich im Player.

**Gemessen statt vermutet.** Auf einem iPhone Pro Max im Querformat:

    Fenster  956×440, Rand 0,62,62,20  →  erwartet 832×420
    Wurzel   832×558,67 @62,−69,33     →  138,67 zu hoch

Der Wurzelbaum ist zu gross für den Vorschlag und wird darin zentriert;
(420 − 558,67) / 2 ergibt exakt −69,33. Der Player liegt als Überlagerung
darin und erbt den Kasten — oben fielen neunundsechzig Punkte heraus,
unten neunundvierzig. Genau dort sitzen Kopfzeile und Zeitangaben.

**Die Ursache:** `FlixySidebar` war eine `VStack` ohne Bildlaufansicht,
mit dem Kommentar „die Einträge sind wenige und passen". Auf einem iPad
und im Hochformat stimmt das. Ein grosses iPhone ist quer
vierhundertvierzig Punkte hoch, und die Leiste braucht mit Kopf, Gruppen,
Trenner und Kontozeile gut fünfhundertdreissig. Eine `VStack` schrumpft
dann nicht — sie überragt, der `HStack` nimmt das Maximum, und alles
darüber wächst mit.

Deshalb war es nur dort zu sehen: nur auf einem grossen iPhone ist die
Anordnung quer `regular`, und nur dann gibt es überhaupt eine
Seitenleiste. Auf einem 17 Pro ist quer `compact` — dort stimmte jede
Messung, und genau deshalb konnte ich es dreimal nicht nachstellen.

Jetzt scrollt die Leiste, wenn sie muss (`basedOnSize`). Wo die Einträge
passen, sieht man keinen Unterschied. Nachgemessen: Wurzel 832×420,
Rahmen des Players 956×440 @0,0 — deckungsgleich mit dem Fenster.

**Mitgenommen:** `statusBarHeight` kam aus `proxy.safeAreaInsets.top` und
ging über `topInset` in einen `safeAreaInset` am selben Baum zurück. Das
Layout lief dadurch in Runden auf die richtige Grösse zu, jede Runde
zweiundsechzig Punkte. Der Wert kommt jetzt aus dem Fenster, das ihn ohne
Rückkopplung kennt — eine Runde statt drei.

`ScreenSafeArea` liegt dafür nicht mehr beim Player, sondern bei den
Modellen: es beantwortet jetzt für zwei Stellen dieselbe Frage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 17:15:50 +02:00
ScarriffleandClaude Opus 5 7bcef03c25 Eingepasst heisst ganz zu sehen — und der Rahmen wird mitgemessen
**Das Bild bleibt im sicheren Bereich, wenn es eingepasst ist.** Vorher
lief es bis an die Bildschirmkante und damit unter die Dynamic Island —
im Querformat frisst sie eine Ecke des Films. „Einpassen" und „ein Stück
fehlt" schliessen sich aus. Das kostet quer gut hundert Punkte Breite;
wem das zu klein ist, legt mit zwei Fingern auf **füllen** um, wo es
bewusst bis an die Kanten läuft und nur der Untertitel ausgespart bleibt.

**Und die Diagnosezeile misst jetzt den Rahmen mit.** Das ist die Frage,
an der alles hängt, und ich habe sie zweimal übersprungen: ist der Rahmen
grösser als der Bildschirm, liegt es nicht an den Rändern — dann wird der
Player in einen zu grossen Kasten gelegt und mittig darin ausgerichtet,
und oben wie unten fällt gleich viel heraus. Genau so sieht es aus, wenn
die Kopfzeile fehlt **und** die Zeitangaben fehlen, während die Knöpfe in
der Mitte stehen.

Der Verdacht, warum ich es nie nachstellen konnte: die Bildschirmfotos
sind 2796×1290, also ein Pro Max. Der ist im Querformat `regular` und
nimmt damit in `RootView` einen **anderen Zweig** — Seitenleiste statt
Leiste unten, mit `safeAreaInset` und `ignoresSafeArea(.container)`. Mein
Simulator ist ein 17 Pro und läuft quer über den anderen Zweig. Belegt
ist das noch nicht; die Zeile wird es sagen.

Im Simulator steht dort „Rahmen 874×402 @0,0 · Schirm 874×402" — gleich
gross, Ursprung in der Ecke, alles liegt drin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:25:13 +02:00
ScarriffleandClaude Opus 5 c7b901b103 Der Player sagt jetzt, was er über seinen Platz weiss
Zweimal habe ich behauptet, die Ränder seien behoben, und zweimal sassen
die Knöpfe weiter ausserhalb. Im Simulator steht dort „SA 0,62,62,20 ·
Schirm 874×402" und alles liegt drin — nachstellen lässt es sich hier
also nicht. Damit ist Raten der falsche Weg.

**Eine Zeile im Entwicklerbau**, unter dem Schieberegler: die Ränder, wie
sie ankommen, die Bildschirmgrösse daneben und der Skalierungsmodus.
Sitzen die Knöpfe daneben, gibt es genau drei Möglichkeiten — die Ränder
kommen als null an, sie kommen falsch an, oder sie stimmen und der Rahmen
ist grösser als der Schirm. Von aussen sehen alle drei gleich aus. Die
Zeile unterscheidet sie.

**Und eine Härtung, die beim Nachlesen auffiel.** `ScreenSafeArea` suchte
nur die Szene, die `foregroundActive` ist. Genau die gibt es in zwei
Momenten nicht: während einer Drehung und kurz nach dem Zurückholen der
App. Wer dann fragt, bekommt `nil`, daraus werden Ränder von null — und
die bleiben stehen, bis sich die Fläche wieder ändert. Die Knöpfe sitzen
dann dauerhaft im Home-Indikator, obwohl die Rechnung stimmt.

Ein Fenster mit den richtigen Rändern ist in dem Moment trotzdem da, es
ist nur nicht „aktiv". Jetzt der Reihe nach: aktiv, sonst sichtbar, sonst
irgendeines. Dazu wird beim Zurückholen der App nachgefasst — eine
Drehung im Hintergrund ändert die Fläche, ohne dass die Ansicht davon
etwas merkt.

Ob das der Fehler war, weiss ich nicht. Die Zeile wird es sagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:59:04 +02:00
ScarriffleandClaude Opus 5 41719595b9 Mediatheken nach oben — und das Regal sagt endlich, was es sieht
**Die Mediatheken sind Reiter geworden.** Sie standen unten auf der
Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen
vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem
die Vorschläge egal sind. Damit fällt die Reihe unten weg: zwei Wege zum
selben Raster sind auf einem Fernseher kein Komfort, sondern eine Frage,
welcher der richtige ist.

Die Leiste liest sich jetzt „Start · Filme · Serien · Gemischtes ·
Suchen" und wird von tvOS mittig gehalten, egal wie viele Mediatheken
dazukommen. Die Stelle der Suche bestimmt Apple, nicht diese Datei:
`role: .search` setzt sie dorthin, wo sie in jeder anderen App auch
steht, und gibt ihr die Lupe.

**Und eine Statuszeile fürs Regal.** Bleibt es leer, gibt es genau drei
Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, oder sie ist
da und das System fragt trotzdem nicht. Von aussen sehen alle drei gleich
aus — nämlich nach dem grossen Plakat aus dem Asset-Katalog. Nach zwei
Runden Vermuten steht unten links jetzt, welcher Fall es ist: „3 Reihen,
9 Einträge, zuletzt 21:55" oder eben „Keine App-Gruppe".

Nur im Entwicklerbau (`#if DEBUG`) und als Überlagerung, nicht als letzte
Zeile der Liste: dorthin müsste man erst an allen Reihen vorbeiscrollen,
und wer nachsieht, warum das Regal leer ist, soll es sofort sehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 21:56:31 +02:00
ScarriffleandClaude Opus 5 08514eb564 Und iOS wieder gebaut — TVServices gibt es dort nicht
Mein Fehler: `TopShelfWriter` liegt in `Flixy/Session/`, und den Ordner
übersetzen beide Ziele. Mit `import TVServices` brach damit das
iPhone-Ziel ab.

Die ganze Datei steht jetzt in `#if os(tvOS)`. Ein `#if` um den einen
Import hätte nicht genügt: der Abhängigkeits-Sucher von Clang prüft ihn
trotzdem und bricht mit „Unable to resolve module dependency: TVServices"
ab, ohne den Fernseher zu erwähnen — dieselbe Falle wie vorhin bei
VLCKit.

Committet hatte ich vorher, ohne iOS zu bauen. Das war der eigentliche
Fehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 21:51:50 +02:00
ScarriffleandClaude Opus 5 47c93d89bb Niemand hat dem System gesagt, dass es neu fragen soll
Das Regal zeigte weiter das grosse Plakat aus dem Asset-Katalog, obwohl
die Datei daneben längst drei Reihen enthielt. Der Grund steht in Apples
eigenem Header, zwei Zeilen über der Methode, die wir überschreiben:

> If the completion handler is not called in a timely manner or is called
> with empty content the system will display the static image instead.

Genau das war zu sehen. Und darunter steht, was fehlte:

> + (void)topShelfContentDidChange;
> Can be called from an application or extension to tell the system that
> your Top Shelf content has changed.

Die Erweiterung kann sich nicht selbst anstossen — sie läuft nur, wenn
sie gefragt wird. Gefragt wird sie, wenn die App es sagt. Die App sagt es
jetzt, unmittelbar nachdem sie den Abzug geschrieben hat.

Ausgeschlossen, bevor ich hier landete, und zwar nachgemessen statt
vermutet: das System kennt die Erweiterung (`pluginkit` listet sie unter
`com.apple.tv-top-shelf`), die Hauptklasse ist im Binärcode auffindbar
(`_TtC13FlixyTopShelf16TopShelfProvider`), die Gerätefamilie stimmt, und
die Datei liegt mit drei Reihen in der App-Gruppe.

Nicht nachgewiesen: dass es jetzt zeichnet. Die Erweiterung startet nur,
wenn das System sie fragt, und das tut es nur, wenn die App im Menü
oben steht und fokussiert ist — beides kann ich im Simulator nicht
auslösen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 21:51:01 +02:00
ScarriffleandClaude Opus 5 a1576921fd Das Regal hatte seine eigene Transportsicherheit
Die Plakate blieben weg, und der Grund stand nicht im Code, sondern
daneben: eine App-Erweiterung hat ihre **eigene** Info.plist, und damit
ihre eigene Transportsicherheit. Die Ausnahme fürs lokale Netz, die in
der Info.plist der App seit jeher steht, gilt dort nicht.

Ein Jellyfin im Heimnetz antwortet auf blankem HTTP. Jede Bildanfrage der
Erweiterung lief damit gegen ATS — lautlos, ohne Fehler, ohne Eintrag.
Das Regal blieb leer und sah aus wie „geht halt nicht".

Dazu die richtige Plakatgrösse: 666×1000 statt 404×606, Apples Mass für
die doppelte Auflösung. Für beide Stufen dasselbe Bild — ein Apple TV
zeichnet das Regal ohnehin doppelt aufgelöst, und zwei Adressen abzulegen
hiesse, dem Server zwei Grössen desselben Plakats abzuverlangen, damit
eine davon nie gebraucht wird.

Nicht nachgewiesen: dass es jetzt zeichnet. Das Regal erscheint nur,
wenn die App in der **obersten** Reihe des Menüs steht und fokussiert
ist, und den Fokus kann ich im Simulator nicht bewegen — tvOS nimmt keine
Touch-Eingaben, und an die Simulator-App komme ich aus dieser Umgebung
nicht heran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 21:16:04 +02:00
ScarriffleandClaude Opus 5 6373566d1c Suchen am Fernseher — und die Leiste, die es dafür braucht
Das Backend konnte die Suche längst; es fehlte die Oberfläche.

**`.searchable`, keine eigene Tastatur.** tvOS macht daraus einen eigenen
Bildschirm mit Rastertastatur oben und Ergebnissen darunter — die
Bedienung, die jeder von Apples Apps kennt. Etwas Eigenes zu bauen hiesse,
sie nachzuahmen und dabei die Diktierfunktion zu verlieren.

**Herausgelöst statt kopiert**, wie schon bei Start und Mediathek:
`SearchFeed` trägt das Entprellen, die Mindestlänge und die zuletzt
gesuchten Begriffe. Drei Stellen, an denen es nicht offensichtlich ist —
und die dritte ist die wichtigste: geprüft wird auf `Task.isCancelled`
und nicht auf `CancellationError`, weil eine abgebrochene Netzanfrage als
Übertragungsfehler zurückkommt. Die naheliegende Prüfung lief ins Leere,
und jeder Tastendruck leerte kurz die Liste.

**Und erst jetzt eine Reiterleiste.** Bis hierher gab es genau einen
Bildschirm, und eine Leiste mit einem Eintrag wäre Zierrat gewesen. Der
Suchreiter trägt `role: .search`: tvOS stellt ihn dadurch ans Ende und
gibt ihm die Lupe, dieselbe Stelle wie überall sonst. Keinen dritten
Reiter für die Mediatheken — die stehen als Reihe auf der Startseite, und
ein Reiter verspräche einen eigenen Bereich, hinter dem dasselbe Raster
läge.

Der offene Reiter wird ausdrücklich gehalten, nicht dem System
überlassen: ein Link aus dem Regal über der App führt zur Startseite, und
stünde gerade die Suche offen, passierte sichtbar nichts.

Nebenbei bekommt die iOS-Suche einen Knopf zum Vergessen des Verlaufs.
Wischen zum Löschen gab es dort, am Fernseher gibt es das nicht — und
eine Liste, die sich nur auf einer Plattform leeren lässt, ist keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 20:30:55 +02:00
ScarriffleandClaude Opus 5 5ee7bb4e6c Lila statt Mint — die Farbe der Knöpfe passt zum Icon
Die Akzentfarbe war Mint. Eine hübsche Farbe, die mit der Marke nur
nichts zu tun hatte: das Plakat auf dem Homescreen ist violett, und die
Knöpfe darin waren grün.

`lilac` (#9B8AD4) ist die entsättigte Schwester desselben Violetts. Auf
dem dunklen Grund erreicht sie 6,5:1 — die Palette verlangt 4,5:1, und
ein Test hält das fest.

Die Farbe steht in der Palette, nicht nur als Vorgabe: wer lieber Mint
mag, findet es weiterhin in den Einstellungen. Geändert wird damit nur,
womit eine frische Installation anfängt.

Der Test, der die Vorgabe festhält, ist mitgeändert — genau dafür steht
er da: ein Umsortieren der Palette soll sie nicht still verändern, ein
bewusster Wechsel schon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 20:27:20 +02:00
ScarriffleandClaude Opus 5 e801502c7a Ein Regal über der App — und ein Weg aus dem Player heraus
**Das Regal.** Wer im tvOS-Menü über der App steht, sah bisher das Icon
noch einmal gross. Jetzt stehen dort „Weiterschauen", „Als Nächstes" und
die Neuzugänge als Plakate, mit Fortschrittsbalken; ein Druck spielt ab,
ein Wisch nach unten öffnet die Detailseite.

**Warum es dafür eine App-Gruppe braucht.** Das Regal zeichnet eine
eigene Erweiterung, also ein anderer Prozess mit eigener Sandbox — und
tvOS ruft sie auf, während die App **nicht** läuft, mit weniger als einer
Sekunde Zeit. Kein Netzabruf, keine Anmeldung, kein Blättern. Also legt
die App beim Laden der Startseite hin, was das Regal braucht, und sonst
nichts: Titel, Bildadresse, Kennung, Fortschritt. Eine Datei, ein
`JSONDecoder`, fertig.

Die Bildadressen tragen den Zugangsschlüssel, weil Jellyfin ihn für
Bilder verlangt und die Erweiterung ihn sonst nirgends herbekäme. Die
Datei liegt in der App-Gruppe — erreichbar für die App und ihre
Erweiterungen, für sonst niemanden. Der Schlüsselbund bleibt, wo er ist.

**Und der Fehler, den du gefunden hast.** Solange der Stream noch
ausgehandelt wurde, gab es keinen Weg zurück: schwarzes Bild, Kreisel,
und der Menü-Knopf tat nichts. `AVPlayerViewController` fängt ihn nämlich
selbst ab — aber erst, wenn er einen Player mit einem Item hat, und genau
den gibt es in dem Moment noch nicht. Der Griff liegt jetzt eine Ebene
darüber, wo es ihn vom ersten Bild an gibt. Doppelt ist er nicht: greift
Apples Player zu, kommt der Befehl dort gar nicht mehr an.

**Was am Gerät noch offen ist.** Die App-Gruppe muss im Entwicklerkonto
eingetragen sein, sonst bleibt das Regal leer — im Simulator legt das
System sie von selbst an, auf einem echten Apple TV nicht. Geprüft ist,
dass die App die Datei schreibt (drei Reihen, richtige Adressen) und dass
tvOS das Schema kennt. Dass das Regal beim Darüberstehen auch zeichnet,
lässt sich ohne Fernbedienung nicht auslösen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 20:14:38 +02:00
ScarriffleandClaude Opus 5 e2714e058e Ein Untertitelmenü statt zweier — und Staffel statt S
Zwei Sachen, die am laufenden Player auffielen.

**Zwei Sprechblasen nebeneinander.** Eine war Apples eingebautes
Untertitelmenü, eine unseres — und welche was tut, sah man erst nach dem
Drücken. Schlimmer als die Doppelung war, was dahinterlag: Apples Menü
verstellt die **Systemeinstellung** für Untertitel, während unsere Engine
`AVTextStyleRule`s aus `SubtitleAppearance` setzt. Zwei Stilregler, von
denen nur einer wirkt.

Apples Knopf ist jetzt abgeschaltet — `allowedSubtitleOptionLanguages` auf
eine leere Liste, der einzige dokumentierte Hebel dafür; die Auswahl von
Hand bleibt unberührt. Unser Menü trägt die Spuren oben und darunter
Schriftgröße und Darstellung als Untermenüs. Tiefer geht es nicht: die
Transportleiste zeigt genau eine Ebene.

**Und das Tonmenü verschwindet, wenn Apple es besser kann.** Wird die Datei
unverändert gespielt und stimmt die Spurzahl, liegen alle Spuren im Stream
— dann zeigt Apple sie selbst, und zwei Knöpfe wären dasselbe zweimal. Nur
beim Umrechnen liegt im Stream genau eine, und dann führt allein unseres
weiter.

**„S7 E2" heisst jetzt „Staffel 7 · Episode 2"**, wo Platz ist: in der
Titelzeile des Players und im Aufmacher am Fernseher. Auf einer Kachel
bleibt die kurze Form — dort ist eine Zeile breit, und ausgeschrieben
verdrängte sie den Titel.

Mitgewandert in die geteilte Schicht sind die Namen der Stilstufen: sie
standen bei der Einstellungsansicht und wurden damit nur vom Telefon
übersetzt. Zwei Listen gleich zu halten ist genau die Art Fehler, die erst
auffällt, wenn jemand „Gelb auf Schwarz" wählt und „Weiss auf Schwarz"
bekommt.

Der Stil wirkt sofort und ohne Neuaufbau, und die Haken im Menü wandern
mit: dafür steht er in der Momentaufnahme, obwohl der Player selbst ihn
nicht braucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 20:05:28 +02:00
ScarriffleandClaude Opus 5 12bfc9e530 Abspielen am Fernseher — mit Apples Player und unseren Spuren
Schritt sieben. Wiedergabe, Spurwahl, Titelzeile, „Intro überspringen".

**Die schwierige Stelle war die Spurwahl.** Unsere Ton- und Untertitelwahl
geht als `audioStreamIndex` an Jellyfin und verlangt bei einem Transcode
einen **neuen Stream**. Apples eingebauter Spurwähler zeigt dagegen die
Auswahlgruppen des **laufenden** Streams — beim Transcode genau eine. Er
ist die richtige Oberfläche für die falsche Liste. Unsere Menüs sitzen
deshalb über `transportBarCustomMenuItems` daneben; `customInfoViewControllers`
wäre falsch, denn die Tonspur ist ein Bedienelement der Wiedergabe und kein
Begleitmaterial.

**Der Neuaufbau wird angesagt, nicht erklärt.** Jeder Eintrag, der Sekunden
mit schwarzem Bild kostet, trägt das als Untertitel — vorher, nicht
hinterher. Wer drückt, sieht den Haken sofort wandern, weil das Menü sich
schliesst und darin kein Platz für einen Kreisel ist. Das schwarze Bild
deckt eine Überlagerung in `contentOverlayView` zu, die hinter den
Bedienelementen liegt und keinen Fokus nimmt — `customOverlayViewController`
täte beides nicht und machte die Fernbedienung währenddessen tot. Und
solange umgebaut wird, sind alle Einträge gesperrt: sonst räumt `restart()`
eine Engine ab, die eine andere gerade baut.

**Die Regel dahinter ist jetzt prüfbar.** „Wann kostet ein Spurwechsel einen
Neuaufbau" lag bisher mitten im Vollzug von `selectAudioStream` und liess
sich nur an einem echten Server mit mehreren Tonspuren auslösen. Als
`TrackMenu` steht sie neben den übrigen Entscheidungsregeln, mit sieben
Tests — darunter der Fall, auf den der Kommentar dort seit jeher hinweist:
stimmt die Spurzahl nicht überein, ist die Zuordnung über die Reihenfolge
falsch, und es muss neu aufgebaut werden.

**Zwei stille Fehler nebenbei.** Die Tonsitzung stand hinter `#if os(iOS)`
und wäre auf tvOS nie gesetzt worden — verfügbar ist sie dort seit 9.0, man
hätte es nur daran gemerkt, dass sich die Wiedergabe gegen anderen Ton
nicht durchsetzt. Und `PlayerModel.downloads` ist jetzt optional statt
eines leeren Koordinators: ein leerer bräuchte einen `ModelContainer`, und
der ist das Einzige, wofür diese App SwiftData benutzt.

**Belegt statt vermutet.** Im Simulator lief die Wiedergabe gegen die
Attrappe, und die Titelzeile zeigte „Unbekannt". Statt an AVKit zu drehen,
hat ein Protokolleintrag die Frage beantwortet: `externalMetadata` wird
gesetzt, der Wert stimmt, und „Unbekannt" ist wörtlich das, was die
Attrappe für unbekannte Kennungen liefert. Geblieben ist davon nur die
Erkenntnis, dass die Beschriftung auf das Item warten muss — beim
Austausch des Players gibt es noch keines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 19:55:02 +02:00
ScarriffleandClaude Opus 5 87f23afe90 Die Startseite am Fernseher — und die Ladeschicht nur einmal
Aufmacher, Reihen, Mediatheken, Raster. Der sichtbare Teil ist neu; der
Teil darunter ist derselbe wie am Telefon, und das war die eigentliche
Arbeit.

**Herausgelöst statt kopiert.** `HomeFeed` und `LibraryFeed` tragen jetzt,
was vorher in `HomeView` und `LibraryView` stand: laden, ordnen, den
Aufmacher auslosen, den Zwischenspeicher abziehen, entdoppeln, nach einem
Fehlschlag sperren. Das zu kopieren wäre der schlimmste Ausgang dieses
Vorhabens gewesen — an mindestens drei dieser Stellen klebte schon je ein
Fehler, der sich als „es sind nicht alle Filme drin" äusserte und sonst
nichts. Einer davon, das Los des Aufmachers an der Position statt am
Titel, wäre in einer Kopie ein zweites Mal zu finden gewesen.

Mitgewandert sind `rowTitle`, `rowArtworkPath`, `posterArtworkPath`,
`runtimeLabel` und `HeroArtwork`: reine Ableitungen, die in einer
Ansichtsdatei lagen und deshalb nur das Telefon übersetzte.

**Nachgeladen wird am Fokus, nicht am Erscheinen.** Die alte Regel hiess
„die letzten fünf" und hing daran, dass eine Kachel sichtbar wird. Beim
ersten Zeichnen erscheinen aber alle sechzig, auch die letzten fünf — das
Nachladen lief sofort los statt beim Blättern. Auf einem Raster aus sechs
Spalten sind fünf Einträge ausserdem nicht einmal eine Zeile. `PagingWindow`
rechnet deshalb in Zeilen; sechs Tests halten es fest, darunter der Fall,
der die alte Regel entlarvt.

**Der Fokus setzt den Aufmacher.** Wer durch „Zuletzt hinzugefügt"
wandert, sieht oben, was er gerade anschaut. Kein Wechsel im Sieben-
sekundentakt wie am Telefon: vor einem Fernseher wäre ein Bild, das sich
von allein austauscht, während man es liest, eine Zumutung.

**Drei Dinge, die erst der Simulator zeigte.** Die Seiten müssen
bildfüllend laufen, weil ein Aufmacher, der achtzig Punkte vor der
Bildschirmkante aufhört, nach Fehler aussieht — also trägt `screenMargin`
auf tvOS den Überscan mit. Die Karten brauchen einen Fokus-Rückruf, weil
`\.isFocused` den nächsten fokussierbaren Vorfahren meint und das in einer
Reihe aus Karten nicht verlässlich die Karte ist. Und der Bau scheiterte
an „Unable to resolve module dependency: VLCKit" — der Abhängigkeits-
Sucher prüft `canImport(FlixyVLC)` auch für ein Ziel, das es nie verlinkt.
Ein `&& !os(tvOS)` genügt; das Paket selbst bleibt unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 19:36:49 +02:00
ScarriffleandClaude Opus 5 ab4fcfb3b8 Dasselbe Icon, nur breit
Die Markenbilder für den Fernseher waren nachempfunden: ein Dreieck, das
ungefähr aussah wie das im App-Icon, mit Farben aus icon.json geraten.
Jetzt sind es dieselben.

**Die Form aus play.svg gerechnet, nicht nachgezeichnet.** Der Pfad
beschreibt ein gleichseitiges Dreieck mit 592,76 Punkten Seite und 54,7
Punkten Eckenradius. Aus diesen Zahlen entsteht die Silhouette — und
zwar über ein um den Radius eingerücktes Dreieck mit rundem Strich, was
nicht eine Annäherung an die Bögen ist, sondern genau sie.

**Die Farben gemessen, nicht geschätzt.** In icon.json stehen
Ausgangsfarben für einen automatischen Verlauf, und wie Icon Composer den
aufspannt, steht nirgends. Also aus dem gerenderten iOS-Icon abgelesen:
Grund 160,112,255 nach 120,69,255, Zeichen 224,152,255 nach 170,97,255,
dazu der helle Saum und der weiche Schatten aus der Gruppe.

**Zwei Dinge, die erst im Vergleich auffielen.** Den optischen Versatz
nach rechts hatte ich auf die Leinwandbreite bezogen — auf dem
quadratischen iOS-Icon dasselbe, auf der breiten Fernseherleinwand
sichtbar zu viel. Er bezieht sich jetzt auf das Zeichen. Und der helle
Saum fehlte ganz; ohne ihn wirkt die Fläche aufgeklebt statt aufliegend.

Das Skript liegt als Tools/TVBrandAssets.py bei. Ändert sich die Marke,
läuft es einmal und alle fünf Grössen stimmen wieder — von Hand wären es
zehn Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 19:22:53 +02:00
ScarriffleandClaude Opus 5 1552e5f2b9 Am Fernseher steht der Code vorn, nicht das Passwort
Schritt drei: anmelden. Die Oberfläche ist neu, das Modell dahinter ist
dasselbe wie am Telefon — `JellyfinSignInModel`, unverändert bis auf
einen Wert im Initialisierer.

**Die eine Verhaltensänderung.** Nach gefundenem Server sprang der Weg
bisher immer zu Benutzername und Passwort, mit der Begründung, ein
Passwortspeicher fülle beide Felder in einer Sekunde. Vor einem
Fernseher stimmt daran kein Wort: es gibt keinen Passwortspeicher, man
tippt mit dem Steuerkreuz, und das Gerät, auf dem man den Code
bestätigt, liegt sowieso auf dem Sofa. Also `preferring: .quickConnect`
— ein Wert im Initialisierer und keine Plattformbedingung mitten in
`findServer()`. Welcher Fall gilt, weiß die Oberfläche; das Modell
nicht. Der Vorgabewert hält iOS unverändert.

Der sechsstellige Code steht in 96 Punkt mit Sperrung: er wird aus drei
Metern abgelesen und in ein Telefon getippt. In Fliesstextgrösse wäre
das eine Zumutung. Beide Wege bleiben erreichbar — für Server, auf denen
Quick Connect abgeschaltet ist, und für den Fall, dass gerade kein
zweites Gerät zur Hand ist.

**Und der Beweis, dass die geteilte Schicht trägt.** Gegen die
Jellyfin-Attrappe im Simulator: die App verbindet sich, lädt die drei
Mediatheken und zeigt sie. FlixySession, JellyfinBackend, die
Zustandsmaschine der Anmeldung — alles unverändert, alles über
Pfadeinträge geteilt, keine Zeile kopiert.

Die Startseite ist noch eine Liste von Namen. Sie kommt in Schritt vier;
hier steht sie, damit die Wurzel vollständig ist und die Anmeldung für
sich prüfbar bleibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:56:50 +02:00
ScarriffleandClaude Opus 5 22f824b000 Die Ränder kommen jetzt von der Stelle, die sie kennt
Mein voriger Versuch war an der Quelle falsch, nicht an der Rechnung.

**Was ich gebaut hatte:** die Sicherheitsränder in `RootView` messen und
über die Umgebung an den Player reichen. Gemessen in einer Ansicht,
angewendet in einer anderen, über eine Drehung hinweg — drei
Gelegenheiten, daneben zu liegen. Im Simulator ging es gut, auf dem
Gerät nicht: dort standen im Querformat links 75 und rechts 28 Punkt
Rand, und der Untertitel wurde von der Bildschirmkante abgeschnitten.

**Warum SwiftUI hier nicht hilft.** Der Sicherheitsrand ist dort kein
Wert, den man erfragt, sondern einer, den man verbraucht: wer ihn
respektiert, sieht null, und wer ihn mit `ignoresSafeArea` überschreitet,
sieht danach ebenfalls null. Beide Auswege habe ich durchgespielt und
beide gemessen. Nimmt man der Ansicht das `ignoresSafeArea`, sitzen die
Knöpfe richtig — aber die Bildfläche verliert es mit, und das Bild ist im
Querformat um die Notch-Breite schmaler. Schiebt man einen
`GeometryReader` dazwischen, passiert dasselbe.

**Das Fenster weiß es immer**, in der richtigen Ausrichtung und in dem
Moment, in dem gefragt wird. `ScreenSafeArea` ist die eine Stelle in
dieser App, die so fragt, und die eine, an der es keinen SwiftUI-Weg
gibt. Nachgeholt wird der Wert bei jeder Änderung der Fläche — das ist
genau der Moment des Drehens.

Nachgemessen im Simulator gegen die Jellyfin-Attrappe: im Querformat
meldet das Fenster 0/62/62/20, und damit stehen Kopfzeile, Schieberegler
und Zeitangaben vollständig im sichtbaren Bereich, während das Bild bis
an die Kanten reicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:32:50 +02:00