Commit Graph
100 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 11a44f6a90 Ein Aufmacher über die volle Breite
Die Startseite begann bei einer Reihe aus 150 px breiten Plakaten — das
grösste Element über dem Falz war 225 px hoch. Plex und Netflix öffnen mit
einem Bild über die ganze Breite, und das ist der grösste Einzelunterschied
im Eindruck. Ein Layout-, kein Farbproblem.

Bis zu sieben Titel, aus dem, was die Startseite ohnehin holt: erst
Angefangenes, dann Neuzugänge, dann Vorschläge. Kein zusätzlicher Abruf.

Nur mit Hintergrundbild. Ein Hero ohne Bild ist schlechter als keiner —
eine graue Fläche über den halben Bildschirm sagt nichts und nimmt den
Reihen den Platz. Gibt es keinen einzigen Titel mit Backdrop, entfällt er
ersatzlos.

Zwei Verläufe statt einem: der von unten lässt die erste Reihe sich
darüberschieben, der von links sorgt dafür, dass der Text trägt, auch wenn
das Bild dort hell ist.

Pause bei Mauszeiger und bei Tastaturfokus — wer gerade liest, soll nicht
nach neun Sekunden woanders landen. Bei "Bewegung reduzieren" wechselt er
gar nicht von selbst. Die Punkte sind echte Knöpfe mit Beschriftung; ein
div ist für Tastatur und Vorlesesoftware kein Bedienelement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:34:57 +02:00
ScarriffleandClaude Opus 5 6bd9cdb12d Eine Formensprache statt vier Radien und sechs Schriftgrössen
Die Oberfläche wirkte zusammengesetzt, nicht entworfen. Vier Gründe, alle
am Quelltext belegbar.

Die Farben waren von der App abgedriftet: elevated, placeholder, text,
text-dim, text-muted und die Trennlinie wichen in der letzten Stelle ab,
und der Standardakzent war in der App Lilac, im Browser Orchid. Einzeln
unsichtbar, zusammen genau der Grund, warum App und Browser nicht wie
dasselbe Programm aussahen. Alles auf FlaroDesign zurückgeführt.

Vier konkurrierende Radien — 6 auf Plakaten, 8 auf Karten, 999 auf jedem
Knopf, zweimal nacktes 3 — sind jetzt drei, und die ersten beiden stimmen
wieder mit FlaroDesign überein. Pillen bleiben, wo sie hingehören: Suche
und Filter.

Sechs Überschriftengrössen ohne Verhältnis zueinander sind eine Skala.
Der Kacheltitel war mit 13 px/500 leiser als die Metazeile daneben; das
erzeugt keine Hierarchie, sondern Rauschen.

Und der Akzent sass auf neun Dingen gleichzeitig. Wenn alles Akzent ist,
markiert er nichts. Der Primärknopf ist jetzt hell mit dunkler Schrift —
genau das macht Netflix, dessen Rot nur im Schriftzug vorkommt.

Dazu drei Dinge, die vorher ganz fehlten: ein sichtbarer Fokusring auf
allem Bedienbaren, ohne den die Oberfläche mit der Tastatur praktisch
unbedienbar war; Rücksicht auf "Bewegung reduzieren", das im Projekt
nirgends vorkam; und ein Hover, der mehr tut als zwei Bildpunkte zu
heben — mitsamt einem Abspielknopf, der nicht länger das halbe Plakat
verdeckt.

Tools/tokens-pruefen.py vergleicht beide Quellen und läuft im
Veröffentlichungsskript mit. Kein Generator: erzeugter Swift liest sich
schlechter als geschriebener, und die Prüfung fängt dasselbe ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:31:00 +02:00
ScarriffleandClaude Opus 5 0cd88acf70 Aus "DEU EAC3 6.0" wird "Deutsch · Dolby Digital Plus · 5.1"
Die Werte gingen roh von ffprobe in die Anzeige. Jedes Stück war richtig
und keines verständlich: EAC3 heisst Dolby Digital Plus, MOV_TEXT heisst
schlicht Text, und sechs Kanäle sind 5.1 — "6.0" ist eine Schreibweise,
die es nicht gibt. Die Auflösung stand als gemessene Höhe da, also "1040p"
für ein beschnittenes Breitbild, was aussieht wie ein Fehler.

Benennung macht daraus Sätze, und zwar im Kern: die Apps brauchen
dieselben, und zweimal formuliert heisst dieselbe Tonspur am Fernseher
anders als im Browser.

CultureInfo taugte dafür nicht, zweimal gemessen: sein NativeName liefert
den Namen in der Sprache selbst — "English", "français" —, und für ein
erfundenes Kürzel wie "xyz" erfindet es stillschweigend eine Kultur, statt
zu scheitern. Also eine eigene Liste, über das zwei- und das
dreibuchstabige Kürzel, weil beide in Mediendateien vorkommen und ISO
639-2 für manche Sprachen zwei kennt.

Unbekanntes bleibt stehen statt zu "Unbekannt" zu werden — die rohe
Angabe ist immer noch eine Auskunft. Und "und" verschwindet ganz: ffmpeg
schreibt das für unbekannt, und als Sprache angezeigt sieht es aus wie
eine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:28:17 +02:00
ScarriffleandClaude Opus 5 921533050b Zwei Titel je Film, und die Sprache lässt sich wählen
Die Mediathek sah zweisprachig aus — "Despicable Me" neben "Ich einfach
unverbesserlich". Es gab nur ein Titelfeld, und welche Sprache darin
landete, entschied die Einstellung zum Zeitpunkt des Scans.

Movie und Series tragen jetzt zusätzlich OriginalTitle. Gesucht wird über
beide: wer "Despicable Me" tippt, findet "Ich einfach unverbesserlich".
Auf der Detailseite steht der Originaltitel unter dem übersetzten, wenn er
abweicht.

Die Sprache der Metadaten ist in der Verwaltung eine Auswahl statt eines
Textfelds. Vorher musste man "de-DE" wissen, und ein Tippfehler darin nahm
TMDB stillschweigend als Englisch an.

Dazu eine eigene Sprache je Benutzer. Das Ehrliche daran steht im Dialog:
die Daten liegen in einer Sprache vor, und wer eine andere wählt, bekommt,
was übersetzt vorliegt — für den Rest die Serversprache, nicht ein leeres
Feld. ItemTranslation hält die zweite Sprache, gefüllt über eine
Sammelaktion. Nicht nebenbei beim Öffnen: das verteilte die Wartezeit über
Wochen und träfe immer den, der gerade etwas sucht.

Alle Sprachen für alle Titel vorzuhalten wäre der Bestand mal Sprachen an
Abrufen und Speicher, für einen Haushalt mit zweien. Deshalb auf Ansage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:23:47 +02:00
ScarriffleandClaude Opus 5 f359807561 Der Fortschritt sagt, woran er gerade ist — und schreibt es mit
Vorher stand in der Verwaltung minutenlang "Wird durchsucht…", ohne
Hinweis, ob überhaupt etwas passiert. Bei vierzigtausend Dateien ist das
nicht zu unterscheiden von einem hängenden Lauf.

Scanschritt trägt Phase, Gegenstand und Zählstand und bildet daraus den
Satz selbst — im Kern und nicht in der Oberfläche, weil die Apps denselben
brauchen und zweimal formuliert er auseinanderdriftet. Sechs Phasen von
"Suche Medien in /media/Filme" über "Suche ‚Ich einfach unverbesserlich'
bei der Metadatenquelle — 44 von 1204" bis "Räume auf und lege Doppeltes
zusammen". Pfade werden auf die letzten zwei Bestandteile gekürzt.

ScanAsync und EnrichAsync bekommen je einen optionalen IProgress; optional
heisst, dass kein vorhandener Aufrufer bricht. Der Koordinator hält nur
den letzten Schritt — eine Liste über vierzigtausend Titel wäre ein Leck
im Arbeitsspeicher.

Dazu das Tagebuch: eine Datei je Tag unter config/logs, nach lokaler Zeit
und nicht nach UTC, weil ein Scan um 23 Uhr am heutigen Datum gesucht
wird. Dreissig Tage Aufbewahrung, einstellbar über FLARO_LOG_TAGE — ohne
Grenze läuft die config-Partition eines NAS lautlos voll. Es wirft nie:
ein volles Dateisystem darf keinen Scan abbrechen.

In der Verwaltung lesbar, nach Fehlern filterbar und herunterladbar. Das
Containerprotokoll ist nach einem Neustart weg und nur über SSH
erreichbar — die Frage "warum wurde der Film gestern Nacht nicht erkannt?"
liess sich damit nicht beantworten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:15:18 +02:00
ScarriffleandClaude Opus 5 637e95fbb1 Die Verwaltung zeichnete sich endlos neu
Gemessen am laufenden Server: nach dem Scan lief eine Schleife mit zwei
Abrufen je Runde, sichtbar als vier dauernd drehende Spinner.

Der Weg dahin: verfolgen() lief bei jedem Aufbau der Mediathekenzeile, sah
den Zustand finished, meldete das über geaendert(), und geaendert() war
mediathekenNeuLaden — also neu zeichnen. Der Neuaufbau startete verfolgen()
erneut, das wieder finished sah. Dass der Lauf längst vorbei war, machte
keinen Unterschied; der Zustand bleibt im Speicher stehen.

Zwei Dinge dagegen. verfolgen() meldet nur noch, wenn es selbst einen Lauf
angestossen hat — beim Aufbau zeigt es den Stand an und schweigt. Und die
Verwaltung frischt über zaehlerAuffrischen() nur noch die Seitenleiste auf
statt die ganze Seite neu zu zeichnen.

Dazu zwei Befunde aus dem Plan, die sich beim Nachsehen als falsch
erwiesen: die Takte werden sehr wohl abbestellt, dafür gibt es seit je
einen MutationObserver, und das Dashboard tauscht seinen Inhalt erst nach
dem Abruf. Geblieben ist der Spinner in den Listen, der bei jedem
Auffrischen erschien — der kommt jetzt nur noch beim ersten Aufbau.

Dazu der Zwischenspeicher für Ansichten. Der Router hängt sie aus statt
sie zu zerstören; beim Zurückkommen stehen sie sofort wieder da, mitsamt
Bildlauf, offenem Reiter und gesetzten Filtern, und laden leise nach.
Gemessen: null Abrufe und 4 von 4 Kacheln beim Zurückkommen aus der
Verwaltung. Höchstens fünf Ansichten, Verfall nach fünf Minuten,
weggeworfen bei allem, was den Bestand ändert. Detailseite und Verwaltung
sind ausgenommen — beide zeigen Laufendes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:11:06 +02:00
ScarriffleandClaude Opus 5 8b67a7ec88 Verschwundene Dateien werden gemeldet, nicht stillschweigend gelöscht
Vorher räumte der Scan jede Datei weg, die er nicht mehr sah, und mit der
letzten Datei fiel der Titel samt Sehfortschritt aller Benutzer. Für eine
wirklich gelöschte Datei ist das richtig. Für eine Platte im Ruhezustand,
ein noch nicht eingehängtes Volume oder einen umbenannten Ordner ist es
Datenverlust — und für den Scan sehen alle vier Fälle gleich aus.

Jetzt trägt MediaFile ein MissingSince. Der Eintrag bleibt stehen, steht
in der Verwaltung unter "Nicht mehr gefunden", und gelöscht wird auf
Ansage. Taucht die Datei wieder auf, verschwindet die Markierung von
selbst.

Dazu die Notbremse, und sie ist der eigentliche Grund für den ganzen
Punkt: Fehlt der Wurzelpfad, oder liegt dort keine einzige Mediendatei
mehr, obwohl vorher welche da waren, bricht der Lauf ab und sagt warum.
Ohne sie erklärte ein nicht eingehängtes Volume beim Start des NAS den
gesamten Bestand für verschwunden — bei 458 Titeln.

Der Abschnitt in der Verwaltung erscheint nur, wenn es etwas zu melden
gibt. Ein dauerhaft leerer wäre Rauschen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:46:55 +02:00
ScarriffleandClaude Opus 5 01ff41fd71 Der Testhelfer wartet dreissig Sekunden statt fünf
Die alte Grenze war für zwei, drei Dateien bemessen. Der neue Test über
120 Dateien schaffte sie allein, fiel aber durch, sobald die ganze Suite
nebenher lief — kein Fehler im Scan, sondern zu knappe Geduld.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:42:09 +02:00
ScarriffleandClaude Opus 5 63c7ebb6d5 Ein Film kommt je Mediathek genau einmal vor
Der Scan verglich den Titel aus dem Dateinamen zeichengenau. Ein Punkt
Unterschied — Dr. Dolittle 2 gegen Dr Dolittle 2 —, oder eine Datei mit
Jahresangabe neben einer ohne, und es entstanden zwei Einträge. Die
Metadaten ordneten beide demselben TMDB-Eintrag zu, beide bekamen
denselben Titel, dasselbe Jahr, dasselbe Plakat, und nichts führte sie je
wieder zusammen.

Drei Ebenen, weil eine nicht trägt.

Titelschluessel vergleicht ohne Satzzeichen, ohne Akzente und ohne
Grossschreibung; ein fehlendes Jahr passt auf jedes, zwei bekannte
verschiedene trennen. Bewusst nicht aggressiver: Spider-Man und
Spiderman bleiben getrennt, und die beiden Dune von 1984 und 2021
sowieso. Ein zusammengelegter Film lässt sich nicht mehr trennen, ohne
zu wissen, welche Datei wohin gehörte.

Ein gefilterter Eindeutigkeitsindex auf (LibraryId, TmdbId) stellt die
Regel in die Datenbank statt in Code, der sie einhalten soll. Gefiltert,
weil ohne Metadaten alle Einträge null tragen.

Zusammenlegung räumt auf, was kein Vergleich erkennen könnte: Ich
einfach unverbesserlich und Despicable Me sind derselbe Film, und das
verrät allein die TMDB-Kennung. Läuft nach jedem Scan, und von Hand über
einen Knopf in der Verwaltung. Der Sehfortschritt wird gefaltet, nicht
umgehängt — die weitere Stelle gewinnt, gesehen gewinnt, weggelegt
gewinnt nicht.

Die Migration räumt zuerst auf und legt dann erst den Index an.
Andersherum scheiterte sie auf jeder Datenbank mit Dubletten, und zwar
beim Start im Container, wo die Meldung niemand sieht. Gemessen an einer
Datenbank mit zwei Dubletten und zwei Sehständen: 3 Filme auf 2, beide
Dateien erhalten, Position 1800 statt 300 behalten.

Dazu Fassungen. MediaFile.VersionLabel liest Mobil, 4K oder Director's
Cut aus dem Namen, sonst aus der gemessenen Bildhöhe. Die Liste ist
geschlossen, damit eine Fassung nicht YIFY heisst. Und der Fassungsname
beendet jetzt den Titel wie jedes andere Beiwerk — vorher wurde aus
"The Matrix (1999) - Mobil.mp4" der Film "The Matrix - Mobil", also ein
zweiter Eintrag neben dem Original. Genau die Dublette, um die es ging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:39:10 +02:00
ScarriffleandClaude Opus 5 d01c5d43f4 Der Scan schreibt auch dann zwischendurch, wenn ffprobe scheitert
Gemessen an achtzig Dateien: der Zählstand steht nach einer Sekunde bei
38, nach zwei bei 75, nach drei bei 80. Der Scan liefert also längst
laufend — anders als ich behauptet hatte. Der Grund steht in
UntersuchenAsync, das je Datei speichert; ich hatte nur das
SaveChangesAsync nach der Schleife gesehen.

Eine Lücke bleibt: scheitert die Untersuchung, kehrt sie vor dem
Speichern um. Ein Bestand aus Dateien, die ffprobe nicht lesen kann,
bliebe damit bis zum Schluss unsichtbar, und wer zusieht, hält den Scan
für hängend. Deshalb jetzt zusätzlich alle fünfzig Dateien ein Schreiben
aus der Schleife heraus — fünfzig und nicht jede, weil ein Schreibvorgang
je Datei bei vierzigtausend mehr kostet, als die Sichtbarkeit wert ist.

Für das Aufräumen danach ist das unbedenklich, aber nicht offensichtlich:
es löscht alles mit älterer SeenAt-Zeit als der Lauf, und würde die Zeit
irgendwo neu genommen statt vor der Schleife festgehalten, risse es den
halben Bestand mit. Ein Test über 120 Nulldateien — an denen ffprobe
scheitert, also genau der neue Weg — hält das fest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:27:41 +02:00
ScarriffleandClaude Opus 5 b25c7a4712 Ordner durchklicken, statt den Pfad zu wissen
Beim Anlegen einer Mediathek verlangte das Feld den Pfad im Container —
nicht den auf dem NAS. Diese Unterscheidung kennt nur, wer die
Compose-Datei selbst geschrieben hat, und ein Tippfehler darin ergibt
eine leere Mediathek ohne erkennbaren Grund.

Neben dem Feld steht jetzt ein Knopf, der die Ordner des Servers im
selben Dialog aufklappt: Pfadzeile in anklickbaren Stücken, Unterordner
darunter, unten der Knopf mit dem gewählten Pfad darauf.

Im selben Dialog und nicht in einem zweiten darüber: zwei gestapelte
Dialoge sind beim Schliessen immer eine Frage zu viel.

Das Feld bleibt tippbar. Wer den Pfad kennt, ist damit schneller, und
bei einem tiefen Baum ist es die einzige geduldschonende Möglichkeit.

An der Wurzel bleiben /proc, /sys, /usr und Verwandte aussen vor. Das
ist keine Absperrung — wer den Pfad kennt, tippt ihn weiterhin ein —,
sondern eine Liste aus zwanzig Einträgen, von denen achtzehn nie eine
Mediathek enthalten, ist unbrauchbar.

Ein Ordner ohne Leserecht wird gemeldet, nicht geworfen. Ohne das flöge
eine UnauthorizedAccessException bis in die Antwort und würde zur 500,
während die ehrliche Auskunft lautet: den darfst du nicht lesen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:22:16 +02:00
ScarriffleandClaude Opus 5 64fb61f2dc Ein typografisches Anführungszeichen zerriss die Zeichenkette
Beim Einhängen des Ordner-Endpunkts stand „{path}" in einer
interpolierten Zeichenkette. Das schliessende Zeichen beendete sie
mitten im Satz, und der Rest las sich für den Übersetzer als Folge
unbekannter Bezeichner — zehn Fehler aus einer Zeile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 10:45:05 +02:00
ScarriffleandClaude Opus 5 8a191d0faf Die Einrichtung läuft auch ohne HTTPS
crypto.randomUUID gibt es nur im secure context — HTTPS oder localhost,
sonst nichts. Wer Flaro auf einem NAS betreibt und ihn unter
http://192.168.x.x:8097 aufruft, hat die Funktion nicht, und die
Geräteerkennung warf mitten in die Einrichtung „crypto.randomUUID is not
a function". Das Konto liess sich nicht anlegen.

Das ist nicht der Sonderfall. Ein Heimserver im eigenen Netz hat kein
Zertifikat, und genau dort wird die Einrichtung durchlaufen.

crypto.getRandomValues ist nicht so beschränkt und liefert denselben
Zufall; daraus wird die UUID von Hand zusammengesetzt. Fällt auch das
aus, bleibt Math.random — kein Zufall für Geheimnisse, aber hier geht es
nur darum, zwei Browser auseinanderzuhalten, und gar keine Kennung wäre
schlechter als eine schwache.

Geprüft mit 20000 Durchläufen: alle entsprechen der Form von Fassung 4,
keine Dublette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 09:04:30 +02:00
ScarriffleandClaude Opus 5 3df873c8cd Der Arbeitsrechner baut, das NAS lädt nur
Der Runner auf dem NAS scheitert an der Docker-App des UGREEN: sie lehnt
den Einhängepunkt /var/run/docker.sock ab. Nachgewiesen durch Halbieren
der Compose-Datei — ohne diese eine Zeile wird das Projekt angelegt, mit
ihr nicht, und die Meldung nennt keinen Grund.

Übrig blieben Docker-in-Docker mit privileged oder der Weg über SSH an
der Oberfläche vorbei. Beide geben dem Runner faktisch Root auf dem NAS,
und das für eine Aufgabe, die der Arbeitsrechner ohnehin erledigen kann:
er ist für die Apps nötig, weil Xcode nirgendwo sonst läuft.

Der Workflow löst deshalb nicht mehr bei jedem Push aus. Sonst sammelt
Gitea Läufe an, die niemand abarbeitet, und die Actions-Seite füllt sich
mit Leichen. Er bleibt als workflow_dispatch stehen, der Auslöser ist mit
drei Zeilen wieder da.

Die Anleitung stellt das Bauen vom Arbeitsrechner nach vorn und hält zwei
Dinge fest, die beim Einrichten Zeit gekostet haben: die Docker-App zieht
Images nicht selbst, und die Anmeldung an der Registry will ein
Zugriffstoken statt des Kontopassworts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:47:42 +02:00
ScarriffleandClaude Opus 5 aa1e14cdab Der Runner fürs NAS, fertig zum Einsetzen
Image bauen und veröffentlichen / image (push) Canceled after 0s
Gitea verteilt Arbeit nur, es baut nicht selbst. Der Workflow ist auf dem
Server und hat einen Lauf angelegt — der bleibt liegen, weil niemand ihn
ausführt. Das ist keine Eigenheit dieser Installation, sondern wie Gitea
Actions arbeitet.

`server/docker-compose.runner.yml` ist der fehlende Teil, und er gehört
**auf das NAS**: dort läuft Docker ohnehin, und damit braucht ein neues
Image nie wieder einen Arbeitsrechner.

Zwei Dinge darin sind nicht offensichtlich und stehen deshalb als Kommentar
daneben: der Docker-Socket ist der Kern — der Runner baut nicht selbst,
sondern lässt den Docker des NAS bauen, und wer in diesen Container kommt,
kann Docker auf dem NAS steuern. Und `GITEA_RUNNER_LABELS` muss
`ubuntu-latest` nennen, weil der Flaro-Workflow genau das in `runs-on`
verlangt; ohne die Zeile meldet sich der Runner mit anderen Marken an und
der Lauf findet ihn nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:27:14 +02:00
ScarriffleandClaude Opus 5 fbf4a29461 Der Bildname kommt nicht mehr vom Repositoriumsnamen
Image bauen und veröffentlichen / image (push) Canceled after 0s
Das Repo heisst auf Gitea noch „halo", das Programm aber Flaro. Der
Workflow leitete den Bildnamen aus `gitea.repository` ab — das Image hiesse
`git.scarriffle.com/scarriffle/halo` und wäre die eine Stelle, an der der
alte Name überlebt. Das Skript daneben schreibt `flaro`; beides zusammen
ergäbe zwei Images für dieselbe Sache.

Gitea legt Pakete am **Besitzer** ab, nicht am Repository — sie dürfen
deshalb anders heissen als das Repo. Jetzt nehmen beide Wege
`<besitzer>/flaro`, überschreibbar über die Variable `IMAGE_NAME`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:10:41 +02:00
ScarriffleandClaude Opus 5 c12748e262 Ein Weg vom Quelltext in die Registry — und von dort aufs NAS
Damit das Image dort landet, wo das NAS es holen kann.

**`.gitea/workflows/image.yml`** — Gitea baut selbst. Bei jedem Push auf
`main` entsteht das Image für **beide** Architekturen und landet in der
Registry desselben Gitea. Niemand muss daran denken, niemand muss angemeldet
sein, und niemand kann die Architektur vergessen.

Voraussetzung ist ein registrierter Act-Runner. Hat deiner keinen, läuft die
Datei nie an — dafür gibt es den zweiten Weg.

**`Tools/image-veroeffentlichen.sh`** — ein Befehl vom Arbeitsrechner, baut
amd64 und arm64 und lädt hoch. Angemeldet wird einmalig von Hand; das
Passwort tippt nur, wer es kennt.

Beides legt `:latest` ab, dazu den Kurz-Hash und auf einem Git-Tag dessen
Namen — damit sich eine Fassung festnageln lässt, statt immer „latest" zu
ziehen.

Die Anleitung bekommt vier neue Abschnitte: Gitea baut selbst, von Hand
bauen, das Paket öffentlich stellen, und **die Registry im NAS eintragen** —
also genau der Dialog „Quellname des Images", mit Alias `gitea` und Link
`https://git.scarriffle.com/`. Für ein privates Paket ein Zugriffstoken mit
`read:package`, nicht das Kontopasswort: ein Token lässt sich einzeln
widerrufen.

Die Beispiel-Compose zeigte auf `ghcr.io/scarriffle/flaro` — dort liegt
nichts und lag nie etwas. Jetzt auf die Gitea-Registry.

---

**Ein Fehler im eigenen Skript, gefunden beim ersten Lauf.** Die Prüfung
„läuft Docker?" war `docker info | grep -q .` — und `grep -q` beendet die
Pipe beim ersten Treffer, Docker bekommt SIGPIPE, und mit `set -o pipefail`
gilt das als Fehlschlag. Das Skript meldete „Docker läuft nicht", während
Docker lief und gerade ein Image gebaut hatte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:08:22 +02:00
ScarriffleandClaude Opus 5 f2b5ee633b Dialoge statt Blöcke, bessere Filter, ein Dashboard — und eine NAS-Anleitung
**Was etwas anlegt oder ändert, steht jetzt in einem Dialog.** Das Formular
zum Anlegen einer Mediathek nahm als Block in der Seite Platz für etwas weg,
das man einmal im Monat braucht — und sah aus wie eine Eingabe, die noch
aussteht. Dasselbe für Benutzer, Umbenennen, den Servernamen und die
Metadaten-Einstellungen.

Gebaut mit dem `<dialog>` des Browsers, nicht mit einem nachgebauten: Escape
schliesst, der Hintergrund wird unbedienbar, der Tastaturfokus bleibt
gefangen, Vorlesesoftware kündigt ihn als Dialog an. Jede Zeile, die das
selbst baut, ist eine Zeile, die es schlechter kann. Scheitert das
Speichern, bleibt der Dialog offen — ihn zu schliessen hiesse, die Eingabe
wegzuwerfen und die Meldung dorthin zu schreiben, wo gerade niemand hinsieht.

**Die Filter sehen nicht mehr nach Eingabe aus.** Bei Plex stehen dort
Wörter mit einem kleinen Pfeil; ein umrandetes Auswahlfeld sagt „hier fehlt
noch etwas", und das stimmt nicht. Gesetzte Filter werden farbig, daneben
steht die Anzahl, und „Filter zurücksetzen" erscheint nur, wenn es etwas
zurückzusetzen gibt. Dazu neu: gesehen / noch nicht gesehen.

Das siebt vorerst in der Oberfläche, nicht am Server — er kann es noch
nicht. Der Preis steht in der Zahl daneben: sie zeigt dann „3 von 7".

---

**„Wird abgespielt" — wer gerade was schaut.**

Abgeleitet aus den Fortschrittsmeldungen, nicht aus einer eigenen Anmeldung
am Spieler: jeder Client meldet beim Abspielen ohnehin alle zehn Sekunden,
wo er steht. Wer das als Lebenszeichen liest, braucht kein zweites
Verfahren, das ein Client vergessen könnte — bleibt die Meldung aus, läuft
der Eintrag nach einer Minute von selbst ab.

Im Arbeitsspeicher, nicht in der Datenbank: „wer schaut gerade was" ist eine
Frage über den Augenblick. Nach einem Neustart lautet die Antwort „niemand",
und das zu speichern hiesse, Wiedergaben anzuzeigen, die es nicht gibt.

Zwei Geräte desselben Menschen sind zwei Zeilen — derselbe Mensch kann auf
dem Fernseher und auf dem Telefon gleichzeitig etwas laufen lassen.
Geschlüsselt wird deshalb über das Gerätetoken, nicht über den Benutzer und
nicht über den Gerätenamen: zwei iPhones heissen beide „iPhone".

**Nur für Administratoren.** Wer wann welchen Film anschaut, ist eine
Auskunft über Menschen, nicht über den Server. In einem Haushalt mag das
harmlos sein; die Vorgabe eines offenen Projekts darf es trotzdem nicht sein.

Der Spieler meldet beim Schliessen ausdrücklich „beendet". Ohne das stünde
ein geschlossener Film noch eine Minute als „läuft" — und wer gerade den
Spieler zugemacht hat, sähe sich selbst beim Schauen zu.

---

**`docs/NAS-EINRICHTEN.md`** beantwortet die Frage, die offen war: **es ist
ein einziges Image.** Die Weboberfläche wird beim Bauen hineingelegt und vom
Server selbst ausgeliefert — ein Container, drei Volumes, kein Proxy, keine
Datenbank daneben.

Drei Wege aufs NAS: über die Container-Registry deines Gitea (die läuft —
nachgeprüft), als Datei über `docker save`, oder dort bauen. Dazu der
Schritt, den man am leichtesten überspringt und am teuersten bezahlt: die
Architektur. Ein Mac baut ohne Angabe arm64, ein Intel-NAS braucht amd64,
und `exec format error` sagt das nicht deutlich.

Eine Zahl darin war geraten und ist korrigiert: `docker save | gzip` ergibt
nicht 90 MB, sondern 230 — die Ebenen eines Images sind bereits gepackt.

378 Tests grün, sechs davon neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:03:07 +02:00
ScarriffleandClaude Opus 5 a7ec3a73ba Angemeldet bleiben — und eine 401 reisst nicht mehr die Sitzung ab
Erst die ehrliche Hälfte: dass es heute **andauernd** abgemeldet hat, geht
auf mein Konto. Ich habe die Datenbank dreimal geleert, um mit frischen
Metadaten zu bauen; jedes Mal verfallen alle Token. Kein Schalter der Welt
hätte das verhindert.

Die andere Hälfte ist ein echter Fehler, und er macht aus einem Aussetzer
eine Abmeldung:

**Eine einzelne 401 warf die Anmeldung weg** — und rief dabei
`/auth/logout`, der Server zog das Token also *aktiv zurück*. War die
Ablehnung nur ein Aussetzer, war eine intakte Anmeldung damit absichtlich
zerstört, und zwar unwiderruflich. Jetzt wird einmal nachgefragt: gilt das
Token wirklich nicht mehr, ist die Sitzung vorbei; antwortet der Server, war
es diese eine Anfrage. Mehrere gleichzeitige Anfragen laufen in dieselbe
Prüfung — sonst stellte eine Startseite mit acht Abfragen achtmal dieselbe
Frage. Antwortet der Server **gar nicht**, ist das kein Beleg gegen das
Token: jemanden abzumelden, weil das WLAN hakt, ist der falsche Schluss.

**Abmelden scheitert nie mehr.** Vorher verlangte der Weg eine gültige
Anmeldung — und genau dann, wenn man ihn am dringendsten braucht, gibt es
keine. Nach dem Leeren des Datenbestands zeigte das Cookie im Browser auf
ein Token, das es nicht mehr gab; es ging bei jedem Bild erneut hinaus, und
der Server lehnte es jedes Mal ab. Achtzig Ablehnungen in einer Stunde, und
nichts davon liess sich von aussen beenden. Jetzt wird das Cookie immer
gelöscht; widerrufen wird, was sich widerrufen lässt.

---

**Und der Schalter, um den es ging.** „Angemeldet bleiben" steht jetzt im
Anmeldefeld, mit Haken als Vorgabe — am eigenen Rechner will sich niemand
täglich neu anmelden.

Umgesetzt über die beiden Speicher, die der Browser ohnehin richtig
aufräumt: mit Haken `localStorage` und ein Cookie für ein Jahr, ohne Haken
`sessionStorage` und ein Sitzungscookie, das mit dem Fenster verschwindet.
Ein eigenes Verfallsdatum zu verwalten wäre die schlechtere Variante — eine
Uhr, die man stellen kann, gegen zwei Mechanismen, die es schon gibt.

Das Token selbst läuft weiterhin **nie** ab; es wird widerrufen, nicht
vergessen. Was hier entschieden wird, ist allein, wie lange der Browser sich
erinnert.

372 Tests grün, vier davon neu. Am Container nachgemessen: mit Haken
`max-age=31536000`, ohne Haken kein `max-age`, und ein Abmelden mit
ungültigem Cookie antwortet 204 statt 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 07:40:19 +02:00
ScarriffleandClaude Opus 5 8c395ac051 Die Oberfläche nach Plex' Vorbild — Hintergrund, Schriftzug, Besetzung
Nach den Bildern deiner Plex-Installation umgebaut. Übernommen ist der
Aufbau, nicht die Farbe: Plex ist bernsteingelb, Flaro bleibt Orchidee.

**Kopfleiste oben** mit Marke, Suche und Konto. Die Suche gehört dorthin und
nicht in jede Mediathek: wer einen Titel sucht, weiss oft gar nicht, in
welcher er liegt. Sie geht quer durch alles.

**Seitenleiste mit Icons** — echte SVG, keine Zeichen aus einer Schriftart.
Ein ▶ aus Unicode sieht auf jedem System anders aus, lässt sich nicht
einfärben und springt in der Grundlinie. Die aktive Zeile bekommt einen
schmalen Balken links statt eines leuchtenden Blocks; das liest sich in
einer langen Liste ruhiger.

**Die Kachel steht jetzt an einer Stelle** statt an fünfen. Sie trägt den
Haken für Gesehenes in der Ecke — wie bei Plex, ein Blick über das Raster
genügt —, die Zahl offener Folgen an derselben Stelle, den Fortschrittsbalken
unten und beim Überfahren einen Abspielknopf. Folgen sind dreizeilig: Serie,
Folgentitel, „S14 · F22".

**Reihen haben Pfeile.** Zwanzig Kacheln passen nicht in den Bildschirm, und
quer zu scrollen geht mit dem Rad nur mit Umschalttaste. Die Überschrift
nennt die Mediathek beim Namen: „Zuletzt in Dokumentationen hinzugefügt".

**Mediatheken haben drei Reiter** — Empfehlungen, Mediathek, Kategorien.
Darunter eine Filterzeile mit Genre, Sortierung und der Anzahl. Die
Kategorien sind farbige Kacheln; die Farbe kommt aus dem Namen, nicht aus
einer festen Liste, die jedes Genre kennen müsste, das TMDB je erfindet.

**Die Detailseite** hat jetzt den unscharfen Hintergrund über die ganze
Seite, den **Schriftzug des Films** statt der Systemschrift, eine Zeile mit
Jahr, Genres, Laufzeit, Altersfreigabe und Bewertung, runde Knöpfe für
gesehen/Favorit/weglegen, die Medienangaben als Tabelle — Datei, Video, Ton,
Untertitel — und zuletzt Besetzung & Stab mit runden Porträts.

**Dazu am Server:** ein Name. Ohne ihn zeigten die Clients den Hostnamen,
und die App schrieb „localhost" über die Anmeldung. Vorgabe ist der
Rechnername des Containers.

---

**Der Fehler, den das erste Bild zeigte:** die Lupe in der Suchleiste war
dreihundert Bildpunkte gross und lag über der halben Seite. Ein rohes
`<svg>` ohne Breite füllt seinen Container; die Regel galt nur für `.icon`.
Jetzt für jedes.

368 Tests grün. Am Container nachgesehen: „Der Untergang" mit eigenem
Schriftzug, FSK 12, Bewertung 7,9, Genres Drama/Historie/Kriegsfilm, beide
Tonspuren in der Tabelle, 22 Porträts darunter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 01:04:27 +02:00
ScarriffleandClaude Opus 5 ede4069cd4 Der Server weiss jetzt mehr als Titel und Plakat
Bisher standen zu einem Film vier Angaben in der Datenbank: Titel, Jahr,
Beschreibung, zwei Bilder. Damit lässt sich eine Kachel zeichnen und sonst
nichts — keine Detailseite, die mehr sagt als der Dateiname, keine
Vorschläge, kein Filter nach Genre. Alles drei hing daran.

Jetzt kommen dazu: **Laufzeit, Bewertung, Werbezeile, Altersfreigabe,
Genres, Besetzung, Stab — und der Schriftzug des Titels.** Letzterer ist
das, was eine Startseite teuer aussehen lässt: „Blade Runner 2049" in seiner
eigenen Schrift über dem Hintergrundbild statt in der Systemschrift.

**Alles in einer Anfrage.** `append_to_response` hängt Besetzung,
Fremdkennungen, Freigaben und Bilder an den Einzelabruf. Vier getrennte
Anfragen je Titel wären bei vierhundert Filmen sechzehnhundert, und TMDB
begrenzt, wie oft man fragen darf. Der eigene Abruf der IMDb-Kennung ist
dabei entfallen — sie hängt jetzt mit dran.

Freigaben sind national: die FSK gilt in Deutschland, die MPAA in den USA,
und TMDB führt beide nebeneinander. Genommen wird die des eingestellten
Landes. Beim Schriftzug entscheidet zuerst die Sprache und erst danach die
Bildbewertung — ein deutscher Titel in deutscher Schrift schlägt den besser
bewerteten englischen.

Vom Stab nur Regie und Drehbuch. TMDB führt dort auch Beleuchter und
Caterer; eine Detailseite, die alle nennt, nennt nichts.

**Genres und Personen als eigene Tabellen**, nicht als Text im Film. Sonst:
filtern über `LIKE`, Tippfehler als eigenes Genre, und bei einer Umbenennung
vierhundert Zeilen anfassen. Personen über die Kennung der Quelle geführt,
nicht über den Namen — es gibt zwei Schauspieler namens „Chris Evans".

Darauf sitzen zwei neue Endpunkte: ein Filter nach Genre (nur die, die in
der Mediathek auch vorkommen — ein Knopf zu einer leeren Liste ist schlimmer
als kein Knopf) und **Vorschläge** über die Genres des zuletzt Gesehenen.
Keine Empfehlungsmaschine: ein Haushaltsserver hat keine Millionen
Sehverläufe, aus denen sich etwas lernen liesse. Eine schlichte Regel ist
nachvollziehbar, was eine undurchschaubare Empfehlung nie ist.

---

**Zwei Fehler, beide fand erst der echte Lauf.**

`UNIQUE constraint failed: Genres.Name`, und damit scheiterte der ganze
Scan — in der Mediathek erschien kein einziger Titel. Vier Bibliotheken
scannen gleichzeitig, jede mit eigenem Datenbankkontext; zwei stossen auf
„Drama", keine sieht die ungespeicherte Zeile der anderen. Statt die Läufe
zu serialisieren — das kostete bei zehn Bibliotheken das Zehnfache an Zeit —
wird der Fehlschlag hingenommen und die Zeile des anderen benutzt. Sie steht
ja genau dafür da.

Und: **eine geänderte Sprache erreichte vorhandene Titel nie.** Ein zweiter
Scan fragt nichts erneut, das ist Absicht. Wer aber von Englisch auf Deutsch
stellt, bekam deutsche Titel erst beim nächsten neuen Film. Dafür gibt es
jetzt „Metadaten auffrischen" je Bibliothek — geholt über die bekannte
Kennung, nicht über eine neue Suche, damit eine Zuordnung von Hand
stehenbleibt.

368 Tests grün, zehn davon neu. Am Container nachgewiesen: „Der Untergang",
156 Minuten, FSK 12, Bewertung 7,9, Genres Drama/Historie/Kriegsfilm,
22 Mitwirkende mit Rollennamen, Schriftzug und Hintergrund da — und nach dem
Umstellen auf de-DE heisst er nicht mehr „Downfall".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 23:12:35 +02:00
ScarriffleandClaude Opus 5 230beb9664 Mehrere Mediatheken, jede für sich
Bisher gab es in der Seitenleiste „Filme" und „Serien" — zwei feste Zeilen,
hinter denen alles zusammenfiel, was der Server kannte. Wer „Filme" und
„Dokumentationen" getrennt angelegt hatte, sah sie wieder in einem Topf.
Das nahm die Entscheidung zurück, sie zu trennen.

Jetzt führt die Seitenleiste **jede Mediathek einzeln**, mit Namen und Zahl,
wie bei Plex und Jellyfin. Die Startseite bekommt je Mediathek eine Reihe
mit dem zuletzt Hinzugefügten; steht mehr drin, als die Reihe zeigt, führt
„Alle zeigen" dorthin. Die Mediathekenseite trägt den Namen im Titel, sucht
innerhalb der Mediathek und lässt sich sortieren.

**Am Server dafür:**

`?sort=` auf `/movies` und `/series` — `title`, `added`, `year`. Der Titel
immer als letzter Vergleich, sonst hätten zwei Filme aus demselben Jahr keine
feste Reihenfolge und Seite 2 zeigte denselben Film noch einmal oder gar
nicht.

Die Liste der Bibliotheken nennt je Zeile einen Zählwert. „Dokumentationen ·
0" sagt mehr als ein Eintrag, hinter dem sich nach dem Klick nichts findet —
da ist der Unterschied zwischen „noch nicht gescannt" und „falscher Pfad"
wenigstens sichtbar. `null` heisst „nicht gezählt", nicht „leer".

Umbenennen und Entfernen, beides nur für Administratoren. **Umbenennen, mehr
nicht:** die Art zu ändern hiesse, Filme in Serien zu verwandeln; den Pfad
zu ändern hiesse, einen Bestand gegen einen anderen zu tauschen, während die
Einträge stehenbleiben. Beides ist kein Bearbeiten, sondern eine neue
Bibliothek.

**Beim Entfernen bleiben die Mediendateien liegen.** Weg sind der Eintrag,
die Zuordnungen und der Sehfortschritt aller Benutzer dazu; die Oberfläche
sagt das vorher, und eine Prüfung hält fest, dass die Datei danach noch da
ist. Ein Server, der beim Aufräumen in der Oberfläche fremde Filme löscht,
ist einer, den man nicht noch einmal benutzt.

---

**Der Fehler, den der Browser zeigte, und er betrifft jeden Betrieb:** nach
einem Server-Update lief weiter die alte Oberfläche.

Die Dateien unter `assets/` tragen eine Prüfsumme im Namen — ändert sich der
Inhalt, ändert sich der Name. `index.html` trägt keine; sie nennt nur deren
Namen. Lag sie im Zwischenspeicher, bekam der Browser weiter die alte App:
nicht kaputt, sondern veraltet, und das fällt niemandem auf. Nachgemessen:
geladen war `index-C0K3V8eg.js`, ausgeliefert wurde längst
`index-DomHN4_b.js`.

Jetzt zwei Lebensdauern — `no-cache` für die Seite, ein Jahr und `immutable`
für die Bausteine. Mit Prüfung, und am Container nachgemessen.

Der Beispielbestand bekommt zwei weitere Mediatheken, „Dokumentationen" und
„Konzerte". Mit nur Filme und Serien liesse sich gar nicht prüfen, ob
mehrere wirklich getrennt bleiben — zwei Bestände **derselben Art** sind der
Fall, in dem es auffällt.

358 Tests grün. Am Container nachgewiesen: vier Mediatheken in der
Seitenleiste mit Zählwerten, je eine Reihe auf der Startseite, und
„Dokumentationen" zeigt zwei Titel statt sieben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 22:57:25 +02:00
ScarriffleandClaude Opus 5 172b38da06 Die Apps können jetzt beides: Jellyfin oder Flaro
`FlaroServerKit` steht neben `JellyfinKit`, nicht darin — genau auf dem
Steckplatz, den Plex geräumt hat. Dass `MediaBackend` zwei Server zugleich
trägt, war schon einmal bewiesen; die Naht hat gehalten, und keine einzige
Ansicht musste dafür neu geschrieben werden.

`ServerKind` ist zurück. Es wurde beim Entfernen von Plex gestrichen, weil
ein Aufzählungstyp mit einem Fall ein umständliches Bool ist. Jetzt gibt es
wieder zwei, und es ist wieder eine Unterscheidung — einer zur Zeit, nicht
beide nebeneinander.

**Flaro steht oben auf dem Anmeldebildschirm.** Nicht aus Eitelkeit: dort
bringt ein Download alle Tonspuren mit und der Fortschritt wird ab der
ersten Sekunde gemerkt. Wer Jellyfin benutzt, sucht gezielt danach — wer neu
anfängt, soll nicht in der schwächeren Hälfte landen, weil ihr Knopf
auffälliger war. Am Fernseher steht bei beiden der sechsstellige Code voran.

---

**Der Zugang geht als Cookie, nicht in der Adresse.** Jellyfin hängt sein
Token an jede Bildadresse, weil eine Bildansicht keine Kopfzeilen schickt;
es steht damit in jedem Zugriffsprotokoll und jedem weitergegebenen Link.
Flaro tut das nicht, und deshalb legt der Client sein Token selbst im
gemeinsamen Keksspeicher ab — auch beim Wiederaufnehmen aus dem
Schlüsselbund, wo gar keine Anmeldung stattfindet und der Server es nie
setzen könnte.

`URLSession` schickt es von allein mit; **AVFoundation nicht**. Der
Systemplayer bekommt die Kekse jetzt ausdrücklich. Serverneutral: wo keine
liegen, ist die Liste leer.

**`BackendCapability.allTracksDownload`** statt einer Spurwahl, die ins
Leere führt. Die Frage „welche Tonspur?" gibt es nur, weil Jellyfins
Transcoder eine je Sitzung abbildet. Bei Flaro führen beide Antworten zum
selben Ergebnis — also wird nicht gefragt, und das Umpacken mit einer
einzigen Spur entfällt gleich mit.

**Die Adressraterei wandert nach `MediaKit`.** Sechzig Zeilen sorgfältig
begründeter Reihenfolge — IP-Adressen sprechen selten HTTPS, Domänennamen
fast immer — unterscheiden sich zwischen beiden Servern nur im Standardport.
Zweimal hingeschrieben wäre die eine Fassung beim nächsten Sonderfall
nachgezogen worden und die andere nicht. Jellyfins Prüfungen laufen
unverändert gegen die verschobene Fassung; das ist der Beweis.

**Nebenbei zwei Altlasten:** `player.jellyfinPending` war unerreichbar — der
Player zeigt diese Meldung nur, wenn gar kein Server da ist, dann aber nie
die mit Jellyfin. Und `signIn.jellyfin.userName` hiess „Benutzername"; der
Schlüssel heisst jetzt so neutral wie der Text.

---

**Der Fehler, den erst der Simulator zeigte:** „Stelle wird
wiederhergestellt", 0:00, für immer.

Muss der Server umpacken, antwortet er auf den Download mit 202 und fängt
an. AVPlayer bekommt darauf kein Video, sondern eine leere Antwort — und
wartet, unbegrenzt, ohne Fehler. Die Weboberfläche fragt in einer Schleife
nach; `preparePlayback` tat es nicht. Jetzt wartet es, mit Obergrenze und
einer Begründung, wenn sie reisst. Mit Prüfung, die zwei Runden erzwingt.

413 Tests grün. Im Simulator gegen den echten Server nachgewiesen: Adresse
eintippen, Server gefunden, angemeldet, Mediathek mit Plakaten, Neustart
ohne erneute Anmeldung — und eine MKV abgespielt, die der Server dafür erst
umpacken musste. Danach stand auf dem Server: Matrix, 2,0 s von 2,0 s,
gesehen, ein Durchlauf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 22:00:02 +02:00
ScarriffleandClaude Opus 5 6d5824b889 Weiterschauen, sichtbar gemacht
Die Oberfläche zeigt jetzt, was der Server seit Stufe 4 weiss.

Auf der Startseite zwei neue Reihen, ganz oben: **Weiterschauen** und
**Nächste Folge**. Sie überschneiden sich nicht — was angefangen ist, steht
links, was ansteht, rechts. Eine leere Reihe wird gar nicht erst gezeigt.

Auf jeder Kachel ein Balken, und zwar **ab der ersten Sekunde**. Bei einem
Zweieinhalbstünder sind zwei Minuten 1,3 % — anderthalb Bildpunkte. Der
Balken hat deshalb eine Mindestbreite, sonst wäre das Versprechen „wir merken
uns jede Sekunde" ausgerechnet dort unsichtbar, wo es zählt. Serien tragen
die Zahl offener Folgen als Plakette in der Ecke.

Auf der Detailseite stehen **beide Knöpfe nebeneinander**: „Weiterschauen ab
0:42" und „Von vorn". Auch bei einem gesehenen Film — der Server wirft die
Stelle nicht weg, wenn das Häkchen kommt, und sie hier zu verschweigen hiesse,
genau den Unterschied wegzuwerfen, um dessentwillen er sie behält. Dazu ein
Schalter für gesehen/ungesehen.

Der Spieler meldet alle zehn Sekunden, beim Pausieren, am Ende und beim
Schliessen. Nicht bei jedem `timeupdate`: das feuert viermal je Sekunde, und
daraus Anfragen zu machen hiesse, den Server mit Meldungen zu bewerfen, die
sich um 250 ms unterscheiden. Eine verlorene Meldung kostet ein paar Sekunden
Fortschritt — dafür mitten im Film einen Fehler einzublenden wäre die
schlechtere Hälfte des Tauschs.

**Nebenbei ein Fehler beseitigt, der nur mit grosser Mediathek aufgefallen
wäre:** die Detailseite holte 200 Titel und suchte darin nach der Kennung.
Bei 201 Filmen hätte sie vom letzten behauptet, es gebe ihn nicht mehr. Jetzt
gibt es den Einzelabruf, und sie benutzt ihn.

Im Container durchgespielt: „Der Untergang" von vorn, die MKV wird umgepackt,
läuft durch — danach steht „Gesehen" unter dem Titel, der Balken ist voll,
und der Knopf bietet beides an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:33:56 +02:00
ScarriffleandClaude Opus 5 67cf0fd1d9 HEAD auf den Download, und /api verschluckt nichts mehr
Zwei Fehler, derselbe Grund — und beide zeigte erst der Browser.

**Der Spieler bekam die Webseite statt des Films.** „MEDIA_ELEMENT_ERROR:
Format error", 531 Byte, `Content-Type: text/html`. Das ist genau die Grösse
von `index.html`.

`MapGet` nimmt HEAD nicht an. Die Oberfläche fragt aber mit HEAD nach, ob das
Umpacken fertig ist — einen halbfertigen Film herunterzuladen, nur um das zu
erfahren, wäre absurd. Die Anfrage fiel durch den Methodenabgleich hindurch
bis zum SPA-Rückfall, und der nimmt jeden Pfad ohne Dateiendung. Antwort:
200, mit der Startseite im Rumpf. Der Client hielt das Umpacken für fertig
und reichte HTML an `<video>` weiter.

Jetzt `MapMethods` mit GET und HEAD. Kestrel lässt den Rumpf bei HEAD selbst
weg.

**Und ein vertippter Aufruf sah aus wie ein Erfolg.** `GET /api/v1/gibtsnicht`
gab 200 und die Webseite zurück — derselbe Rückfall. Ein Fehler, der erst
dort auffällt, wo jemand HTML durch einen JSON-Parser schickt.

`app.Map("/api/{*rest}", 404)` davor. Buchstaben schlagen Platzhalter, also
gewinnt jeder echte Endpunkt; was übrig bleibt, ist nicht da und sagt das
auch.

350 Tests grün, die zwei neuen eingerechnet. Am Container nachgemessen: HEAD
auf den Download gibt 200 `video/mp4` mit 71 920 Byte, `/api/v1/gibtsnicht`
gibt 404, und ein Neuladen auf `/verwaltung` bekommt weiterhin die Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:33:56 +02:00
ScarriffleandClaude Opus 5 c4081ff251 Stufe 4: Fortschritt ab der ersten Sekunde, ohne Schwelle
Der Anlass für diesen Server, jetzt als Code. Fortschritt, Gesehen-Status,
Weiterschauen, Nächste Folge, Favoriten und Bewertungen — alles je Benutzer.

**Die Jellyfin-Werte sind jetzt belegt statt behauptet.** Das Entwurfsdokument
hatte das ausdrücklich offengelassen („aus dem Gedächtnis"); hier stehen sie
aus `MediaBrowser.Model/Configuration/ServerConfiguration.cs` und
`Emby.Server.Implementations/Library/UserDataManager.cs`:

| | Vorgabe | Was passiert |
|---|---|---|
| `MinResumePct` | 5 | darunter `positionTicks = 0` — Kommentar im Quelltext: „ignore progress during the beginning" |
| `MaxResumePct` | 90 | darüber gesehen, Position **auf 0** |
| `MinResumeDurationSeconds` | 300 | alles unter fünf Minuten gilt nach dem ersten Lebenszeichen als gesehen |

Bei einem Zweistünder sind die ersten sechs Minuten damit weg. Eine
vierminütige Folge gilt nach dreissig Sekunden als vollständig gesehen. Und
fehlt die Laufzeit, nimmt Jellyfin an, der Titel sei durch — wörtlich:
„we'll just have to assume it was fully played".

**Flaro legt jede Meldung ab und wirft nie eine Position weg.** Eine Grenze
fürs Häkchen gibt es trotzdem — 90 %, sonst stünde jeder zu Ende geschaute
Film für immer bei 98 % in Weiterschauen. Der Unterschied ist nicht, *dass*
es die Grenze gibt, sondern was sie anrichtet: hier wird ein Häkchen gesetzt,
dort zusätzlich die Stelle vernichtet. Wer einen gesehenen Film noch einmal
anfängt, hat beides — das Häkchen und den Punkt, an dem er steht.

Ohne bekannte Laufzeit wird nichts vermutet. Das ist der Fehler, den man
nicht zurücknehmen kann, ohne ihn zu bemerken.

---

**Eine Spalte statt einer zweiten Regel.** „Gehört in Weiterschauen" vergleicht
die Stelle mit einem Anteil der Laufzeit, und diese Rechnung lässt sich nicht
nach SQL übersetzen. Die Regel daneben noch einmal als Abfrage zu schreiben
wäre die Variante, bei der beide irgendwann auseinanderlaufen — unbemerkt,
weil keine von beiden dabei falsch aussieht. Stattdessen hält `InProgress`
das Ergebnis fest, geschrieben an genau einer Stelle. Eine Prüfung hält Reihe
und Regel über vier Stände gegeneinander.

**„Weiterschauen" und „Nächste Folge" überschneiden sich nicht.** Was
angefangen ist, steht links; was ansteht, rechts. Jellyfin zeigt beides
doppelt, und auf einer Startseite mit zwei Reihen nebeneinander sieht das
nach einem Fehler aus.

Dazu, weil die App sie braucht: Einzelabruf für Film und Serie, die nächste
Folge nach einer gegebenen, und der Zustand hängt an jeder Liste mit dran —
in einer Abfrage je Seite, nicht in einer je Titel.

Ein Titel wird über seine Kennung allein angesprochen, ohne dass der Aufrufer
sagen muss, ob es ein Film oder eine Folge ist. Die App hat diese Auskunft an
der Stelle nicht.

348 Tests grün. Im Container nachgewiesen: fünf Minuten „Der Untergang" von
2:36 bleiben stehen, bei Minute 150 kommt das Häkchen und die Stelle bleibt,
beim zweiten Anlauf steht der Film mit beidem wieder in der Reihe — und nach
Folge 1 von Dark zeigt „Nächste Folge" S01E02.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:25:55 +02:00
ScarriffleandClaude Opus 5 705ec87416 Ein lokaler Server mit Beispielmedien, zum Ausprobieren
`Tools/demo-medien.sh` legt unter `~/.flaro-medien` einen kleinen Bestand
an: Der Untergang, Matrix, Blade Runner 2049, Inception, Dark mit drei
Folgen, Notruf Hafenkante. Echte Namen, damit TMDB etwas zu finden hat —
aber jede Datei ist zwei Sekunden Testbild mit Ton und wiegt ein paar
Kilobyte.

**Eine davon hat zwei Tonspuren**, Deutsch und Audiodeskription, mit
gesetzter Sprache und gesetztem Namen. Das ist der Fall, um dessentwillen
der ganze Server existiert: Jellyfins Transcode-Endpunkt bildet eine
Tonspur je Sitzung ab, und einmal hat die App dabei still die
Audiodeskription gewählt — drei Gigabyte Mobilfunk für eine unbrauchbare
Datei. Wer das nachstellen will, braucht eine Datei, die es kann.

`docker-compose.local.yml` daneben: benannte Volumes statt NAS-Pfaden,
Port 18080 statt 8080, Beiwerk eingeschaltet.

**`user: "0:0"` ist eine Mac-Eigenheit und steht deshalb kommentiert da.**
Colimas Dateifreigabe reicht die Besitzverhältnisse des Wirts nicht in die
VM durch — eingehängte Ordner erscheinen im Container als root, und ein
Server, der als 1000 läuft, kann dort keine NFO ablegen. Auf einem
Linux-NAS gehört die Zeile weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:07:44 +02:00
ScarriffleandClaude Opus 5 91eb934048 Die Weboberfläche — im Zuschnitt von Plex, vom Server selbst ausgeliefert
Einrichtung, Anmeldung, Mediathek, Detailseite mit Abspielen und
Herunterladen, Zuordnung korrigieren, Verwaltung. Die Oberfläche aus der
Jellyfin-Zeit ist dabei vollständig ersetzt: `types.ts`, `prefs.ts`,
`contrast.ts`, `views/cards.ts`, `views/player.ts` und `views/settings.ts`
sprachen Jellyfins Schnittstelle und hatten hier nichts mehr zu suchen.

Seitenleiste links, waagerechte Reihen auf der Startseite, Plakate im
Verhältnis 2:3, unscharfer Hintergrund hinter dem Kopf der Detailseite —
das ist der Zuschnitt von Plex, weil er gefällt. **Die Farbe bleibt Flaros
Orchidee**, nicht Plex' Orange; der Aufbau ist das Vorbild, nicht das
Aussehen.

Welche Bibliothek gezeigt wird, entscheidet die Seitenleiste. Eine zweite
Umschaltung innerhalb der Seite wäre dieselbe Entscheidung an zwei Orten.

---

**Der Zugang liegt jetzt auch in einem Cookie.** Ein `<img>`, ein `<video>`
und ein Download-Link senden keine eigenen Kopfzeilen — eine App kann ihr
Token in jede Anfrage legen, ein Browser nicht. Das Token stattdessen in
die Adresse zu schreiben wäre der naheliegende Ausweg und stünde danach in
jedem Zugriffsprotokoll, in jedem Verlauf und in jedem weitergegebenen
Link.

`HttpOnly`, damit kein eingeschlichenes Skript herankommt. `SameSite=Strict`
gegen Anfragen von fremden Seiten. `Secure` nur über https — sonst liesse
sich die Oberfläche im Heimnetz über http gar nicht benutzen. Die Apps
benutzen weiterhin die Kopfzeile: ein zweiter Weg zum selben Token, kein
zweites Verfahren.

---

**Der Fehler, den erst der echte Container zeigte:** eine weisse Seite,
ohne Hinweis worauf.

Die Rückfallregel verlangt für jede Anfrage ohne eigenen Endpunkt einen
angemeldeten Benutzer, und `/assets/index.js` hat keinen. Die statischen
Dateien standen unter `UseAuthorization` — ein neuer Besucher bekam 401 auf
genau das Skript, das ihm die Anmeldemaske zeichnen sollte. `/` kam durch,
weil der SPA-Rückfall `AllowAnonymous` trägt; alles mit Dateiendung nicht.
Jetzt liegen sie davor. Mit Prüfung.

Beim Bau desselben Images der zweite: seit der Kontext die Projektwurzel
ist, überschrieb `COPY server/` die `obj/`-Verzeichnisse aus dem Container
mit denen vom Arbeitsrechner — die zeigen auf osx-arm64, gebaut wird
linux-arm64, `dotnet publish` brach mit NETSDK1047 ab. Dafür gibt es jetzt
eine `.dockerignore` in der Wurzel; die unter `server/` sucht Docker seit
dem Umzug nicht mehr.

---

Vier Prüfungen schlugen danach fehl, und zwar zu Recht: sie hiessen „ohne
Anmeldung gibt es nichts", benutzten aber den Client, der nach dem Anmelden
ein Cookie hält. Sie prüften also den Angemeldeten. Jetzt sagen sie
ausdrücklich, dass sie ohne Zugang fragen.

313 Tests grün. Im Container nachgewiesen: Anmelden, Plakate, Abspielen,
Staffeln und Folgen, Verwaltung — und ein Neuladen auf `/verwaltung`, das
nicht in einem 404 endet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:07:29 +02:00
ScarriffleandClaude Opus 5 a7cf6c5def Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine
eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt
so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner
Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging
daneben.

**Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den
deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche
Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in
Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store
der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden
frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler.

Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen
ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim
letzten Mal stattfand.

---

2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der
Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`,
`Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen
`FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und
das Image.

**Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen
Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes
Ersetzen hätte den sonst zerlegt.

`docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte
Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das
Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden
in die Irre führt.

**Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag:
`com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat
jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe.

308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS
bauen, das Image baut, und ein frischer Container läuft durch Einrichtung
und Anmeldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:38:27 +02:00
ScarriffleandClaude Opus 5 f98b995050 Beiwerk im Medienordner, und eine Zuordnung lässt sich korrigieren
Plakate, Hintergründe und NFO-Dateien landen jetzt neben dem Film — und eine
falsche Zuordnung lässt sich richtigstellen, ohne dass der nächste Scan sie
wieder überschreibt.

    Der Untergang (2004)/
    ├── Der Untergang (2004).mkv
    ├── poster.jpg
    ├── fanart.jpg
    └── movie.nfo

Bei Serien liegt `tvshow.nfo` eine Ebene über dem Staffelordner, je Folge
kommen NFO und Standbild daneben. Liegen mehrere Filme in einem Ordner, heisst
das Beiwerk `Titel (Jahr)-poster.jpg` — sonst überschrieben sie sich
gegenseitig.

**Die Erlaubnisliste ist der ganze Schutz.** Damit das geht, muss die
Medieneinhängung ohne `:ro` auskommen, und damit könnte der Server dort alles
anstellen. Dass er es nicht tut, hängt an `SidecarNames`: sechs Arten von
Datei, nichts sonst. Ein Name, der nicht daraufsteht, wirft — laut, nicht
still, denn das wäre ein Fehler im Programm. Mediendateien werden nie
geändert, umbenannt oder gelöscht; es gibt keine Stelle, die das könnte.
Geprüft an echten Dateien, einschliesslich der Versuche mit `../`, mit
absoluten Pfaden und mit einer Mediendatei als Ziel.

Standardmässig **aus**. In fremde Medienordner zu schreiben ist nichts, was
ein Server ungefragt tun sollte.

**Gelesen wird auch ohne Schreiberlaubnis.** Eine vorhandene NFO ist eine
Zuordnung, die jemand getroffen hat — Halo nimmt sie und sucht gar nicht
erst. Dasselbe gilt für Ordnernamen der Form `[tmdbid-613]`. Halo liest sie
und benennt **nichts** um: Umbenennen ist der einzige Eingriff, der eine
Bibliothek wirklich zerlegen kann.

Nebenbei: eine NFO aus dem Medienordner hat Halo nicht geschrieben. DTDs sind
beim Lesen abgeschaltet — eine untergeschobene Datei mit externer Entität
könnte sonst den Server dazu bringen, `/config/halo.db` einzulesen und als
Titel auszuliefern. Mit Prüfung.

---

**Der Fehler, den erst der echte Lauf zeigte** — und er ist die zweite
Auflage eines schon einmal behobenen:

Nach der Korrektur stand im Ordner „Dark (2017)", in der Datenbank aber 2004.
Der nächste Scan suchte nach *(Dark, 2017)*, fand nichts, legte den Film neu
an und löschte den korrigierten als verwaist. Überlebt hat die Zuordnung nur,
weil sie in der NFO stand — das System hat sich selbst gerettet, aber der
Eintrag war ein anderer und das Jahr wieder falsch.

Dieselbe Klasse wie bei `ParsedTitle`: zugeordnet wurde über einen Wert, den
die Metadatenquelle verändert. Jetzt gibt es `ParsedYear`, und das ändert sich
nie. Mit Prüfung, die genau diesen Ablauf festhält.

---

Dazu von TMDB: die IMDb-Kennung wird nachgeschlagen, damit sie in der NFO
steht — wer später auf ein anderes Werkzeug wechselt, findet denselben Titel
wieder, auch wenn das TMDB gar nicht kennt.

308 Tests grün. Im Container nachgewiesen: Zuordnung falsch, korrigiert,
zweiter Scan — derselbe Eintrag, dieselbe Kennung, Jahr bleibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:01:26 +02:00
ScarriffleandClaude Opus 5 79b6001f86 Metadaten ohne Einrichtung — mit Lese-Token statt API-Schlüssel
Halo bringt einen TMDB-Zugang mit. Wer den Server startet, bekommt Titel,
Beschreibungen und Plakate, ohne irgendwo ein Konto anzulegen. Am echten
Dienst nachgewiesen: „Der Untergang" (2004) mit deutscher Beschreibung,
„Blade Runner 2049" (2017), Plakate als JPEG aus Halos eigener Ablage.

**Warum überhaupt die Frage stand.** Jellyfin liefert ebenfalls einen
Schlüssel mit — fest im Quellcode, offen im Repository, Konstante `ApiKey` in
`TmdbUtils.cs`. Ein Abkommen mit TMDB dazu liess sich nicht belegen: deren
Bedingungen erwähnen das Teilen von Schlüsseln mit keinem Wort, und die
einzige Äusserung eines Moderators zu genau dieser Frage im TMDB-Forum ging
nicht darauf ein, sondern wies nur darauf hin, dass Missbrauch zur Sperrung
führt. Geduldet also, nicht gesegnet.

Deshalb **mit Ausweg**: wer einen eigenen Zugang hinterlegt, benutzt den. Würde
der mitgelieferte je gesperrt, steht damit nicht jede Installation still.

**Ein Lese-Token, kein API-Schlüssel.** TMDBs eigene Dokumentation bestätigt,
dass er auch an den v3-Endpunkten gilt. Zwei Gründe, und beide zählen bei
einem Zugang, der öffentlich im Repo steht: er kann nur lesen, und er geht als
`Authorization`-Kopfzeile hinaus statt als `?api_key=` in der Adresse — und
eine Adresse steht in jedem Zugriffsprotokoll und jedem Reverse Proxy. Der
ältere Schlüssel wird weiterhin angenommen; er kann nur in die Adresse.

**Der Hinweis auf TMDB ist Bedingung.** Logo und ein fester Satz, so verlangt
es deren Lizenz. `system/info` liefert beides ohne Anmeldung, damit ein Client
es auch auf dem Anmeldebildschirm zeigen kann. Das fehlte bisher.

---

**Zwei Funde, beide nur durch einen echten Lauf sichtbar:**

*„Dark" wurde zu „Dark Shadows" von 1966.* TMDB sortiert nach Beliebtheit, und
ohne Jahresangabe führt das in die Irre. Jetzt sticht ein Treffer mit genau
diesem Namen den beliebteren — danach: „Dark" (2017), deutsche Beschreibung,
Folgentitel „Geheimnisse" und „Lügen". Nur bei Gleichheit, nicht bei
Ähnlichkeit: eine eigene Bewertung wäre TMDBs Rangfolge nachgebaut, mit
weniger Daten.

*Die Tests griffen plötzlich übers Netz zu.* Sobald ein Schlüssel mitgeliefert
wird, fragt jeder Scan wirklich bei TMDB nach — und prompt hiess „Der
Untergang" im Test „Downfall", weil die Voreinstellung Englisch ist. Genau
das, was ein Testwerk nicht tun darf: am WLAN hängen und an fremden Daten.
Der Anbieter ist jetzt für alle Tests abgeschaltet, ausser ein Test schiebt
ausdrücklich einen eigenen nach.

238 Tests grün, keiner davon geht ins Netz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 17:52:48 +02:00
ScarriffleandClaude Opus 5 e4a90fce4e Stufe 3: Download mit allen Ton- und Textspuren
**Der Grund, warum es diesen Server gibt.** Eine MKV mit zwei Tonspuren geht
hinein, eine MP4 mit zwei Tonspuren kommt heraus — mitsamt Sprachkennung und
Spurnamen. Im Container nachgewiesen, nicht nur im Test:

    0 video     h264     und
    1 audio     aac      deu   Deutsch
    2 audio     aac      deu   Audiodeskription
    3 subtitle  mov_text deu

Jellyfins Transcode-Endpunkt bildet je Sitzung **eine** Tonspur ab und
verliert beim Umpacken zusätzlich die Sprachkennungen. Der Plan hier hat gar
kein Feld „welche Tonspur" — es gibt nichts auszuwählen, weil alle mitgehen.

**Vorher fragen können.** `GET /files/{id}/delivery` sagt, was geschehen wird:
umgepackt oder nicht, welche Tonspuren neu gerechnet werden müssen, welche
Untertitel nicht mitkönnen. Drei Gigabyte zu laden und erst danach zu merken,
dass die falsche Tonspur drin ist, war der Anlass für das Ganze.

**Umgepackt wird auf die Platte, nicht in die Antwort.** Der bequeme Weg wäre,
ffmpeg durchzureichen. Dann kennt aber niemand die Länge im Voraus: kein
Fortschrittsbalken, kein Fortsetzen, kein Springen. Jetzt kommt beim ersten
Abruf ein 202 mit Fortschritt, danach die fertige Datei mit `Content-Length`
und `Accept-Ranges`.

**Bildbasierte Untertitel fallen weg, und das steht im Plan.** PGS passt in
keine MP4 — betrifft fast jeden Blu-ray-Rip. Stillschweigend wegzulassen
hiesse, jemanden nach Untertiteln suchen zu lassen, die der Server bewusst
entfernt hat.

---

**Zwei Funde aus echten Messungen:**

*Die Spurnamen überlebten das Umpacken nicht* — „Deutsch" kam als `null` an,
und damit wären zwei deutsche Tonspuren hinterher nicht mehr zu unterscheiden
gewesen. Ein Versuch an einer echten Datei zeigte den Grund: **MP4 nennt das
Feld `name`, Matroska nennt es `title`.** ffmpeg schreibt das eine ins andere
um; mein Leser kannte nur `title`. Keine Grenze des Formats, ein Lesefehler.
`handler_name` steht ausdrücklich nicht in der Suche — dort schreibt ffmpeg
„SoundHandler" hinein.

*Der Image-Bau brach mit Code 100 ab.* `intel-media-va-driver` gibt es für
arm64 nicht, und die Meldung verrät nicht, welches von sechs Paketen fehlt.
Die Intel-Treiber kommen jetzt nur auf amd64 dazu; `mesa-va-drivers` bedient
AMD auf beiden.

---

**Die ffmpeg-Zeile ist eine reine Funktion und wird ohne Ausführen geprüft.**
Hier liegt die Erfahrung dieser Woche: eine falsche Parameterkombination
kostete Tage, weil sie nur gegen einen echten Server zu entdecken war. Dazu
gehört die Verwechslung, vor der jetzt ein Test steht: `-c:a:1` meint die
zweite Tonspur der **Ausgabe**, nicht Spur 1 der Datei.

ffmpeg läuft als eigener Prozess, nicht als eingebundene Bibliothek — das
hält die Lizenzen getrennt und lässt einen Absturz dort den Server nicht
mitnehmen.

223 Tests grün (98 Kern, 31 Metadaten, 26 Medien, 68 Schnittstelle).
Image baut für amd64 und arm64, mit ffmpeg 6.1.1 und VAAPI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 17:14:55 +02:00
ScarriffleandClaude Opus 5 deefc79fc1 Das Image nahm das neue Projekt nicht mit
`Halo.Metadata` fehlte in der Liste der Projektdateien, die vor dem
Wiederherstellen kopiert werden — der Bau auf dem Arbeitsrechner lief
weiter, der im Container wäre gescheitert. Genau die Art Fehler, die eine
Liste erzeugt: beim nächsten Projekt wird sie wieder vergessen.

Jetzt ein Muster statt einer Liste (`COPY --parents src/*/*.csproj ./`). Die
Ebenenzwischenablage für das Wiederherstellen bleibt dabei erhalten.

**Gegen den laufenden Container geprüft**, nicht nur gebaut: zwei Bibliotheken
angelegt, gescannt, Ergebnis abgerufen.

- Drei Filme gefunden, die danebenliegende `.srt` nicht.
- „Blade Runner 2049" behielt seine Zahl und bekam 2017 als Jahr.
- Sortiert ohne Artikel: Blade Runner, Matrix, Untergang.
- „Dark" mit zwei Folgen — mit `@eaDir` wären es drei gewesen.
- Folgentitel „Geheimnisse" und „Lügen" aus den Dateinamen gelesen.
- Ohne TMDB-Schlüssel meldet der Bericht das Nachschlagen als übersprungen,
  und die Mediathek steht trotzdem da.

Nebenbei bestätigt: die Meldung für einen Pfad, den es im Container nicht
gibt, traf zu, als Colimas Dateifreigabe `/private/tmp` nicht durchreichte.
Sie nannte genau den Grund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:55:41 +02:00
ScarriffleandClaude Opus 5 acfe2668a8 Stufe 2b: Metadaten von TMDB — und ein Testlauf, der 19-mal schneller ist
Titel, Beschreibungen und Bilder kommen jetzt von TMDB. Dazu zwei Fehler, die
erst das Zusammenspiel zeigte.

**Der Anbieter steckt hinter einer Schnittstelle, und das ist kein Vorrat für
einen zweiten.** Es geht darum, dass alles, was darauf aufbaut, *ohne Netz und
ohne Schlüssel* prüfbar bleibt. Ein Projekt, dessen Tests einen fremden Dienst
brauchen, hat am Ende keine Tests: sie scheitern, wenn das WLAN hakt, und
irgendwann schaltet sie jemand ab.

**Ein Fehlschlag bei TMDB ist kein Abbruch.** Kein Netz, Kontingent erschöpft,
ein Anmeldeportal im Hotel-WLAN, das HTML mit Status 200 schickt — in allen
Fällen bleibt der Titel beim Dateinamen und der Scan läuft durch. Die eine
Ausnahme ist ein zurückgewiesener Schlüssel: „nichts gefunden" und „dein
Schlüssel ist falsch" sehen in der Mediathek gleich aus, und ohne diese
Unterscheidung sucht jemand stundenlang. Der Schlüssel steht im Scanbericht.

**Der Schlüssel kommt nie wieder heraus.** Abgefragt wird nur, ob einer da
ist — sonst gäbe die Oberfläche jedem Angemeldeten das TMDB-Kontingent des
Betreibers in die Hand. Geprüft, dass er nicht einmal versehentlich im
Antwortkörper steht.

**Bilder holt die App bei Halo, nicht bei TMDB.** Sonst bräuchte sie Internet
für ein Plakat, das zehn Zentimeter weiter auf der Platte liegt — und TMDB
erführe, wer wann was ansieht. Der Schlüssel ist der Hash der Quelladresse,
also lädt dasselbe Plakat nie zweimal. Dass daraus kein Weg ins Dateisystem
wird, ist eigens geprüft: ohne die Formprüfung wäre
`/api/v1/images/../../config/halo.db` ein Weg, die Datenbank samt aller
Token-Fingerabdrücke herunterzuladen.

---

**Der erste Fehler: nach dem Eintragen der Metadaten legte der nächste Scan
den Film erneut an.** Zugeordnet wurde über den Titel — und den hatte TMDB
gerade von „The Matrix" auf „Matrix" geändert. Beim übernächsten Lauf dasselbe
noch einmal. Das ist die Sorte Fehler, die man erst nach Wochen bemerkt, wenn
jeder Film fünfmal dasteht. Jetzt gibt es `ParsedTitle`: den Titel aus dem
Dateinamen, der sich nie ändert, und nur der entscheidet beim Zuordnen.

**Der zweite Fehler kostete 15 Minuten je Testlauf.** Zwei Tests löschen die
Pfadvariablen, um den Rückfall auf die Voreinstellung zu prüfen — und liessen
sie gelöscht zurück. Jeder danach gestartete Testserver fiel dann auf
`<Projektverzeichnis>/data` zurück, und Dutzende Server teilten sich **eine**
SQLite-Datei, auf die sie der Reihe nach warteten. Dazu wartete jeder Host
beim Herunterfahren die vollen fünf Sekunden.

Gefunden durch Halbieren, nicht durch Raten: einzeln war jede Testklasse
schnell, zusammen nicht. Der Beweis lag am Ende als leeres Verzeichnis
`src/Halo.Api/data` auf der Platte.

**15 Minuten 32 → 49 Sekunden.** 180 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:52:46 +02:00
ScarriffleandClaude Opus 5 78b7e3bcb8 Stufe 2a: Der Bestand wird gelesen, nicht mehr nur verwaltet
Dateinamen erkennen, Verzeichnisse durchlaufen, abgleichen, blättern. Noch
ohne TMDB — die Titel kommen vorerst aus den Dateinamen, und genau daran
entscheidet sich später, ob die Metadatenquelle etwas findet.

**Der Dateiname ist die Stelle, an der Medienserver in der Praxis scheitern.**
Was auf der Platte liegt, heisst nicht „Der Fall", sondern
`Notruf.Hafenkante.S01E01.German.1080p.WEB-DL.x264-GRP.mkv`. Das Verfahren
schneidet ab dem ersten Technik-Merkmal ab, statt einzelne Tags
herauszulösen: Letzteres klingt gründlicher, lässt aber bei jedem unbekannten
Tag Bruchstücke stehen — und unbekannte Tags gibt es immer.

Drei Fälle, an denen sich die Regeln entscheiden, alle geprüft:

- **„Blade Runner 2049" behält seine Zahl.** Eine Jahreszahl muss plausibel
  sein; was weit in der Zukunft liegt, gehört zum Titel. Ohne diese Grenze
  hiesse der Film „Blade Runner", erschienen 2049, und keine Metadatenquelle
  fände ihn so.
- **„S.W.A.T." behält seine Punkte.** Trennpunkte werden nur dort zu
  Leerzeichen, wo sonst keine sind — stehen welche im Namen, hat jemand von
  Hand benannt, und seine Punkte sind gewollt. (Der Schlusspunkt fiel erst
  einem Test auf.)
- **Ohne Folgennummer wird nichts geraten.** Eine erfundene Staffel 1 landete
  unter einer Serie, zu der die Datei nicht gehört.

**`@eaDir` ist der Grund, warum der Walker von Hand absteigt.** Synologys
Vorschauablage enthält eine Kopie *jeder* Datei; mit
`SearchOption.AllDirectories` — der bequemen Fassung — stünde jeder Film
zweimal in der Mediathek, zur Hälfte als unbrauchbare Kopie. Äste beschneiden
kann nur, wer selbst absteigt. Ein gesperrter Unterordner bricht den Lauf
nicht ab, sondern wird vermerkt.

**Die Datei zeigt auf den Titel, nicht umgekehrt.** Sieht herumgedreht aus und
ist Absicht: ein Film liegt oft als 4K *und* als 1080p vor. Trüge der Film den
Verweis auf seine eine Datei, wäre die zweite Fassung ein Umbau am
Datenmodell — so ist sie eine Zeile mehr. Geprüft.

**Der Scan läuft im Hintergrund**, nicht in der Anfrage: ein Bestand mit
40 000 Titeln braucht Minuten, und eine so lange offene Anfrage läuft hinter
jedem Reverse Proxy ins Zeitlimit. Einer je Bibliothek, sonst löschen sich
zwei Läufe gegenseitig die Einträge. Aufgeräumt wird über den Zeitstempel des
Laufs — billiger, als sich alle gesehenen Pfade zu merken.

Zweimal scannen ergibt dasselbe wie einmal. Das ist der Fehler, den man sonst
erst nach Wochen bemerkt, wenn jeder Film fünfmal dasteht.

131 Tests grün (88 Kern, 43 Schnittstelle).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 13:56:25 +02:00
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
ScarriffleandClaude Opus 5 6326c8c24c Ein Apple-TV-Target, und es zeigt schon etwas
Das leere Gerüst: eigene Wurzel FlixyTVApp, eigenes Target FlixyTV in
project.yml, eigener Asset-Katalog. Es beweist Target, Icon, Signierung
und Paketauflösung auf einen Schlag — bevor eine Zeile Logik daran hängt.

**Das Icon von Hand gebaut.** Icon Composer deckt iOS, macOS und watchOS
ab, tvOS nicht: der Fernseher verlangt gestapelte Bilder, damit die
Parallaxe beim Wischen etwas zu bewegen hat. Zwei Ebenen aus derselben
Marke — Verlauf hinten, Dreieck vorn —, dazu App-Store-Icon und beide
Top-Shelf-Bilder. Ohne das breite lehnt App Store Connect den Upload mit
90513 ab.

**Zwei Annahmen aus dem Plan haben sich beim Bauen erledigt.**
PlexWebAuthenticator übersetzt nicht: ASWebAuthenticationSession steht
zwar im tvOS-SDK, aber presentationContextProvider, cancel() und der
Rest sind dort nicht verfügbar. Der Typ bleibt trotzdem stehen — als
Attrappe, die sofort „ging nicht" meldet. Ihn herauszuschneiden hiesse,
die Zustandsmaschine der Anmeldung für eine Plattform zu spalten, die
diesen Zweig nie betritt. Dieselbe Bauform wie bei OrientationLock.
NowPlaying wartet auf den Player in Schritt sieben.

**Und eine Messung statt einer Vermutung.** Der Plan führte zwei offene
Annahmen über den Sicherheitsrand. Der Simulator beantwortet beide:
SwiftUI legt ihn von selbst an, und er misst sechzig oben und unten,
achtzig seitlich — nicht neunzig. Nutzfläche 1760×960.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:18:03 +02:00
ScarriffleandClaude Opus 5 6010229b80 Die Pakete wissen jetzt, was ein Fernseher ist
Schritt eins zur Apple-TV-Fassung: nichts davon ist sichtbar, aber ohne
das lässt sich für tvOS nicht einmal übersetzen.

**Der stille Fehler zuerst.** `FlixyPlatform.isMac` hatte einen
`#else`-Zweig, der `true` zurückgab — eine Antwort auf eine Frage, die
niemand gestellt hatte. Auf tvOS fiel er dort hindurch: kein
Übersetzungsfehler, keine Warnung, nur ein Fernseher, der sich für einen
Mac hielt. Jetzt steht dort `#error`. Die nächste Plattform bricht den
Bau ab und verlangt eine Entscheidung, statt sich eine auszusuchen. Ein
Test hält fest, dass nicht beides zugleich gelten darf; er hätte genau
diesen Fehler gefangen.

**Ein echter Übersetzungsfehler**, und er lag an der Reihenfolge:
`UIPasteboard` steht hinter `canImport(UIKit)`, und das ist auf tvOS
wahr. Der tvOS-Zweig steht deshalb jetzt davor — ein Fernseher hat
ohnehin nichts, wohin man etwas einfügen könnte.

**Drei Vermutungen waren falsch**, nachgeschlagen im tvOS-SDK statt
geraten: `presentationDetents` gibt es dort seit tvOS 16 (übersetzt,
bewirkt nur nichts — jetzt steht es ausdrücklich da), `.topBarTrailing`
seit 14.0, und `isMac` hatte acht Fundstellen in fünf Dateien, nicht
fünfzig in siebenundzwanzig.

**Schrift und Abstände** bekommen einen eigenen Zweig statt durchzufallen.
Nicht feste Grössen: Dynamic Type gibt es auf tvOS auch, und dass `body`
dort grösser ausfällt, ist der Sinn der Sache. Falsch war nur die
Zuordnung der Rollen — die Skala ist anders gespreizt, und eine
Beschriftung in `subheadline` unter einem Plakat ist aus drei Metern
Zierrat. Der Aufmacher geht auf `.title`, weil UIKit `largeTitle` auf
tvOS gar nicht kennt und nirgends steht, was SwiftUI daraus macht.
`screenMargin` steht auf 48 und nicht auf 90: die neunzig Punkte des
Überscan-Bereichs liefert das System als Sicherheitsrand, und wer sie
hier aufschlägt, hat hundertachtzig.

**Zwei Dinge, die sich erst beim Bauen zeigten.** `FlixyDownloads`
übersetzte nicht — `volumeAvailableCapacityForImportantUsage` gibt es
auf tvOS nicht. Das Paket wird dort trotzdem mitverlinkt, damit
`PlayerModel` den Koordinator nennen darf, ohne dass zweitausend Zeilen
Wiedergabelogik von `#if` durchzogen werden; angelegt wird nie einer.
Und `FlixyProbe` liess das ganze Paket scheitern: ein
Kommandozeilenwerkzeug mit `getpass` und `Host`, das auf einem Fernseher
nie laufen sollte, hielt den Bau für alle anderen auf. Es ist jetzt
ausdrücklich macOS-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:09:18 +02:00
ScarriffleandClaude Opus 5 4576eac67c Das Bild füllt den Schirm, und die Schrift bleibt darin
Vier Dinge am iPhone-Player, die zusammengehören, weil sie alle davon
handeln, wo der Bildschirm aufhört.

**Einpassen oder füllen.** `videoGravity` stand fest auf `.resizeAspect`,
und zwar in einer Zeile, die nach jedem Neuaufbau erneut läuft — eine
Wahl hätte dort also jeden Qualitätswechsel nicht überlebt. Jetzt steht
sie im Modell, und die Zeile liest sie. Umgelegt wird mit zwei Fingern
oder über einen Knopf; stufenlos wäre falsch, weil alles zwischen den
zwei Zuständen ein Bild ist, das weder ganz zu sehen ist noch den Schirm
füllt. Auf dem Mac erscheint der Knopf nicht: dort steht der
Vollbildknopf darüber, und zwei Knöpfe, die beide „grösser" bedeuten,
sind eine Zumutung.

**Untertitel im Füllmodus.** Dort reicht das Bild über die untere
Bildschirmkante hinaus, und genau dort zeichnet AVFoundation die
Untertitel. Statt sie anzuschneiden, wird die Bildebene um den
Sicherheitsrand eingerückt — rund zwanzig Punkt Bild gegen eine Zeile,
die man lesen kann. Dafür bekommt die Bildfläche echtes Layout: eine
`autoresizingMask` hält Ränder verhältnismässig, und ein fester Einzug
von zwanzig Punkten wäre im Querformat plötzlich vierzig gewesen.

**Die Bedienleiste kannte ihre Ränder nicht mehr.** Der Player bekommt
`ignoresSafeArea()` von der Wurzel und sieht danach überall null —
Schieberegler und Zeitangaben sassen im Home-Indikator, der Zurück-Pfeil
hinter der Dynamic Island, wo ihn niemand drücken konnte. Gemessen wird
jetzt eine Ebene weiter aussen, wo die Ränder noch gelten, und über die
Umgebung nach innen gereicht. Dieselbe Bauart wie der Akzent. Die Zeile
steht ganz am Ende der Kette: stünde sie vor dem Overlay, umschlösse das
Overlay sie, und ausgerechnet der Player ginge leer aus.

**Gedreht wird einmal, gesperrt wird nicht.** Die Maske blieb auf
`.landscape` stehen — wer im Bett auf der Seite lag, konnte nicht mehr
ins Hochformat drehen. Ein Film beginnt quer, weil er quer ist; danach
gehört die Entscheidung dem Nutzer. Dazu ein Drehknopf für den Fall,
dass die Ausrichtungssperre des Systems an ist, und ein Fehlerbehandler
an `requestGeometryUpdate`: eine abgelehnte Drehung ist dann eine
Einstellung und keine Panne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 17:05:44 +02:00
ScarriffleandClaude Opus 5 1ff65e823a Ein Vollbildknopf im Player — wenn der Mac ihn annimmt
Fuer eine „fuer iPad angepasste\" App gibt es dafuer keine oeffentliche
Schnittstelle. UIKit kennt `isFullScreen` nur lesend und nur fuer Catalyst;
einen Umschalter gibt es nirgends. Das Fenster gehoert AppKit, und an
AppKit kommt diese App nicht heran.

Was bleibt, ist die Antwortkette: `toggleFullScreen:` ist dieselbe Aktion,
die der gruene Knopf und Strg-Cmd-F ausloesen, und UIKit reicht
unbeantwortete Aktionen an das tragende AppKit-Fenster weiter. Ob es das
tut, ist nirgends zugesagt.

Deshalb wird vorher gefragt statt hinterher gehofft: `target(forAction:)`
sagt, ob die Aktion jemand entgegennimmt, **ohne sie auszuloesen**. Erst
wenn es einen Empfaenger gibt, erscheint der Knopf. Ein Knopf, der nichts
tut, ist schlimmer als keiner — er laesst den Nutzer an sich zweifeln statt
an der Plattform.

Das Symbol zaehlt den Zustand selbst mit, weil er sich nicht abfragen
laesst. Wer mit dem gruenen Knopf umschaltet, bringt es aus dem Tritt; das
Umschalten funktioniert trotzdem, es zeigt nur kurz das falsche Bild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 06:31:46 +02:00
ScarriffleandClaude Opus 5 4323c47db6 Schneller anfangen, dann hinaufgehen
Es dauerte eine halbe Minute bis zum ersten Bild. Der Grund war nicht die
Leitung, sondern der Server: „Auto\" begann mit acht Megabit, und das heisst
1080p von der ersten Sekunde an. So lange braucht er, um das zu rechnen.

Jetzt wird mit drei Megabit begonnen — also 720p. Das laeuft in wenigen
Sekunden an, sieht einen Augenblick lang weicher aus, und von dort geht es
hinauf, sobald sich zeigt, dass die Leitung mehr traegt.

Dabei steckt eine Falle, die den ganzen Gewinn aufgezehrt haette: der
Server baut seine Leiter aus der Bestellung. Wer mit drei Megabit anfaengt,
bekommt eine Leiter, deren oberste Sprosse drei Megabit ist — und der
billige Hebel bewegt sich nur **innerhalb** dieser Leiter. Aus dem
schnellen Start waere ein Film geworden, der den ganzen Abend in 720p
bleibt.

Hoeher geht es deshalb ueber einen Neuaufbau, und der braucht Regeln: nicht
zu frueh, nicht nach einem Ruckler, nicht ohne Vorrat, nicht oefter als
zweimal. Sie stehen in `UpgradeRule` — reiner Typ, dreizehn Tests, wie beim
Absenken. Und deutlich zurueckhaltender als dort: ein zu spaetes Absenken
bedeutet Ruckeln, ein zu spaetes Hinaufschalten ein etwas weicheres Bild.
Das erste ist ein Fehler, das zweite eine Geschmacksfrage.

Bei Live-TV wird nicht hinaufgegangen: dort kostet jeder Neuaufbau den
Tuner, und ein schaerferes Bild ist diesen Einsatz nicht wert.

`StallLedger` kann jetzt sagen, wann der letzte Ruckler war — das brauchte
die Regel, und „noch nie gestockt\" ist etwas anderes als „vor langer Zeit\".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 06:25:15 +02:00
ScarriffleandClaude Opus 5 57170b20ea Der Untertitelstil wirkt jetzt sofort
`textStyleRules` wird nur gelesen, wenn die Untertitelspur ausgewaehlt
wird — eine laufende Einblendung zeichnet AVFoundation davon nicht neu.
Genau das war der Eindruck: man stellt um, es passiert nichts, und erst wer
die Untertitel aus- und wieder einschaltet, sieht die Aenderung.

Dieses Aus- und Einschalten **ist** die Neuwahl. Also machen wir sie selbst,
gleich nachdem der Stil gesetzt wurde.

Abgewaehlt und wieder angewaehlt wird dabei dieselbe Option, nicht die
erste: bei mehreren Spuren waere das ein stiller Sprachwechsel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 06:11:44 +02:00
ScarriffleandClaude Opus 5 c681d69fcc Fünf Fehler auf einem Weg — warum nie ein Untertitel kam
Sechs Reparaturversuche haben nichts bewirkt, weil jeder an einer Stelle
ansetzte, die gar nicht die Ursache war. Eine systematische Untersuchung —
45 Hypothesen, jede von drei Skeptikern gegengeprüft — hat fünf voneinander
unabhängige Defekte auf demselben Weg gefunden. Jeder einzelne genügt.

**1. Es wurde eine Playlist bestellt, die keine Untertitelspur tragen kann.**
Jellyfins TranscodingUrl lautet `main.m3u8` — eine Media Playlist, also eine
Liste von Schnipseln. `#EXT-X-MEDIA`, die Zeile, mit der ein Server eine
Untertitelspur ankündigt, darf nach RFC 8216 nur in einer Master Playlist
stehen. Der Wechsel main→master hing aber an der Leiter, und die wird beim
Umpacken und bei jeder von Hand gewählten Stufe nicht bestellt. Ein Matroska
geht immer über das Umpacken — die halbe Sammlung konnte strukturell keine
Untertitel haben.

Der Kommentar daneben war überholt und hat den Fehler verdeckt: er stammte
aus der Zeit, als Umpacken eine fortlaufende MP4 war. Seit es über HLS läuft,
ist die Master-Liste genau richtig. Pfadwechsel und Leiter sind jetzt zwei
Fragen, nicht eine.

**2. Der lokale Server etikettierte jeden Schnipsel als video/mp4.** Auch die
WebVTT-Schnipsel der Untertitelspur. So angekündigt gehen sie bei
AVFoundation an den falschen Parser, finden keine Box, und es entsteht
lautlos keine Einblendung — die Spur steht im Blatt, ist ausgewählt, bleibt
leer. Der Vorrat behält jetzt die Sorte der Gegenstelle; die Endung ist nur
noch der Notnagel.

**3. Nach dem ersten Untertitel wählte niemand mehr etwas aus.**
`selectedSubtitleTrack` wurde beim Neuaufbau nie zurückgesetzt, und beide
Wähler prüfen genau darauf. Eine frische Engine hat aber nichts ausgewählt —
also blieb sie für den Rest des Films stumm, während das Symbol „ein" zeigte.
Auch das Nachfassen im Sekundentakt war damit stillgelegt. Die Bestellung
(`selectedSubtitleStream`) bleibt, die Merker fallen.

**4. Die Schutzmarke war nach 250 ms tot.** `restart()` setzte sie als
Allererstes — und wartete danach auf einen echten Netzaufruf mit fünf
Sekunden Zeitgrenze, bevor es den alten Ereignisstrom abbestellte. In diesem
Fenster meldete der alte Player weiter seine Stelle, und die Prüfung „sind
wir angekommen" traf sofort zu. Die Marke war gelöscht, bevor der neue Stream
bestellt war; danach setzte jede Meldung des anlaufenden Streams die Stelle
auf null, und der nächste Neuaufbau merkte sich den Filmanfang. Das war der
Sprung — und der Grund, warum mein Fix von 88ba1a5 wirkungslos blieb.

**5. Der Sprung wurde einmal versucht und nie geprüft.** Der Rückgabewert von
`seek` wurde verworfen, die gemerkte Stelle verbraucht, bevor gesprungen
wurde, und framegenaue Toleranz verlangt — in einen Stream, den der Server
gerade erst erzeugt. Jetzt bleibt die Stelle stehen, bis ein Sprung bestätigt
ist, die Toleranz liegt bei einer halben Sekunde, und jedes Wachsen des
springbaren Bereichs ist ein neuer Anlass.

Belegt statt vermutet: ein neuer Test startet den lokalen Server gegen einen
eigenen Herkunftsserver und liest die Antworten Byte für Byte — ohne Gerät,
ohne Simulator, in 0,03 Sekunden. Er hat Fehler 2 vor der Reparatur
reproduziert und danach bestätigt. Fünf weitere Tests halten Fehler 1 fest.

Zwei alte Tests hielten das fehlerhafte Verhalten fest und waren damit
mitschuldig; beide sind mitgeändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 06:04:40 +02:00
ScarriffleandClaude Opus 5 8e0e8ec246 Der Vorrat gibt dem Player Vorrang und weicht, wenn er im Weg steht
Sobald das Umrechnen teuer wird — und das Einbrennen eines Untertitels ist
teuer —, bedient Jellyfin einen Schnipsel nach dem anderen. Der Vorrat hat
dabei gleichzeitig weitergeholt und dem Player die Leitung weggenommen:
zwei Anfragen machen die dringende langsamer, nicht schneller. Herauskam
„Es kommen keine Daten mehr\".

Jetzt ruht der Vorrat, solange eine Anfrage des Players offen ist.

Dazu ein Rueckzugsweg, der gefehlt hat. Der Speicher steht zwischen Player
und Server, und was dort schiefgeht, sieht von aussen aus wie eine tote
Leitung. Kommt nach zwoelf Sekunden nichts an, wird er weggenommen und es
laeuft ohne ihn noch einmal an — das kostet nur den Vorsprung und
unterscheidet die beiden Faelle, statt sie zu verwechseln. Erst danach die
kleinere Bitrate, erst danach das Aufgeben.

Und drei Zeilen ins Protokoll, die drei Runden Ratens erspart haetten: was
bestellt wurde, was der Player daraus meldet, und welche Spur eingeschaltet
wurde. „Nicht geliefert\" und „geliefert und nicht angezeigt\" sehen von
aussen gleich aus — im Protokoll nicht mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:50:57 +02:00
ScarriffleandClaude Opus 5 5e33971452 AVPlayer hat die Untertitel wieder abgeschaltet
Der Server lieferte sie, die Spur lag im Stream, ausgewaehlt war sie auch —
und angezeigt wurde trotzdem nichts.

Der Grund ist eine Eigenschaft, die von Haus aus auf `true` steht:
`appliesMediaSelectionCriteriaAutomatically`. Der Player richtet sich damit
nach den Untertiteleinstellungen des Systems, und die stehen in aller Regel
auf „aus\". Er nahm also jede Wahl, die wir trafen, wieder zurueck —
lautlos, und erst nachdem wir sie gesetzt hatten.

Beim Ton aendert das Abschalten nichts: welche Spur klingt, ist laengst
beim Aushandeln entschieden, und bei einem umgerechneten Stream kommt
ohnehin nur eine an.

Dazu eine zweite Absicherung: bei einer Abspielliste laedt AVFoundation die
Nebenspuren nachtraeglich. Meldet sie beim ersten Mal noch keine, ginge der
bestellte Untertitel verloren — deshalb wird im Sekundentakt nachgefasst,
solange nichts ausgewaehlt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:40:30 +02:00
ScarriffleandClaude Opus 5 cff0c06745 Untertitel brachten den Vorratsspeicher auf 404
Untertitel stehen in der Abspielliste als eigene Spur unter EXT-X-MEDIA —
eine Anweisung mit einer Adresse darin. Umgeschrieben habe ich bisher nur
EXT-X-MAP, also blieb diese relativ stehen und zeigte danach auf den
lokalen Server, wo es sie nicht gibt. Zu sehen war der Fehler erst, als
jemand Untertitel einschaltete: HTTP 404, mitten im Film.

Jetzt sagt der Umschreiber, **was** fuer eine Adresse er gerade in der Hand
hat: eine Qualitaetsstufe, eine Nebenspur, ein Kopfstueck oder ein
Schnipsel. Das ist noetig, weil sie Verschiedenes wollen — hinter einer
Stufe liegt Bild und die soll vorgeladen werden, hinter einer Untertitel-
spur nicht. Sonst lueden wir Untertitel statt Bild: der Film bliebe stehen,
waehrend die Anzeige reichlich Vorrat meldet.

Dazu wandern alle Adressen im lokalen Server auf die **vollstaendige**
Form. Eine relative gilt je nach Liste gegen eine andere Grundadresse — die
Untertitelspur liegt woanders als das Bild —, und wer das erst beim Abholen
aufloest, loest es irgendwann gegen die falsche auf. Das waere der naechste
Fehler derselben Art gewesen.

Drei Tests halten die Unterscheidung fest, darunter die Liste mit
Untertitelspur, die den 404 ausgeloest hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:36:35 +02:00
ScarriffleandClaude Opus 5 88ba1a540c Untertitel werden auch gezeigt, und die Stelle geht nicht mehr verloren
Zwei Fehler, die beide beim Umstellen im laufenden Film auftraten.

Der Server lieferte den gewaehlten Untertitel brav mit — nur sagte danach
niemand dem Player, dass er ihn **zeigen** soll. Gewaehlt wurde allein in
`applyPreferredLanguages()`, und das greift nur, wenn in den Einstellungen
eine Untertitelsprache steht. Wer keine eingestellt hatte, schaltete die
Untertitel ein und sah nichts. Jetzt wird die bestellte Spur nach dem
Neuaufbau angezeigt; bei einem umgerechneten Stream liegt genau eine drin,
naemlich die bestellte, also gibt es nichts zu verwechseln.

Und der Sprung an den Filmanfang, der „teilweise\" passierte: ein frisch
geoeffneter Stream meldet erst einmal null, eins, zwei — er springt erst
danach an die gemerkte Stelle. Wer in diesem Augenblick `currentTime`
liest, liest zwei Sekunden statt einundzwanzig Minuten. Genau das tut ein
zweiter Neuaufbau, wenn man zuegig noch etwas umstellt: er merkt sich die
falsche Stelle. Daher trat es nicht immer auf, sondern nur manchmal.

Waehrend auf die gemerkte Stelle gewartet wird, gelten die Meldungen des
anlaufenden Streams jetzt nicht mehr — dasselbe Prinzip wie beim Spulen,
wo die Nachzuegler der alten Stelle verworfen werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:30:26 +02:00
ScarriffleandClaude Opus 5 0815432efd Untertitel lassen sich jetzt einstellen
Groesse in vier Stufen, Schrift und Hintergrund als fuenf feste Paare —
Weiss, Weiss auf Schwarz, Schwarz auf Weiss, Gelb, Gelb auf Schwarz. Feste
Paare statt zweier Regler, weil unabhaengige Achsen unlesbare Mischungen
zulassen: Schwarz auf durchsichtig ueber einer Nachtszene ist eine
gueltige Einstellung und eine wertlose.

Die Uebersetzung nach AVFoundation ist eine Tabelle aus Schluesseln, bei
der jeder Fehler lautlos ist — ein falscher Schluessel laesst den
Initialisierer `nil` zurueckgeben, und dann gibt es gar keine Regel. Auf
dem Bildschirm sieht „keine Regel\" genauso aus wie „Regel ohne Wirkung\".
Zehn Tests decken das ab, darunter die drei Fallen: Alpha zuerst in der
Farbe, Prozentwert statt Punktgroesse, und der Hintergrund auch fuer
„durchsichtig\" ausdruecklich gesetzt statt weggelassen — weggelassen gilt
sonst, was Jellyfins WebVTT selbst mitbringt, und das ist oft ein
schwarzer Kasten.

Als Protokollmethode, nicht als Feld an `PlaybackSource`: die wird einmal
beim Oeffnen gelesen, und eine Einstellung, die erst beim naechsten Film
wirkt, haelt jeder fuer kaputt. Angewandt wird an zwei Stellen — am
laufenden Gegenstand und am frisch angelegten. Nur das erste zu tun hiesse,
dass die naechste Folge wieder in der Vorgabe erscheint.

VLC erbt die leere Vorgabe. Die Einstellungsseite sagt beides: dass es fuer
den Standardplayer gilt, und dass eingebrannte Bilduntertitel von Blu-rays
sich nicht gestalten lassen, weil es dort keinen Text gibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:22:22 +02:00
ScarriffleandClaude Opus 5 a1ff5cb7bb Von der Kachel direkt abspielen, auch bei Serien
Wer weiss, was er sehen will, klickte bisher auf die Kachel, wartete auf
die Detailseite und drueckte dort „Abspielen\". Zwei Schritte fuer eine
Absicht. Jetzt gibt es oben rechts einen Knopf — auf dem Mac beim
Darueberfahren, auf iPad und iPhone ueber langes Druecken.

Bei einer Serie ist die Absicht nicht „spiel die Serie\", sondern „mach da
weiter, wo ich war\". Welche Folge das ist, entscheidet `EpisodeSelection`:
die aelteste ungesehene, fortgesetzt falls angefangen, und von vorn, wenn
alles gesehen ist. Ein reiner Typ mit achtzehn Tests — im App-Ziel gibt es
kein Unit-Test-Ziel, und „er hat die falsche Folge gestartet\" ist sonst ein
Befund, den man nicht nachstellen kann.

Ausdruecklich die aelteste ungesehene, nicht die am weitesten
fortgeschrittene: wer sich eine Folge aufgehoben hat, soll sie nicht
dadurch verlieren, dass er weiter vorn hineingeschaut hat.

Dazu `episodes(inSeries:)` im Vertrag — bei Jellyfin eine Anfrage statt
dreizehn, fuer alle anderen die Vorgabe ueber `children(of:)`. Nicht
`/Shows/NextUp`: der braucht eine Seriennummer, die eine Serie selbst nicht
hat, versteckt vollstaendig gesehene Serien und bringt eine eigene
Zeitgrenze mit. Welche Folge dran ist, soll eine Regel entscheiden, nicht
der Server.

Nebenbei behoben: der grosse Abspielknopf im Aufmacher scheiterte bisher
mit „keine abspielbare Datei\", sobald dort eine Serie stand — und Jellyfins
„Zuletzt hinzugefuegt\" liefert neue Folgen als ihre Serie.

Der Knopf liegt als Ueberzug auf dem NavigationLink, nicht darin: ein
Button im Label eines Links loest auf iOS nie aus, und `.plain` macht das
Versagen lautlos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 21:18:39 +02:00
ScarriffleandClaude Opus 5 2a2afa8573 Der Aufmacher bleibt stehen, und Folgen bekommen ein Bild
Drei Sachen am Karussell.

Die Vorschlaege wechselten bei jedem Start, obwohl es dafuer laengst ein
Zeitfenster von sechs Stunden gab. Der Grund: das Los hing an der Position
in der Liste, nicht am Titel. Kam einer dazu, verschwand einer, oder
aenderte sich ein Fortschritt, bekamen alle dahinter ein anderes Los — und
„Weiterschauen\" waechst und schrumpft staendig. Jetzt haengt das Los an der
Kennung; wer dazukommt, stoert niemanden. Als eigener Typ mit elf Tests,
weil „es wechselt zu oft\" kein Befund ist, den man debuggen kann.

Folgen zeigten kein Bild. Die Kette Folge → Serie gab es zwar, aber sie
hoerte beim ersten Glied auf, das sich **zusammensetzen** liess — und ob es
das Bild dahinter gibt, stellt sich erst beim Abruf heraus. Die Reihenfolge
wandert deshalb ins Laden: Folge, Staffel, Serie, der Reihe nach, bis eine
Adresse antwortet.

Und auf dem Mac gibt es jetzt Pfeile links und rechts, sobald der Zeiger
ueber dem Aufmacher liegt, sowie Punkte, die unter dem Zeiger wachsen und
auf die man springen kann. Nur dort: auf dem iPad und dem iPhone wischt man
ohnehin, und zwei Knoepfe staenden nur im Bild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 20:46:34 +02:00
ScarriffleandClaude Opus 5 4b084bc926 Ein Sprung in den Vorrat laeuft jetzt sofort weiter
Drei Gruende, und der erste erklaert das ganze Aergernis.

AVPlayer besteht nach einem Sprung darauf, erst seinen eigenen Puffer zu
fuellen. Der stand auf sechzig Sekunden — richtig, solange er selbst
vorladen sollte. Mit einem Speicher davor ist es das Gegenteil: springt man
mitten in den Vorrat, liegt dahinter vielleicht noch eine halbe Minute, zu
wenig fuer seine sechzig, und er wartet auf den Server, obwohl alles da
ist. Der Abstand liegt jetzt bei uns; seiner darf klein sein.

Der Vorrat holte ausserdem weiter Schnipsel hinter der Abspielstelle, wenn
der Player vorausgesprungen war — und der eine, auf den gerade gewartet
wurde, kam als letzter. Er macht jetzt dort weiter, wo gesprungen wurde.

Und dieselbe Liste noch einmal zu holen setzte den Vorrat zurueck. Der
Player holt sie staendig: nach einem Sprung, beim Stufenwechsel, von sich
aus. Zurueckgesetzt wird nur noch bei einem echten Wechsel der Quelle.

Dazu ein latenter Fehler, der beim Umpacken ein Schwarzbild gegeben haette:
in EXT-X-MAP steckt die Adresse des Kopfstuecks einer fragmentierten MP4,
ohne das kein einziger Schnipsel zu dekodieren ist. Sie blieb stehen und
zeigte damit relativ auf den lokalen Server. Der alte Test hielt genau
dieses Verhalten fest — er hat mitgeaendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 20:36:33 +02:00