Commit Graph
100 Commits
Author SHA1 Message Date
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
ScarriffleandClaude Opus 5 bbd1c3b572 Der Vorrat steigt jetzt auch eine Ebene tiefer
Bei „Auto\" bestellen wir eine Liste von Stufen; die Schnipsel stehen eine
Ebene darunter. Der Speicher erkannte das zwar, stieg dann aber aus und
reichte die Liste mit relativen Adressen durch — die zeigten danach auf den
lokalen Server, wo es sie nicht gibt. Es wurde nichts vorgeholt, und
beinahe waere auch nichts mehr gelaufen.

Jetzt wird auch die Stufenliste umgeschrieben. Holt der Player eine Stufe,
kommt er wieder bei uns an, und dort beginnt das Vorauslaufen — bei der
Stufe, fuer die er sich entschieden hat.

Dazu eine Zeile im Qualitaetsblatt, die sagt, ob der Vorrat laeuft und wie
weit er voraus ist. Von aussen ist das nicht zu sehen: laeuft er nicht,
spielt es trotzdem, nur ohne Vorsprung — und genau so sieht auch ein
Vorrat aus, der laeuft und nichts bekommt. Ohne diese Zeile sind die beiden
Faelle nicht zu unterscheiden, und ich haette wieder geraten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 20:29:20 +02:00
ScarriffleandClaude Opus 5 134dc82927 Der Vorrat laeuft jetzt voraus, nicht der Player
AVPlayer entscheidet selbst, wie weit er puffert, und bei einer Abspielliste
laesst Apple uns nicht dazwischen: auf eine Anfrage nach einem Schnipsel
nimmt er von uns nur eine Weiterleitung an, keine Daten. Der Denkfehler war,
seinen Puffer vergroessern zu wollen.

Wir muessen aber gar nicht dazwischen — wir koennen die Gegenstelle sein.
Ein winziger HTTP-Server auf 127.0.0.1 liefert dem Player die Abspielliste,
umgeschrieben auf sich selbst, und holt die Schnipsel zuegiger von Jellyfin,
als abgespielt wird. Jedes Segment, das der Player verlangt, liegt dann
schon lokal. Faellt die Verbindung im Tunnel aus, merkt er es nicht.

Das ist der Unterschied zwischen Puffer und Vorrat: den Puffer bestimmt
Apple, den Vorrat bestimmen wir.

Streng der Reihe nach, und mit begrenztem Vorsprung. Der Server erzeugt die
Schnipsel gerade erst; wer Nummer 40 verlangt, waehrend er bei 12 ist, sieht
fuer Jellyfin aus wie jemand, der gesprungen ist — und dann beginnt das
Umrechnen von vorn, an der falschen Stelle.

Der Lauscher haengt ausdruecklich an 127.0.0.1, nicht an allen
Schnittstellen. Ein Server, der den Film ins Netz stellt, waere das Letzte,
was eine Mediathek-App tun sollte.

Kommt er nicht hoch, spielt es trotzdem — ueber die urspruengliche Adresse,
ohne Vorrat. Ein fehlender Vorrat ist ein Verlust an Bequemlichkeit; keine
Wiedergabe ist ein Verlust an allem.

Das Lesen und Umschreiben der Abspielliste ist ein eigener Typ ohne Netz,
mit sieben Tests. Eine falsch umgeschriebene Liste ergibt entweder ein
schwarzes Bild oder — schlimmer — eine, die stellenweise am Speicher
vorbeigeht und nur manchmal hakt. So etwas findet man im laufenden Film
nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 20:23:14 +02:00
ScarriffleandClaude Opus 5 46550fc30b Umgepackt wird als HLS, und ein Fehlschlag faellt auf VLC zurueck
Der Versuch mit einer fortlaufenden MP4 ist am Geraet gescheitert:
AVErrorServerIncorrectlyConfigured (-11850), sofort. Der Grund ist
einleuchtend, sobald man ihn kennt — AVPlayer verlangt bei einer
fortlaufenden Datei Bereichsanfragen, er will hineinspringen koennen. Eine
MP4, die ffmpeg gerade erst erzeugt, kann das nicht: sie hat weder Laenge
noch ein Inhaltsverzeichnis am Anfang.

Das war eine Annahme, die ich haette messen sollen, bevor ich darauf baue.

Vom Ziel bleibt damit das Umpacken, nicht das weite Vorausladen — und das
ist immer noch der groessere Teil: ein Matroska-Rip mit HEVC und AC3 landet
bei AVPlayer statt bei VLC, mit Bild-in-Bild und AirPlay, ohne dass ein Bild
neu gerechnet wird. `Container: mp4` statt `ts`, weil HLS als fragmentierte
MP4 auch HEVC traegt; in MPEG-TS waere dafuer eine Neukodierung noetig,
also genau das, was hier vermieden werden soll.

Dazu ein Rueckzugsweg, der vorher gefehlt hat. Ob der Server die Stroeme
wirklich kopieren kann, entscheidet sich erst beim Ausliefern, und manche
Antworten nimmt AVPlayer nicht an. Das vorher zu wissen ist nicht moeglich;
es hinterher aufzufangen schon. Scheitert das Umpacken, laeuft derselbe
Titel einmalig mit VLC an, statt den Nutzer vor eine schwarze Wand mit
einer Fehlernummer zu stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 20:11:49 +02:00
ScarriffleandClaude Opus 5 62691fc2bc Matroska geht jetzt an AVPlayer, nicht mehr an VLC
Ein Blu-ray-Rip liegt als Matroska vor, und AVFoundation oeffnet Matroska
nicht — obwohl das Bild darin H.264 oder HEVC ist und der Ton AC3, also
alles, was AVPlayer bestens kann. Im Weg stand allein die Verpackung.

Dafuer gibt es jetzt einen dritten Weg neben Durchreichen und Neurechnen:
umpacken. Dieselben Stroeme, Byte fuer Byte, in eine MP4. Kein Bild wird
angefasst, es geht nichts an Qualitaet verloren, und den Server kostet es
fast nichts.

Entscheidend ist dabei `Protocol: http` statt `hls`. Eine Abspielliste
laesst sich nicht vorausladen — AVPlayer holt Segment fuer Segment, und
Apple laesst uns dazwischen nicht eingreifen. Eine fortlaufende Datei kommt
ueber gewoehnliche Bereichsanfragen, und die kann man so weit vorausholen,
wie man will. Der Vorratswunsch steht dort deshalb bei zehn Minuten statt
bei einer.

Der Gewinn ist gross: solche Titel bekommen Bild-in-Bild, AirPlay und die
Systembedienung, die es bei VLC nicht gibt.

Umgepackt wird nur, wenn **allein** die Schachtel im Weg steht. Stimmt auch
nur ein Codec nicht — DTS, TrueHD, AV1 —, fehlt nicht der Zugang zur Datei,
sondern das Koennen; dann bleibt es bei VLC. Sechs Tests halten die Grenze
fest, in beide Richtungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 20:00:44 +02:00
ScarriffleandClaude Opus 5 806eb381c7 Beim Durchreichen darf viel weiter vorgeladen werden
Es sind zwei verschiedene Lagen, und sie hatten bisher dieselbe Zahl.

Rechnet der Server um, existieren die Schnipsel noch gar nicht — ffmpeg
erzeugt sie gerade, und weiter voraus als bis dorthin kommt niemand. Eine
Minute ist dort reichlich; ein groesserer Wunsch liefe ins Leere.

Wird die Datei unveraendert durchgereicht, liegt sie fertig auf der Platte
und kommt ueber gewoehnliche Bereichsanfragen — dieselbe Lage wie bei einem
Video im Netz, das man beliebig weit vorausladen kann. Dort bei einer
Minute aufzuhoeren war schlicht zu schuechtern; jetzt sind es zehn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 19:38:00 +02:00
ScarriffleandClaude Opus 5 f48e018fa3 Der Regler springt sofort, und man sieht, bis wohin geladen ist
Zwei Dinge, die beide dasselbe Missverständnis erzeugt haben.

Der Regler blieb beim Spulen liegen, wo er war, und rückte erst nach, wenn
das erste Bild von der neuen Stelle da war — über das Netz Sekunden später.
Das liest sich als „der Sprung ist nicht angekommen", und man spult noch
einmal. Der Sprung ist aber angekommen, in dem Moment, in dem man ihn
auslöst; alles Weitere ist Warten auf Daten, und dafür gibt es den Kreisel.
Die Stelle steht jetzt sofort. Nachzügler von der alten Stelle werden
verworfen, sonst zöge die letzte Meldung den Regler wieder zurück.

Und bis wohin geladen ist, stand nirgends. Dafür ist der `Slider` gewichen:
er hat genau eine Spur, und die gehört dem Fortschritt. Etwas dahinter zu
zeichnen und auf gleiche Innenabstände zu hoffen ginge so lange gut, bis
Apple sie ändert. Der neue Regler zeichnet drei Schichten aus derselben
Rechnung — Grund, Geladen, Gespielt — und hat einen Griff, den ein Finger
trifft.

Der Vorrat wird dabei abgelegt statt ausgerechnet. Eine berechnete
Eigenschaft wird nur gelesen, wenn ohnehin neu gezeichnet wird, und das
geschieht, weil die Abspielstelle weiterläuft. Im pausierten Zustand läuft
sie nicht — und ausgerechnet da will man zusehen, wie der Vorrat wächst.
Die Schleife, die ihn nachführt, lief bisher ausserdem nur bei
„automatisch": wer eine Stufe von Hand gewählt hatte, hätte nie gesehen,
wie weit geladen ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 19:28:44 +02:00
ScarriffleandClaude Opus 5 67c679006f Untertitel kommen aus der Datei, und Fortsetzen geht wieder
Im Spurenblatt stand „CC" und sonst nichts — derselbe Fehler wie beim Ton:
das ist AVFoundations Name für den einen Untertitel, den ein umgerechneter
Stream mitbringt. Zur Auswahl steht jetzt, was in der Datei liegt, und die
Nummer geht als `subtitleStreamIndex` mit in die Anfrage.

`-1` heisst dabei ausdrücklich „keine". Das ist nicht dasselbe wie nichts zu
schicken: dann sucht der Server sich einen aus, und es steht plötzlich ein
Untertitel im Bild, den niemand bestellt hat.

Und der Klick ins Bild: Anhalten ging, Fortsetzen nicht. Der Abdunkler
hinter der Bedienung deckt das ganze Fenster ab und lag über dem Film —
solange die Bedienung zu sehen war, landete jeder Klick auf ihm. Auffallen
konnte das nur an einer Stelle: nach dem Pausieren bleibt die Bedienung
stehen, also traf der Klick, der wieder starten sollte, nie das Bild. Der
Abdunkler ist jetzt unantastbar; er dunkelt ab, mehr nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 19:14:07 +02:00
ScarriffleandClaude Opus 5 60194c2d2a Die Bedienung blieb stehen, weil der Zeiger sich nie bewegt hat
Seit die Bedienung dauerhaft montiert ist, zeichnet der Scrubber darin im
Sekundentakt neu. `onContinuousHover` meldet nicht nur bei Bewegung, sondern
auch bei jedem Neuzeichnen — also setzte die Ausblendfrist sich endlos
selbst zurück. Die Bedienung blieb für immer stehen, und die Überblendung,
die dadurch nie stattfand, sah aus wie eine kaputte Animation. Sie war nie
das Problem; das war sie schon beim letzten Anlauf nicht.

Jetzt gilt der Zeiger nur als bewegt, wenn er an einer anderen Stelle ist
als zuletzt.

Und der Klick in den schwarzen Rand: ich habe zweimal ausgerechnet, wo das
Bild aufhört, und lag zweimal daneben. Jetzt rechnet das Layout. Die
Bildfläche ist auf ihr Seitenverhältnis zugeschnitten und hat ihren eigenen
Griff; der schwarze Rand ist schlicht der Hintergrund dahinter. Was am
Player ankommt, ist damit per Definition daneben gegangen — es muss niemand
mehr feststellen.

Damit fallen `videoRect`, `videoFrame`, `playerSize`, der benannte
Koordinatenraum und die Abfrage der `AVPlayerLayer` ersatzlos weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 18:01:27 +02:00
ScarriffleandClaude Opus 5 983015b9ad Die Sprache lässt sich jetzt auch je Serie und je Mediathek sagen
Das Regelwerk stand schon und wurde abgefragt — es gab nur keinen Ort, an
dem man etwas hineinschreiben konnte. Jetzt steht die Wahl auf der
Serienseite und im Filter-Sheet der Mediathek, beide über denselben
Auswahlknopf: zwei mit demselben Inhalt liefen unweigerlich auseinander.

Bei der Mediathek in einem eigenen Abschnitt, nicht zwischen den Filtern.
Ein Filter schränkt ein, was zu sehen ist; das hier bestimmt, was zu hören
ist — dazwischengestellt läse es sich als „nur Titel mit deutscher Tonspur
zeigen".

Die Rangfolge Serie → Mediathek → allgemein ist dabei von den Einstellungen
zu `AudioTrackChooser.resolve` gewandert. Sie ist eine Regel, keine Ablage,
und im App-Ziel gibt es kein Unit-Test-Ziel — dort liesse sie sich nicht
durchspielen. Sechs Tests decken sie jetzt ab, darunter der Fall, an dem
man sonst lange sucht: eine leere Zeichenkette steht in den Einstellungen
für „soll der Server entscheiden" und darf nicht als Sprache gelten, sonst
schaltet sie die Ebene darüber stillschweigend ab.

Nebenbei bekommt das Filter-Sheet die Mediathek selbst statt nur ihrer Art.
Die Genres wurden vorher über irgendeine Mediathek dieser Art nachgeschlagen
— bei zwei Filmmediatheken also über die falsche.

Und der Puffer steht auf einer Minute statt auf 25 Sekunden. Es ist ein
Mindestwunsch, keine Obergrenze; der harte Deckel liegt ohnehin woanders,
nämlich darin, wie weit der Transcoder schon produziert hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 17:12:17 +02:00
ScarriffleandClaude Opus 5 596a143545 Die Tonspur wird aus der Datei gewählt, nicht aus dem Stream
Im Spurenblatt stand eine einzige Zeile: „Unbekannt". Das ist der Name, den
AVFoundation einer namenlosen Spur gibt — und bei einem umgerechneten
Stream liefert der Server genau eine. Der Player hatte nichts zur Auswahl,
weil die Wahl längst gefallen war, bevor er gefragt wurde.

Gewählt wird jetzt aus den Spuren, die **in der Datei** stehen, und zwar
bevor der Stream ausgehandelt wird. Die Nummer geht als `audioStreamIndex`
mit hinaus; wer mitten im Film umstellt, bekommt den Stream neu — ausser
bei unveränderter Wiedergabe, wo der Player ohne Unterbrechung umschaltet.

Die Abwägung selbst macht `AudioTrackChooser`: gemerkte Spur, dann
Wunschsprache, dann Rangliste, dann Standardspur — und Audiodeskription
mit deutlichem Abzug statt Ausschluss, damit eine Sprache, die es nur so
gibt, immer noch besser ist als Stille.

Drei Ebenen in `LanguagePreferences`: Serie, Mediathek, allgemein. Gefragt
wird von unten nach oben. Die Mediathek wird nur nachgeschlagen, wenn
überhaupt jemand eine Sprache je Mediathek gesetzt hat — sonst kostete jede
Wiedergabe eine Anfrage für eine Ebene, die niemand benutzt.

Wer von Hand umstellt, dessen Wahl wird für die Serie gemerkt, ohne
Nachfrage. Der Dialog „für diese Serie merken?" ist genau die Art, die man
nach dem dritten Mal wegklickt. Sichtbar ist es trotzdem: das Spurenblatt
zeigt es und lässt es löschen.

Dazu die Rangliste in den Einstellungen, ziehbar, und der Schalter
„Audiodeskription meiden".

Und die Player-Oberfläche blendet endlich als Ganzes über. Die Kopfzeile
hatte gar keine Überblendung — sie sprang herein, während die Bedienung
darunter verblasste. Jetzt steht die Animation an einer Stelle statt an
sechs, und sie läuft über Deckkraft statt über Einfügen und Entfernen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 16:31:38 +02:00
ScarriffleandClaude Opus 5 2dbe260901 Mediatheken stehen sofort da statt sich jedes Mal neu zu laden
Jeder Aufruf einer Mediathek war ein Serverabruf mit Ladekreis davor. Die
Plakate lagen längst auf der Platte; was fehlte, war die Liste, die sagt,
welche davon hierhergehören.

Jetzt liegt die erste Seite als Abzug daneben: beim Aufrufen steht sie
sofort da, der Server wird still nachgefragt, und man sieht in aller Regel
nichts davon. Antwortet er nicht, bleibt der Abzug stehen — eine Liste von
gestern ist im Zug brauchbarer als ein Fehlerbildschirm.

Bewusst nur die erste Seite. Stünden 240 gespeicherte Titel da und der
Server antwortet mit 60, müsste entweder die Liste vor den Augen schrumpfen
oder der Versatz fürs Nachladen wäre falsch.

Gespeichert wird ausserdem nur, was eine Kachel zeigt. Ein Titel aus einer
Listenabfrage bringt jede Spur jeder Datei mit — im Raster steht davon
nichts, und ausgepackt werden müsste es bei jedem Aufrufen, also genau da,
wo der Abzug Zeit sparen soll.

Die Mediathekenliste selbst wird nur als Rettungsanker gemerkt, nicht als
Beschleuniger: sie sofort zu zeigen hiesse, eine Sitzung für fertig zu
erklären, bevor feststeht, dass der Token noch gilt.

Abmelden räumt alles weg. Titel, Zusammenfassungen und Sehfortschritt sind
persönlich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 15:59:15 +02:00
ScarriffleandClaude Opus 5 18958aadbc Der Klick in den schwarzen Rand trifft jetzt den schwarzen Rand
Er hat weiterhin angehalten statt ein- und auszublenden, und der Grund
steckte in der Rechnung: der schwarze Rand wurde aus der Fenstergrösse
ausgerechnet, das Bild liegt aber in einer Fläche mit `ignoresSafeArea()`.
Auf dem Mac ist der Einzug der Fensterleiste genau der Unterschied
zwischen beiden — die Rechnung lag um diesen Betrag daneben.

Jetzt wird die Bildfläche gemessen statt angenommen, und beide Zahlen
stehen in einem gemeinsamen, benannten Koordinatenraum. Vor allem aber
wird zuerst `AVPlayerLayer.videoRect` gefragt: der Player weiss selbst,
wo Bild steht. Die eigene Rechnung bleibt für den Moment vor dem ersten
Bild und für VLC, das keine solche Ebene hat.

Dazu zwei Worte: „Nachschub" heisst jetzt „Nachladen", und „Blatt" heisst
wieder „Sheet" — das ist ein Fachbegriff und bleibt englisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 06:49:15 +02:00
ScarriffleandClaude Opus 5 f47c6385e3 Auflösungen heissen jetzt, wie Menschen sie nennen
Auf der Detailseite stand „1080" — die Zeichenkette des Servers in
Grossbuchstaben. Weder eine Zahl mit Bedeutung noch ein Name, den jemand
benutzt. Jetzt steht dort „Full HD", „4K", „HD", „SD".

Die Einstufung geht neu über die **Breite**, nicht über die Höhe. Ein
Kinofilm in 2,39:1 misst 1920 × 804; nach der Höhe war das bisher „720p",
und zwar ausgerechnet bei den grossen Filmen. Die Höhe bleibt als Notnagel
für Server, die keine Breite melden, und der Name des Servers als letzter.

Nebenbei fällt damit der 4K-Filter bei Jellyfin nicht mehr durch, wenn ein
breitwandiges 4K nur 1608 Zeilen hat, und die doppelte Einstufung in
PlexMedia verschwindet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 06:33:26 +02:00
ScarriffleandClaude Opus 5 e61cb0403d Die Tonspur wird jetzt bestellt, nicht abgewartet
Der Befund, der die ganze Umsetzung bestimmt: `AudioStreamIndex` kam in
der PlaybackInfo-Anfrage **nicht vor**. Der Server wählte, wir nahmen was
kam — und bei einem umgerechneten Stream kommt danach nur noch diese eine
Tonspur an. `selectAudioTrack` schob seither ins Leere, weil im Player
eine Liste mit einem Eintrag stand.

Damit ist auch erklärt, warum die Spracheinstellung beim ZDF-Fall nichts
ausrichten konnte: sie lief auf einer Auswahl, die es gar nicht gab.

`PlaybackIntent` trägt die Spurnummer jetzt mit, und die Aushandlung
nennt sie ausdrücklich. Zwei Tests halten fest, dass sie ankommt — und
dass sie ohne Wunsch weggelassen wird, statt eine Null zu schicken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 19:16:51 +02:00
ScarriffleandClaude Opus 5 afb4a63655 Der Kern der Sprachwahl: Kürzel, die nicht lügen, und Abwägen statt Filtern
**`LanguageCode`** bringt Kürzel auf eine vergleichbare Form. Der Anlass
ist ein stiller Fehler: verglichen wurden die ersten zwei Zeichen, also
`ge` gegen `de` — und `ger` ist die ISO-639-2/B-Form von Deutsch, die
ffmpeg in viele Dateien schreibt. Bei solchen Titeln wirkte die
Spracheinstellung überhaupt nicht.

`gsw` bleibt `gsw`. Schweizerdeutsch ist keine Schreibweise von Deutsch,
und es gleichzusetzen hiesse, jemandem Schweizerdeutsch unterzuschieben,
der Hochdeutsch wollte. Es gehört in die Rangliste, nicht in eine
Gleichsetzung.

**`AudioTrackChooser`** wägt ab, statt zu filtern: jede Spur bekommt
Punkte, die höchste gewinnt. Damit gibt es immer eine Antwort, und
Audiodeskription wird gemieden, ohne unmöglich zu werden — gibt es eine
Sprache nur mit Audiodeskription, ist sie besser als Stille.

Gemerkt wird nicht eine Nummer, sondern eine Beschreibung der Spur:
Folgen derselben Serie haben unterschiedlich sortierte Spuren, und
„Spur 2" bedeutet in der nächsten Folge etwas anderes.

Siebzehn Tests, darunter der ZDF-Fall wörtlich — deutsche Spur mit
Audiodeskription als Standard, englisch betitelte deutsche Spur daneben
— und zwar in beide Richtungen: mit Meiden gewinnt die richtige, ohne
Meiden gewinnt das Kürzel. Sonst stünde der Schalter nur dabei.

Dazu trägt `MediaStream` jetzt `title`, `isDefault`, `isHearingImpaired`
und `isOriginal`. Der Name aus der Datei ist das zweite Signal neben dem
Kürzel und manchmal das einzige richtige.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 19:15:08 +02:00
ScarriffleandClaude Opus 5 946de29893 Nachsehen, was in den Tonspuren wirklich steht
Die Sprachwahl vergleicht Kürzel, und Kürzel lügen — beim ZDF-Fall ist
die als deutsch ausgezeichnete Spur die mit Audiodeskription. Ob der
**Name** der Spur das rettet, sieht man nur, wenn man beides nebeneinander
legt. Alles andere wäre geraten, und davon hatten wir heute genug.

`--spuren <host> <suchbegriff>` sucht einen Titel und druckt zu jeder
Tonspur: Kürzel, Name aus der Datei, wie Jellyfin es zusammensetzt,
Codec, Kanäle und die Kennzeichen.

Dafür holt der Decoder jetzt auch `Title`, `IsHearingImpaired` und
`IsOriginal`. `Title` ist das zweite Signal neben dem Kürzel und
manchmal das einzige richtige: wo „eng" steht, kann im Namen „Deutsch"
stehen. Wir haben es bisher weggeworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 19:11:27 +02:00
ScarriffleandClaude Opus 5 41cd7331a9 Entwurf für Standardsprachen der Tonspur
Drei Ebenen (allgemein → Mediathek → Serie), eine globale Rangliste als
Boden darunter, und die Wahl als prüfbares Punktesystem.

Zwei Funde bestimmen den Entwurf:

**Jellyfin hat kein Kennzeichen für Audiodeskription.** `MediaStream`
führt `IsDefault`, `IsForced`, `IsHearingImpaired`, `IsOriginal` — nichts
für Sehschädigung; die `visual_impaired`-Disposition reicht der Server
nicht durch. Sie ist nur am Text zu erkennen, und deshalb muss die
Erkennung nachsichtig sein und sich überstimmen lassen.

**Der Sprachvergleich greift heute bei `ger` nicht.** Verglichen werden
die ersten zwei Zeichen, also `ge` gegen `de` — und `ger` ist die
ISO-639-2/B-Form von Deutsch, die ffmpeg in viele Dateien schreibt. Bei
solchen Titeln wirkt die Spracheinstellung lautlos gar nicht. Gemessen,
nicht vermutet.

Auf Serienebene wird nicht eine Sprache gespeichert, sondern die
Signatur der tatsächlich gewählten Spur. Nur so ist der ZDF-Fall lösbar,
bei dem die deutsche Spur die mit Audiodeskription ist und die als
englisch ausgezeichnete die richtige.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:59:46 +02:00
ScarriffleandClaude Opus 5 10caa5fdd1 Platzhalter statt Ratespiel, und zwei Griffe im Player
**Das Raster sagt jetzt, ob noch etwas kommt.** Eine Mediathek lädt
seitenweise nach, und wer unten ankam, sah ein Raster, das aufhört — ohne
unterscheiden zu können, ob das alles war oder gerade nachgeladen wird.
Beides sah gleich aus.

Beide Server nennen die Gesamtzahl (Jellyfin `TotalRecordCount`, Plex
`totalSize`), und wir lesen sie längst. Also stehen die nächsten Kacheln
schon da, in der Form, die sie haben werden — gedeckelt auf ein Dutzend,
weil zweihundert Platzhalter niemandem helfen. Ist wirklich alles da,
steht es am Ende ausdrücklich: „Alle 252 Titel geladen".

Gegen die Attrappe mit 252 Titeln durchgeblättert und nachgesehen: die
Zeile steht da, und alle 252 sind angekommen.

**Herein sanft, hinaus zügig.** Das Einblenden der Bedienung begleitet
eine Bewegung des Zeigers und soll wirken, als käme sie der Hand
entgegen; zwei Zehntel wirkten wie ein Aufblitzen. Das Ausblenden
dagegen ist ein Aufräumen, und dabei muss niemand zusehen.

**Ein Klick ins Schwarze blendet um, statt anzuhalten.** Ein Film füllt
das Fenster fast nie ganz aus — die Balken daneben gehören nicht zum
Film, und dort zu klicken ist die natürliche Geste für „zeig mir die
Bedienung". Wo das Bild aufhört, wird aus `presentationSize` gerechnet,
also aus dem, was wirklich ankommt. Fehlen die Maße, verhält es sich wie
bisher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:50:26 +02:00
ScarriffleandClaude Opus 5 279d8e36ae Zwei Fehler um dieselbe Bildfläche
**Die Wiedergabe sprang von selbst wieder an.** Auf dem Mac pausiert ein
Klick ins Bild — und der Klick, mit dem man ein Fenster nach vorn holt,
kommt ebenfalls dort an. Man aktiviert das Fenster und drückt dabei auf
Play. Klicks innerhalb einer Viertelsekunde nach dem Aktivieren zählen
deshalb nicht: länger braucht niemand bis zum ersten gemeinten Klick,
kürzer erwischt den Fall nicht.

**Schwarzbild mit Ton nach dem Vergrössern.** Kachel und Vollbild
bestehen beim Wechsel kurz gleichzeitig, und beide holten sich dieselbe
Bildfläche in ihre Hülle. Eine Ansicht hat aber nur einen Vorgesetzten —
wer zuletzt aktualisiert, gewinnt, und das war nicht verlässlich die
richtige. Blieb die Fläche bei der Kachel, die gleich darauf verschwand,
lief im Vollbild der Ton weiter und das Bild blieb schwarz.

Die Hülle allein genügte nicht: sie verhindert, dass das Abräumen der
einen Ansicht der anderen etwas wegnimmt, entscheidet aber nicht, wer
die Fläche bekommen **soll**. Das sagt jetzt `isActive` — abgeleitet aus
`playback.presentation`, also aus derselben Quelle, die auch bestimmt,
welche Ansicht überhaupt zu sehen ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:44:20 +02:00
ScarriffleandClaude Opus 5 348ec96282 Eine misslungene Seite beendete das Nachladen für immer
Der Grund für „es sind wieder nicht alle Filme drin": im Fehlerfall stand
hier `page = nil`. Eine einzige misslungene Anfrage auf einer wackeligen
Leitung, und die Mediathek blieb endgültig bei sechzig Titeln stehen —
lautlos, ohne dass irgendetwas darauf hindeutete. Im Zug passiert das
zuverlässig.

Der Kommentar daneben begründete das Schweigen, und das war auch richtig:
ein Fehlerblatt mitten im Scrollen wäre störend. Nur hat er das
eigentliche Problem verdeckt — nicht die fehlende Meldung war schlimm,
sondern dass es danach keinen Weg zurück gab.

Jetzt bleibt die Seite erhalten, und unten erscheint eine Zeile zum
Weiterladen. Von selbst wird nach einem Fehlschlag **nicht** mehr
nachgefasst: sonst liefe bei jedem Scrollen eine Anfrage in dieselbe tote
Leitung.

Dazu der Knopf zum Neuladen in der Leiste, wie gewünscht. Ziehen von oben
gibt es auf dem Mac nicht, und in einem Raster ist es ohnehin kaum zu
treffen — wer den Verdacht hat, dass etwas fehlt, braucht einen Knopf
und keine Geste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:40:53 +02:00
ScarriffleandClaude Opus 5 24d7423f9c Bei Jellyfin galt zuhause als unterwegs
Beinahe-Regression, gefunden bevor sie jemanden traf: `isLocalConnection`
kam aus `session.server?.connection.local`, und `server` ist ein
`PlexServer` — bei Jellyfin immer `nil`. Jede Jellyfin-Wiedergabe galt
damit als entfernt, auch zuhause am Kabel. Der neue Deckel hätte dort ein
67-Mbit-Remux ohne Not auf acht Megabit heruntergerechnet.

Jetzt wird gefragt statt geraten: `/System/Endpoint` beantwortet es mit
**demselben** Aufruf, mit dem Jellyfin auch selbst entscheidet, ob es eine
Bitratenleiter ausliefert. Ein Blick auf die Adresse hätte nicht genügt —
wer über einen Reverse Proxy geht, erreicht seinen Server auch von der
Couch aus unter einem öffentlichen Namen, und über ein VPN kommt eine
private Adresse von weit her.

Gefragt wird bei beiden Wegen, beim Anmelden und beim Wiederherstellen:
dasselbe Konto kann heute zuhause und morgen im Zug aufwachen.

Im Zweifel `false` — lieber einmal zu vorsichtig gedeckelt als eine
Wiedergabe, die über eine Fernverbindung gar nicht erst anläuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:37:16 +02:00
ScarriffleandClaude Opus 5 fd11a9eebe „Auto" misst jetzt die Verbindung, statt die Einstellung abzulesen
Der Denkfehler, und er war meiner: `automatic` hiess wörtlich „nimm, was
in den Einstellungen steht". Stand dort „Original", bat die App über eine
Zugverbindung um eine 4K-Datei und wartete vierzig Sekunden auf etwas,
das nie kommen konnte. Das ist keine Automatik, das ist eine Durchreiche
mit einem irreführenden Namen.

**Die Einstellung ist jetzt die Obergrenze, nicht das Ziel** — „bis
hierhin, wenn die Leitung es trägt". Im eigenen Netz bleibt alles wie
bisher: dort gibt es keine Grenze, weil die Leitung das Original trägt
und ein Deckel das Bild ohne Grund verschlechterte. Die Unterscheidung
steht in `isLocalConnection` und ist nicht geraten.

Unterwegs beginnt „Auto" bei acht Megabit — genug für ordentliches
1080p, wenig genug, dass es fast überall anläuft — und tastet sich von
dort hinauf, solange es gut geht, bis höchstens zur eingestellten Decke.
Hinunter geht es sofort, sobald es hakt.

**Und die Auflösung geht mit.** Bitrate allein genügt nicht: 4K mit zwei
Megabit sieht schlechter aus als 720p mit zwei Megabit — dieselbe
Datenmenge, auf viermal so viele Bildpunkte verteilt. Wer die Bitrate
senkt, muss die Auflösung mitnehmen, sonst spart er Daten und
verschenkt das Bild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:32:33 +02:00
ScarriffleandClaude Opus 5 efed8717a3 „Auto" gibt nicht mehr auf, es versucht es kleiner
Der Fehlstart im Zug hat den eigentlichen Mangel gezeigt: die ganze neue
Regelung misst **ab dem ersten Bild**. Kommt nie eines, misst sie nichts,
greift nichts — und „Auto" hat sich als das erwiesen, was es wörtlich
heisst: „nimm, was in den Einstellungen steht". Steht dort „Original",
bittet die App über eine Zugverbindung um eine 4K-Datei und wartet
vierzig Sekunden auf etwas, das nie kommen kann.

Statt aufzugeben geht der Watchdog jetzt eine Sprosse tiefer und baut
neu auf — bei Filmen über `restart()`, bei Sendern über den Tuner-Abbau.
Gezählt gegen dasselbe Budget wie die Sicherheitsleine, sonst schickt
eine tote Leitung die App endlos im Kreis.

Scheitert auch das, nennt die Meldung jetzt den Grund und den Weg statt
nur „keine Daten mehr": wer ohne Grenze angefragt hat, soll erfahren,
dass es daran liegt und wo es sich ändern lässt.

Und „Strom" heisst Stream. Zweiunddreissig Stellen, dazu das
Gedächtnis ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:30:10 +02:00
ScarriffleandClaude Opus 5 4abce1c8b9 Name und Passwort zuerst, und Blätter in Fenstergrösse
**Die Anmeldung.** Sobald ein Jellyfin-Server Quick Connect anbietet,
sprang der Weg von selbst in den Code-Fluss. Gut gemeint — dabei läuft
kein Passwort durch diese App —, aber am Alltag vorbei: der Code will in
einem zweiten Gerät bestätigt werden, und wer im Zug sitzt, hat das
nicht offen. Ein Passwortspeicher füllt dagegen beide Felder in einer
Sekunde. Der Code bleibt als Knopf darunter erreichbar.

**Die Blätter.** Wieder `#if os(macOS)`, das auf dem Mac nie greift, weil
dort der iOS-Bau läuft. Filter, Spurwahl und Qualitätsblatt bekamen
deshalb eine Blattkante in iPhone-Höhe — ein schmaler Streifen, der in
einem grossen Fenster unten im Nichts klebt. Jetzt zur Laufzeit
entschieden: auf dem Mac volle Höhe, sonst wie bisher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:27:38 +02:00
ScarriffleandClaude Opus 5 8ff74ed4fa Der Hinweis bei „ohne Grenze" — aber nur mit Beleg
Das letzte offene Stück aus dem Plan. Du wolltest die WLAN-Voreinstellung
nicht verschoben haben, sondern einen Hinweis — und einen, der sich auf
etwas stützt.

Ohne Grenze kommt die Entscheidung oft bei direkter Wiedergabe heraus,
und dann gibt es keine Stufe, auf die irgendetwas ausweichen könnte.
Beim Testserver heisst „Original" konkret: 67 Mbit, 4K, HEVC. Über eine
Fernverbindung trägt das keine Leitung.

Der Hinweis erscheint deshalb nur, wenn beides zutrifft: die Einstellung
steht auf „ohne Grenze" **und** die letzte Wiedergabe hat tatsächlich
gehakt — gemessen am gleitenden Fenster, nicht geraten. Eine Warnung,
die immer dasteht, liest nach dem zweiten Mal niemand mehr.

Der Befund überlebt die Wiedergabe, weil der Player längst zu ist, wenn
jemand in die Einstellungen schaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:19:48 +02:00
ScarriffleandClaude Opus 5 c334fca665 Den grössten Titel messen, und die Quelle dazusagen
Die Reihe lieferte viermal dasselbe — 480p bei 1,1 Mbit, gleichgültig ob
mit `videoResolution` oder mit 20 Mbit. Das sieht nach einem Fehler in
der Anfrage aus und ist wahrscheinlich keiner: gemessen wurde „2 Fast 2
Furious", der erste Titel im Alphabet, und vermutlich ein 540p-Rip. Plex
rechnet nichts hoch.

Damit misst die Reihe die **Datei** statt die Einstellungen — und aus
einer Messung, die immer dasselbe sagt, liest man leicht das Falsche
heraus.

Die Probe nimmt jetzt den Titel mit der höchsten Bitrate aus einer
Stichprobe von sechzig und druckt die Quelle dazu: Auflösung, Bitrate,
Codec. Damit ist auf einen Blick zu sehen, ob eine kleine Antwort eine
kleine Quelle ist oder ein echter Deckel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:16:40 +02:00
ScarriffleandClaude Opus 5 d2237c29cd Fragen statt starten — die Probe überfuhr den Server
Beim letzten Lauf scheiterte auch die Anfrage, die vorher funktioniert
hatte. Kein neuer Befund, sondern mein Messfehler: die Probe warf sechs
Transcode-Sitzungen in Folge an, und danach lehnt Plex alles mit nackten
400ern ab — auch das, was vorher ging.

Jede Anfrage an `start.m3u8` startet einen Transcoder. Für eine
Parameterreihe ist das das falsche Werkzeug. Der **Entscheider** sagt,
was der Server täte, ohne etwas zu tun: Auflösung, Bitrate, und ob
umgerechnet oder umgepackt wird. Die Reihe läuft jetzt darüber.

Die zwei verbleibenden echten Sitzungen bekommen Abstand voneinander.

Der Wert wird aus der Antwort gesucht statt geparst — Plex antwortet je
nach Kopfzeile als XML oder JSON, und es geht um eine Anzeige, nicht um
Daten, mit denen weitergerechnet wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:15:05 +02:00
ScarriffleandClaude Opus 5 b861ca0599 Prüfen, woran die kleine Plex-Auflösung hängt
Die Servereinstellungen sind es nicht: „Begrenzung der Stream-Bitrate
übers Internet" steht auf Original, keine Upload-Grenze, Hardware-
Transcoding an. Die Vermutung ist damit widerlegt.

Bleibt die eigene Anfrage. Bei „automatisch" schickt die App
`maxVideoBitrate` und `videoQuality`, aber **kein** `videoResolution` —
dort steht `nil`, weil die Stufe keine vorgibt. Verdacht: Plex nimmt
dann seine Voreinstellung für die Qualitätsstufe, und die ist 540p.

Wäre das so, bekäme jeder Plex-Nutzer bei „automatisch" über eine
Fernverbindung stillschweigend 960x540 bei 1,1 Mbit, auch auf einer
schnellen Leitung. Die Probe fährt deshalb vier Kombinationen ab und
hält jeden Transcoder einzeln wieder an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:06:04 +02:00
ScarriffleandClaude Opus 5 7ecb16d4eb autoAdjustQuality bricht die Plex-Wiedergabe — wieder raus
Gemessen, nicht vermutet: zwei Anfragen an denselben Titel, getrennte
Sitzungskennungen, einzige Abweichung der Wert. Ohne ihn kommt eine
Playlist, mit ihm eine nackte **HTTP 400**.

Die Zeile hätte also nicht die Qualität verbessert, sondern die
Wiedergabe verhindert. Der Plan hat vor genau diesem Verhalten gewarnt —
nur habe ich es am Entscheider vermutet und ihn dort ausgespart, während
die Start-Adresse ihn ebenso ablehnt.

Plex' eigene Clients schicken den Wert; welche weitere Angabe sie dabei
mitführen, ist offen. Bis das gemessen ist, bleibt er draußen: eine
Leiter, die es vielleicht gibt, ist keinen Strom wert, der sicher nicht
läuft.

`TranscodeRequest.adaptive` bleibt samt seinen Tests bestehen. Das
Gerüst steht, es fehlt die richtige Beschwörungsformel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:02:37 +02:00
ScarriffleandClaude Opus 5 165a82acd0 Der Plex-Vergleich verglich zwei Mal dasselbe
`askingForLadder()` behält die Sitzungskennung bei — richtig für die App,
weil sonst beim Aufräumen ein Transcoder stehen bliebe. Für eine
Gegenüberstellung ist es tödlich: die zweite Anfrage bekam die laufende
Sitzung der ersten zurück, und die Antwort war zwangsläufig identisch.
Im Wortlaut stand zweimal dieselbe Sitzungs-GUID — der Beweis, dass gar
nichts gemessen wurde.

Jetzt zwei eigenständige Anfragen mit getrennten Kennungen, beide
Kennungen werden gedruckt, und beide Transcoder werden hinterher
angehalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:49:36 +02:00
ScarriffleandClaude Opus 5 e324a474fd Watchdog heisst Watchdog, Engine heisst Engine
Eingedeutschte Fachbegriffe in den Kommentaren — „Wachhund" für
Watchdog, „Motor" für Engine. Das liest sich albern und ist unklarer als
das Original; niemand sagt Wachhund, wenn er einen Watchdog meint.

Acht Stellen berichtigt. Gewöhnliche Sprache bleibt deutsch, der Stil
der Kommentare ändert sich nicht.

Dazu die Plex-Leiter messbar gemacht: `--leiter` baut dieselbe Anfrage
zweimal, mit und ohne `autoAdjustQuality=1`, und zählt die Stufen. Die
Zeile steht seit dem Umbau im Code und war nie geprüft — genau die Art
ungeprüfter Annahme über Serververhalten, die bei Jellyfin vier Runden
gekostet hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:47:31 +02:00
ScarriffleandClaude Opus 5 314c1a3bfc Die Schnipsel wiegen, nicht die Playlist zählen
Berechtigter Einwand: eine Playlist zu bekommen beweist gar nichts. Sie
ist Text und kostet ein Kilobyte; die Datenrate steckt in den
Schnipseln. Die vorige Fassung zählte `#EXTINF`-Zeilen und hätte
„funktioniert" gemeldet, auch wenn der Server bei jeder Stufe denselben
Strom ausliefert.

Jetzt lädt die Probe je Stufe den **ersten Schnipsel** und rechnet aus,
was er an Datenrate bedeutet — inklusive der unveränderten Stufe als
Vergleichsmass. Werden sie kleiner, trägt die Idee. Bleiben sie gleich
gross, rechnet der Server trotz anderer Zahl dasselbe, und ein
selbstgebautes Menü brächte nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:37:14 +02:00
ScarriffleandClaude Opus 5 f48eb55f35 Prüfen, ob wir uns die Leiter selbst bauen können
Der Wortlaut der Master-Playlist zeigt, woraus Jellyfins eigene Leiter
bestünde: dieselbe `main.m3u8`-Adresse, mehrfach, mit anderem
`VideoBitrate`. Eine Master-Playlist ist nur ein Menü.

Liefert der Server diese Unter-Playlists auch dann, wenn **wir** die
Zahl ändern, brauchen wir seine Zustimmung gar nicht — dann schreiben
wir das Menü selbst, und die Frage, warum er es verweigert, ist
gegenstandslos.

Die Probe versucht deshalb drei Stufen von Hand: halbe Bitrate, ein
Viertel, und ein Viertel zusammen mit `maxHeight=480`. Das letzte ist
der interessante Fall — Jellyfins eigene Leiter lässt die Auflösung
stehen und dreht nur an der Bitrate. Eine Stufe, die auch kleiner
rechnet, sieht bei wenig Bandbreite deutlich besser aus als dieselbe
Auflösung mit zu wenig Daten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:34:00 +02:00
ScarriffleandClaude Opus 5 6e18fda4aa Die Probe zeigt Fassung und Wortlaut
Der Proxy ist gelöst — Jellyfin hält die Verbindung jetzt für
ausserhalb —, und es kommt trotzdem nur eine Stufe. Damit sind alle
Hürden widerlegt, die im Quelltext von `master` stehen. Also stimmt
etwas an der Annahme nicht, und am wahrscheinlichsten ist es die
Fassung: ich lese womöglich den Quelltext einer anderen Version als der,
die dort läuft.

Deshalb meldet die Probe jetzt `Jellyfin <Version>` — und druckt bei nur
einer Stufe die Master-Playlist **im Wortlaut**. Ab da hilft keine
Zusammenfassung mehr: was fehlt, sieht man nur im Original.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:30:25 +02:00
ScarriffleandClaude Opus 5 e4e6cc6cbd Die Probe liest zurück, was der Server wirklich eingetragen hat
Ein Eintrag unter „Bekannte Proxys", der gespeichert, aber nicht
übernommen wurde, sieht von außen genauso aus wie gar keiner — und bei
der Fehlersuche unterscheidet das alles.

`GET /System/Configuration/network` führt `KnownProxies`. Zusammen mit
`IsLocal` und `IsInNetwork` steht damit in drei Zeilen, woran es liegt:
kein Eintrag, der falsche Eintrag, oder fehlender Neustart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:28:10 +02:00
ScarriffleandClaude Opus 5 539ea0aacb Die Probe zeigt beide Werte, nicht nur das Urteil
`IsLocal` und `IsInNetwork` beantworten verschiedene Fragen, und der
Unterschied benennt die Ursache: `IsLocal` heißt, die Anfrage kommt über
127.0.0.1 — dann steht der Proxy auf demselben Host, und ein Eintrag
unter „Bekannte Proxys" mit der LAN-Adresse geht ins Leere, weil Jellyfin
diese Adresse nie zu sehen bekommt.

Vorher stand nur „LOKAL" da, und damit blieb offen, ob der Eintrag fehlt
oder der falsche ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 16:25:29 +02:00
ScarriffleandClaude Opus 5 a79d562642 Die Sicherheitsleine — der teure Weg, mit engen Regeln
Wenn der billige Hebel nichts ausrichtet, weil die Playlist nur eine
Stufe hat oder schon die unterste läuft, bleibt nur der Neuaufbau mit
kleinerer Bitrate: Transcoder anhalten, Motor abreißen, neu aushandeln,
zurückspringen. Sekunden mit schwarzem Bild.

**Ein Notnagel, keine Regelschleife.** Zu oft ausgelöst erzeugt er genau
das Ruckeln, das er beheben soll. `DowngradeRule` ist deshalb eine Liste
von Bedingungen, die alle gelten müssen, und keine Schwelle: nur bei
„automatisch", nur wenn der billige Weg erschöpft ist, nur ab drei
Rücklern in anderthalb Minuten oder einem einzelnen über fünfzehn
Sekunden, nicht in den ersten zehn Sekunden, nicht binnen zehn Sekunden
nach einem Sprung, höchstens alle drei Minuten, höchstens zweimal.

Ein Sprung leert den Puffer **immer**. Ihn als Beleg für eine schlechte
Leitung zu nehmen hieße, sich für das eigene Tun zu bestrafen.

Das Urteil ist eine Aufzählung und kein `Bool`: „es hakt, aber wir tun
nichts" ist ohne Grund von einem Fehler nicht zu unterscheiden. Vierzehn
Tests schreiben jede Bedingung einzeln hin.

Ist das Budget verbraucht, **wird es gesagt** statt lautlos weiter
abgesenkt. Still bis zur untersten Sprosse zu gehen ist schlimmer, als
einmal zuzugeben, dass die Leitung es nicht trägt.

**Live-TV ist dabei**, wie gewünscht — aber mit Tuner-Abbau vorweg und
Budget eins statt zwei. Er ist ein Stück Hardware, vier Stück gibt es,
und ein doppelt belegter ist schlimmer als ein Stottern. Bekannte
Grenze: `endLiveSession` meldet nicht, **ob** die Freigabe geklappt hat
— das Protokoll gibt nichts zurück. Deshalb der eine Versuch und jeder
Schritt im Protokoll, damit ein verlorener Tuner nachvollziehbar bleibt.

Nebenbei aufgeräumt: die Wahl von Hand und die Sicherheitsleine teilen
sich jetzt denselben Neuaufbau. Was sie unterscheidet, steht in
`effectivePolicy` — die Wahl setzt `quality`, die Automatik
`automaticCeiling`, und beides fließt dort zusammen. Die Automatik
schreibt dabei nie `rememberedQuality` fort: eine schlechte Zugfahrt
soll nicht dauerhaft das Wohnzimmer verschlechtern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:56:50 +02:00
ScarriffleandClaude Opus 5 d878289929 Der Server sagt jetzt selbst, ob er uns für lokal hält
Die Probe endete bisher mit einem Konjunktiv: „Kam dieser Aufruf von
zuhause, ist Punkt 1 die Erklärung." Das ist eine Vermutung an genau der
Stelle, an der die ganze Messung hängt.

`GET /System/Endpoint` beantwortet `IsInNetwork` mit **demselben**
Aufruf, mit dem auch die ABR-Sperre entscheidet:
`_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())`.
Damit steht als Erstes im Bericht, ob eine Leiter überhaupt möglich
wäre — und ob das Folgende eine Aussage über unterwegs enthält oder
keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:43:55 +02:00
ScarriffleandClaude Opus 5 52d6ebba43 „Automatisch" misst jetzt, statt die Einstellung durchzureichen
Bisher hieß `PlayerQuality.automatic` wörtlich: nimm die Zahl, die für
WLAN oder Mobilfunk hinterlegt ist. Keine Messung, kein Nachregeln — die
Entscheidung fiel einmal beim Start und galt bis zum Ende.

`StreamHealth` beurteilt den Strom jetzt fortlaufend aus Proben im
Sekundentakt. Ein reiner Typ: kein AVFoundation, keine Uhr, kein Netz.
Damit lässt sich eine schlechte Zugfahrt als Zahlenreihe hinschreiben
und nachsehen, was sie auslöst — zehn Tests tun genau das.

**Welches Signal wann meldet**, ist der eigentliche Inhalt. Der
Pufferverlauf ist der einzige echte Frühwarner: schrumpft er schneller,
als die Zeit vergeht, kommt weniger herein als hinausgeht, und das sieht
man zehn bis zwanzig Sekunden vor dem Rückler. `isLikelyToKeepUp` ist
früh, aber sprunghaft — es zählt erst, wenn es anhält. Der Rücklerzähler
meldet zu spät zum Regeln und bleibt trotzdem der Massstab, an dem sich
hinterher ablesen lässt, ob es besser wurde. Die **gemessene** Bitrate
steht bewusst nicht drin: auf einer kleinen Sprosse holt der Player
wenig, ohne dass die Leitung schlecht wäre.

Ausdrücklich kein Bandbreitentest. Der kostet Daten, ist beim Vorliegen
veraltet und misst die Vergangenheit. Der Pufferverlauf ist die bessere
Messung, weil er die einzige Größe misst, auf die es ankommt: lädt es
schneller, als es läuft.

Gehandelt wird über `preferredPeakBitRate` — der billige Hebel, der ohne
Neuaufbau wirkt. Gezielt wird dabei auf eine Sprosse, **die es in
`AVAsset.variants` wirklich gibt**: ein Wert zwischen zweien wählt
dieselbe wie die untere und machte jede Anzeige darüber zur Lüge.
Absichtlich unsymmetrisch — runter sofort, hinauf frühestens nach einer
Minute Ruhe.

Und getrennt von `quality`: die Automatik ruft **nicht** `select(quality:)`.
Die Methode schreibt `rememberedQuality` fort und setzte eine manuelle
Stufe — womit die Automatik sich selbst abschaltete und eine schlechte
Zugfahrt dauerhaft das Wohnzimmer verschlechterte.

Hat die Playlist nur eine Stufe, greift der Hebel ins Leere. Das ist kein
Fehler, sondern der Fall, für den die Sicherheitsleine gebaut wird — bis
dahin steht er wenigstens im Protokoll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:40:48 +02:00
ScarriffleandClaude Opus 5 cdff53fa0d Eine Leiter bestellen statt einer einzelnen Stufe
Der Kern des Ruckelproblems: die App bestellte bei beiden Servern eine
HLS-Liste mit genau einer Variante. AVPlayer kann nur zwischen Stufen
wechseln, die in der Liste stehen — bei einer gibt es nichts zu wechseln,
und wenn die Leitung einbricht, kann nichts nachgeben.

**Jellyfin.** `enableAdaptiveBitrateStreaming` wird gesetzt, und zwar
ersetzend: die Voreinstellung im Server ist `false`, und bei zwei
gleichnamigen Werten gilt beim Server der erste — dieselbe Falle wie bei
`maxHeight` eine Funktion weiter oben. Dazu wird `main.m3u8` zu
`master.m3u8`, falls der Server die Schnipselliste nennt; nur auf der
Stufenliste kann der Parameter wirken.

Am Geräteprofil ändert sich **nichts**. Mehrere `TranscodingProfiles`
wären der naheliegende Fehlgriff — die beschreiben Formate, nicht
Bitraten. `MaxStreamingBitrate` bleibt die oberste Sprosse; was darunter
liegt, erfindet der Server.

**Plex.** `autoAdjustQuality=1`, als Eigenschaft der Anfrage statt als
feste Zeile: `queryItems` speist auch die Adresse zum Herunterladen, und
dort ergäbe eine Leiter keinen Sinn. Bewusst **nicht** am Entscheider —
der antwortet auf Unbekanntes mit nackten 400ern, wofür es hier eigens
`transcoderBisect` gibt. Er beantwortet ohnehin nur „darf das gespielt
werden". Stünde der Wert dort und nähme der Server ihn übel, wäre nicht
die Leiter kaputt, sondern die Wiedergabe.

Beides nur bei „automatisch". Wer im Blatt ausdrücklich „720p · 3 Mbps"
antippt, bekommt das und nicht „höchstens 3, meistens weniger" unter
unverändertem Etikett.

**Was das nicht kann, und das gehört dazugesagt:** Jellyfin verweigert
die Leiter im lokalen Netz grundsätzlich — „Within the local network this
will likely do more harm than good." Ein schwaches WLAN zählt dort mit,
und vom Client aus ist das nicht zu überstimmen. Ebenso fällt sie weg,
wenn Bild oder Ton nur kopiert statt gerechnet werden. Ob am Ende
wirklich mehrere Stufen ankommen, steht deshalb nicht im Code, sondern in
`PlaybackStatistics.variantCount`.

Elf neue Tests, darunter der doppelte Parameter, die andere
Schreibweise, und dass die Sitzungskennung beim Umbau dieselbe bleibt —
sonst bliebe beim Aufräumen ein Transcoder stehen.

Die Probe zeigt jetzt außerdem, ob kopiert statt gerechnet wird. Das ist
die zweite Hürde, und sie ist von zuhause aus nicht sichtbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:26:57 +02:00
ScarriffleandClaude Opus 5 2d3c4adbba Die Probe räumt auf und sagt, woran es liegt
Sie hat am Server eine Transcode-Sitzung angelegt und nicht wieder
beendet — ein Messwerkzeug, das Arbeit hinterlässt, ist ein schlechtes.
Jetzt läuft `endPlayback` am Ende.

Und sie erklärt das Ergebnis, statt es nur hinzustellen. Kommt nur eine
Stufe zurück, nennt sie die Hürden aus Jellyfins eigenem Quelltext
(`DynamicHlsHelper.EnableAdaptiveBitrateStreaming`) — allen voran die
erste und überraschendste: im lokalen Netz liefert Jellyfin
**grundsätzlich** keine Leiter. Wörtlich dort: „Within the local network
this will likely do more harm than good." Ein schwaches WLAN ist für
Jellyfin auch nur lokales Netz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:21:36 +02:00
ScarriffleandClaude Opus 5 ab597ee2f0 Stillstand ohne Absicht ist kein Pausieren
`timeControlStatus` meldet zwei verschiedene Dinge als `.paused`: das
Anhalten durch den Nutzer und den Fall, dass `play()` gerufen wurde, der
Player aber nicht losläuft — weil die Quelle nicht bereit ist oder weil
nichts ankommt. Die Engine hat beides als „pausiert" weitergegeben.

Sichtbar war das als Player mit Abspieldreieck und Scrubber auf 0:00,
während in Wahrheit auf Daten gewartet wurde. Wer dann tippte, drückte
auf Play bei etwas, das längst laufen sollte.

Die Engine führt jetzt mit, ob überhaupt jemand spielen wollte.
Stillstand mit Absicht ist Pause, Stillstand ohne Absicht ist Warten.

Dazu die Anzeigeregel, wie gewünscht: **der Kreisel gilt immer**, mit
oder ohne eingeblendete Bedienung. Er ist keine Verzierung der Knöpfe,
sondern die Auskunft „es kommt gleich weiter" — und die braucht man
gerade dann, wenn nichts eingeblendet ist und man vor einem stehenden
Bild sitzt. Das Play-Pause-Symbol erscheint dagegen nur mit der übrigen
Bedienung, und an seiner Stelle bleibt beim Laden eine Lücke, damit der
Kreisel hindurchscheint statt zwei Anzeigen übereinanderzulegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:14:10 +02:00
ScarriffleandClaude Opus 5 baa542690c Vorrat anlegen, und VLC meldet endlich das Stocken
Zwei Änderungen, die an der Wiedergabe selbst etwas tun — beide ohne
Abhängigkeit davon, ob die Server je eine Leiter liefern.

**Vorrat.** Ohne Angabe legt AVPlayer so viel an, wie es für einen
zügigen Start braucht, und keinen Meter mehr. Das ist richtig, solange
die Leitung trägt, und falsch, sobald sie aussetzt: ein Loch von fünf
Sekunden trifft dann auf einen Puffer von fünf Sekunden, und das Bild
steht. Fünfundzwanzig Sekunden sind ein Anfangswert, kein Messergebnis.

Nur für Filme und Folgen über das Netz. **Nicht für Sender** — dort
hieße ein halbminütiger Vorrat eine halbe Minute Verzug und ein zähes
Umschalten. Und nicht für Dateien von der Platte, die brauchen keinen.
Wer das entscheidet, muss mehr wissen als der Motor, deshalb reist die
Zahl mit der Quelle statt dass die Engine rät.

**VLC hat das Stocken nie gemeldet.** `VLCMediaPlayerState` kennt keinen
Wert dafür — die Aufzählung führt nur `opening`, `playing`, `paused`,
`stopped`, `error`. Der Puffer-Fortschritt kommt über
`mediaPlayerBufferingChanged:`, und die Methode fehlte. Folge:
`.buffering` ging dort **nie** an, der Wachhund im Player löste nie aus,
und ein hängender Strom lief stumm ins Nichts — ohne Kreisel, ohne
Meldung, ohne Abbruch. Ausgerechnet auf dem Weg, der die großen Remuxe
spielt, bei denen eine schwache Leitung am ehesten scheitert.

Gewertet wird nur aus dem Spielen heraus: laut Dokumentation kommt vor
jeder Wiedergabe einmal 0,0 und einmal 1,0, und dieses anfängliche
Füllen ist der Start, kein Rückler.

Dazu meldet VLC jetzt überhaupt Statistik. `variantCount` bleibt dabei
`nil` und nicht `1` — eine Datei hat keine Leiter, und „gibt es nicht"
ist etwas anderes als „gibt es einmal". Bei der Bitrate steht die
Annahme im Kommentar statt im Nebel: libvlc führt `inputBitrate`
vermutlich in Byte je Sekunde, daher der Faktor acht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:06:36 +02:00
ScarriffleandClaude Opus 5 afab93f53a FlixyProbe kann die Leiter zählen
Die Frage, von der alles Weitere abhängt, ist eine Tatsachenfrage:
Liefert der Server eine HLS-Liste mit mehreren Stufen oder mit einer?
Sie über einen Build aufs Gerät zu beantworten kostet einen halben Tag;
über HTTP kostet sie Sekunden. Genau dafür gibt es dieses Werkzeug.

`--playlist <adresse>` holt eine Playlist und zählt die
`EXT-X-STREAM-INF`-Zeilen mitsamt Bandbreite und Auflösung. Es
unterscheidet dabei die drei Fälle, die man auseinanderhalten muss: eine
Master-Playlist mit mehreren Stufen, eine mit genau einer — und eine
Varianten-Playlist, die gar keine Stufen führt, sondern Schnipsel. Die
letzte Verwechslung ist die naheliegende: `main.m3u8` sieht aus wie eine
Playlist und ist die falsche.

`--jellyfin <host>` meldet sich an, holt einen Film, lässt den Server
eine Transcode-Adresse aushandeln und prüft sie dreimal: unverändert,
als `master.m3u8`, und als `master.m3u8` mit
`enableAdaptiveBitrateStreaming=true`. Damit steht danach fest, ob der
Umbau in JellyfinBackend überhaupt etwas bringen kann.

Das Passwort wird eingetippt statt übergeben, ohne Echo und ohne Ablage.
Gedruckt werden Pfad und die **Namen** der Abfragewerte, nie die
vollständige Adresse mit Schlüssel — dieselbe Regel, nach der das
Werkzeug schon vorher gebaut war.

Beide Modi laufen vor der Plex-Anmeldung: die eine Frage hat mit Plex
nichts zu tun, die andere bringt ihre eigene Anmeldung mit.

Der Leser selbst ist gegen drei erfundene Playlists geprüft — mehrstufig,
einstufig, und eine Schnipselliste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 15:02:43 +02:00
ScarriffleandClaude Opus 5 903bfe5f63 Der Player weiß jetzt, wie es ihm geht
Bisher gab es über eine laufende Wiedergabe zwei Zahlen — gemessene
Bitrate und verworfene Bilder —, und die standen nur zur Anzeige da. Die
Frage „trägt die Leitung das gerade" war nicht zu beantworten, weder für
den Nutzer noch für die App.

**Die wichtigste neue Zahl ist `variantCount`.** Sie kommt aus
`AVAsset.variants`, also vom Parser des Players, und beantwortet die
Frage, ob es überhaupt etwas zum Umschalten gibt. Steht dort 1, greift
jedes Nachregeln ins Leere — und genau das ist heute der Fall, weil die
App bei beiden Servern eine Playlist mit einer Stufe bestellt.

Dazu der Pufferstand aus `loadedTimeRanges`: der einzige echte
Frühwarner. Schrumpft er schneller, als die Zeit vergeht, kommt weniger
herein als hinausgeht — zehn bis zwanzig Sekunden, bevor das Bild steht.
Alles andere meldet sich erst danach.

**Verworfene Bilder werden jetzt über alle Protokolleinträge summiert.**
Das Zugriffsprotokoll legt bei jedem Stufenwechsel einen neuen an, dessen
Zähler bei null beginnt; wer nur `last` liest, hält eine Wiedergabe für
makellos, die schon zwanzigmal Bilder verworfen hat.

**Der Wachhund war blind für den häufigsten Fall.** Vierzig Sekunden am
Stück ohne Daten brechen ab — aber er setzte sich bei jedem kurzen
Erholen zurück, und zehn Rückler à dreißig Sekunden lösten ihn nie aus.
`StallLedger` zählt daneben mit, wie viel Zeit im gleitenden Fenster
fürs Warten draufging. Es bricht nichts ab, es stellt nur fest — und ist
ein reiner Typ ohne Uhr und ohne AVFoundation, damit sich eine Folge von
Rücklern als Zahlenreihe hinschreiben und das Urteil prüfen lässt.
Sieben Tests, darunter genau der Fall, an dem der alte Wachhund
vorbeisah.

Gezählt wird nur der Weg vom Spielen ins Warten. Das anfängliche Puffern
ist der Start, kein Rückler; würde es mitgezählt, hätte jede Wiedergabe
eine Vorstrafe.

Und zwei Kommentare sind berichtigt. Sie behaupteten, AVPlayer wechsle
die Bitrate von selbst und das sei „die zugesagte automatische
Qualitätsanpassung". Umschalten kann er nur zwischen Varianten, die in
der Liste stehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 14:59:29 +02:00
ScarriffleandClaude Opus 5 9c66727aca Mediatheken ohne bekannte Art verschwanden wortlos
`isSupported` war `movie || show`. Alles Übrige fiel heraus — auch eine
Mediathek mit gemischtem Inhalt, die Jellyfin ohne `CollectionType`
meldet. Wer Filme und Serien nicht getrennt hat, sah sie nirgends:
nicht in der Seitenleiste, nicht auf der Startseite, und nirgends eine
Meldung. Eine unbekannte Art ist kein Grund, etwas zu verschweigen.

Musik, Fotos und Live-TV bleiben draußen, und das aus jeweils eigenem
Grund — jetzt aufgezählt statt stillschweigend.

Die Attrappe blättert außerdem richtig: vorher kam immer alles auf
einmal und `TotalRecordCount` war die Länge der Antwort, womit eine App,
die richtig blättert, genauso aussah wie eine, die es gar nicht
versucht. Damit ist belegt, dass die Mediathek alle 252 Titel holt —
in fünf Seiten, bis StartIndex 240.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 11:10:38 +02:00
ScarriffleandClaude Opus 5 682049b45a Staffeln ohne eigenes Plakat blieben grau
Die Bildadresse entstand aus der Kennung, nicht aus einer Antwort des
Servers: jeder Titel ohne eigenes Plakat trug eine Adresse, die mit
„nicht gefunden" antwortet. Und weil ein 404 zu Recht nicht wiederholt
wird, blieb die Kachel für immer grau.

Betroffen waren vor allem Staffeln — die haben bei Jellyfin oft kein
eigenes Bild, sondern leihen das der Serie. Genau diesen Rückfall gab es
nur für Folgen, nicht für Staffeln.

Beides behoben: kein Bild, keine Adresse — und eine Staffel darf sich
nehmen, was die Serie hat. Dieselbe Vorsicht stand eine Zeile tiefer
schon beim Hintergrundbild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 11:10:38 +02:00
ScarriffleandClaude Opus 5 00db1bbab9 Der Folgen-Schlüssel hieß beim Auslesen anders als beim Nachschlagen
`Text("episode.number \(episode)")` trug Xcode beim Auslesen als
`episode.number %@` in den Katalog ein, während die Laufzeit
`episode.number %lld` nachschlägt. Zwischen zwei verschiedenen
Schlüsseln findet man nie einen — und auf dem Bildschirm stand weiter
der Schlüssel selbst.

Jetzt über `String(localized:)` wie die Staffel zwei Blöcke darüber.
Damit ist der Platzhalter eindeutig, und die übersetzte Tabelle im
gebauten Bündel führt genau die beiden Schlüssel, die der Code sucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 11:10:38 +02:00
ScarriffleandClaude Opus 5 5d09836686 Die Medientasten bedienen jetzt Flixy
Die App hat sich beim System nie als „läuft gerade" gemeldet — das
MediaPlayer-Framework kam im ganzen Projekt nicht vor. macOS schickt die
Medientasten aber genau dorthin: an das Programm, das sich gemeldet hat.
Meldet sich keines, gewinnt irgendein anderes, und man pausiert etwas,
das man gar nicht sieht.

`NowPlaying` meldet Titel, Untertitel, Länge, Stelle und Tempo und nimmt
entgegen: Abspielen, Anhalten, Umschalten, Vor- und Zurückspulen mit den
**eingestellten** Weiten, und das Ziehen am Balken. Damit tun
Medientasten, Kontrollzentrum, Sperrbildschirm und Kopfhörerknöpfe
dasselbe wie die Knöpfe im Player — es ist eine Sache, nicht vier.

Eine Anmeldung für die ganze App, nicht eine je Player: das System kennt
ohnehin nur einen laufenden Titel. Abgemeldet wird nur vom Player, der
auch angemeldet ist — sonst räumte einer, der spät zu Ende geht, dem
längst gestarteten nächsten die Tasten weg.

Die Rückrufe springen auf den Hauptakteur, statt ihn anzunehmen. Dass sie
dort ankommen, ist nirgends zugesagt, und dieselbe Annahme hat die App im
VLC-Engine schon einmal zum Absturz gebracht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:55:49 +02:00
ScarriffleandClaude Opus 5 fc812f5a25 Zwei Schlüssel ohne Platzhalter, zwei Kreisel übereinander
**Die Schlüssel.** Im Quelltext steht `"season.number \(season)"`, der
Schlüssel heißt damit `season.number %lld`. Im Katalog stand er ohne
Platzhalter — also traf er nie, und die App zeigte den Schlüssel selbst:
„The 50 · season.number 1 · episode.number 1". Beide umbenannt. Ein
Überbleibsel `episode.number %@` ohne eine einzige Übersetzung ist
mitgegangen, und aus „Episode" wurde „Folge", wie überall sonst im Haus.

**Die Kreisel.** Die Abdeckung beim Wiederherstellen der Stelle ist eine
ganze Seite, kein Schleier — nur lag das Übrige weiter darüber. Zu sehen
waren Bedienelemente mitten im Satz, ein Kreisel in der Abdeckung und ein
zweiter im Abspielknopf, den man in dem Moment ohnehin nicht drücken
soll. Jetzt ist die Abdeckung ausschließlich. Die Kopfzeile bleibt: sie
ist der Weg zurück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:52:08 +02:00
ScarriffleandClaude Opus 5 8a63f815cd Das Qualitätsblatt hat die App angehalten
Kein Hänger, sondern ein Absturz, der nicht durchkam. Der Stapel des
eingefrorenen Prozesses endet in `_assertionFailure` beim Lesen eines
`@Environment`-Werts — mitten in `SheetBridge.present`, also während das
Blatt aufgebaut wird. Weil der Player in diesem Moment in einem
Layoutdurchlauf steckt, erschien nie eine Meldung: das Fenster stand
still.

`@Environment(PlaybackSettings.self)` ohne Fragezeichen ist keine Bitte,
sondern eine Behauptung — fehlt der Gegenstand, hält Swift die Anwendung
an. Und ein Blatt hängt an einem eigenen Darstellungsbaum. Auf dem iPad
kam die Umgebung des Players dort bisher an, auf dem Mac nicht.

Zwei Änderungen, beide nötig:

- Der Player reicht `PlaybackSettings` und den Akzent ausdrücklich in
  seine Blätter hinein, statt sich auf Vererbung zu verlassen.
- `QualitySheet` liest die Einstellungen als Optional. Ein Vergessen
  kostet künftig eine fehlende Zeile im Blatt statt der ganzen App.

Dazu ein Test, der genau diesen Weg geht: abspielen, Qualitätsknopf,
Blatt da? Er lief schon vor der Korrektur durch — der Unterschied
zwischen iPad und Mac ist mit einem Simulator nicht zu fassen. Er hält
trotzdem fest, dass der Weg überhaupt gegangen werden kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:45:09 +02:00
ScarriffleandClaude Opus 5 5dd2daf3ed FlixyMac ist weg
VLCKit liefert Teile für iOS, macOS, tvOS und die Simulatoren — aber
keinen für Catalyst. Deshalb hing `FlixyVLC` gar nicht erst am Mac-Ziel,
und deshalb musste dort jedes MKV und jeder DTS-Ton vom Server
umgerechnet werden. Bei einem Medienclient ist das nicht ein Detail am
Rand, sondern der Zweck der Übung.

„Für iPad angepasst" ist derselbe iOS-Bau und lädt denselben iOS-Teil von
VLCKit. Damit läuft auf dem Mac, was auf dem iPad läuft.

Was Catalyst dafür konnte — eine `.app` zum Weitergeben, Intel-Macs, die
Fensterleiste über AppKit — wiegt hier nichts auf: ein Nutzer, ein
Apple-Silicon-Mac.

Die Fenstergröße hing an `#if targetEnvironment(macCatalyst)` und fragt
jetzt `FlixyPlatform.isMac`. `sizeRestrictions` ist auf dem iPhone `nil`,
das Fragezeichen ist die ganze Fallunterscheidung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:00:44 +02:00
ScarriffleandClaude Opus 5 f3ea1ea891 Die Fensterleiste schluckte die halbe Kopfzeile
Der Player deckt das ganze Fenster ab, und auf dem Mac zeichnet das
System seine Ampelknöpfe darüber. Die Kopfzeile lag damit nicht nur
optisch unter ihnen — der Streifen gehört der Fensterleiste, und die
nimmt jeden Klick für sich. Verkleinern, Schließen, Tonspur, Untertitel
und die Qualitätsstufe waren dort tot, und ein Doppelklick auf dieselbe
Stelle legte das Fenster ins Dock.

Jetzt beginnt die Kopfzeile auf dem Mac achtundzwanzig Punkte tiefer.

Dazu: Tabulator springt in der Jellyfin-Anmeldung vom Namen zum Passwort
und zurück. „Name, Tab, Passwort, Eingabe" ist der Weg, den jeder mit
einem Passwortspeicher nimmt; SwiftUI schiebt den Fokus dort nicht von
selbst weiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:56:36 +02:00
ScarriffleandClaude Opus 5 23d10d61f3 Die Mac-App bekommt eine eigene Kennung
`DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER: NO` gab der
Catalyst-Fassung dieselbe Bündelkennung wie der iPhone-App:
`com.scarriffleservices.flixy`. Auf einem Apple-Silicon-Mac, auf dem die
iPad-Fassung installiert ist, gibt es diese Kennung damit zweimal — und
das System entscheidet sich für eine davon, nicht unbedingt für die eben
gebaute. Der Start sah aus, als geschähe nichts.

`lsregister` zeigte sechs Einträge unter derselben Kennung. Apples
Voreinstellung ist `YES`, und zwar genau deshalb. Daraus wird
`maccatalyst.com.scarriffleservices.flixy`.

Dazu richtig signiert statt ad hoc: Flixy legt den Server-Token im
Schlüsselbund ab, und dem verweigert er sich bei einem Sandbox-Programm
ohne Team.

Und die Schemata stehen jetzt im Projekt. Xcode legte sie im
Benutzerordner an, wo `xcodegen generate` sie mitnahm — danach fand
`xcodebuild` keines mehr und konnte die Pakete nicht auflösen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:48:18 +02:00
ScarriffleandClaude Opus 5 37a6c2648f Der Mac ist nicht immer Catalyst
Die Bedienung mit Zeiger und Tastatur hing an
`#if targetEnvironment(macCatalyst)`. Das ist in dem Bau, der tatsächlich
benutzt wird, **falsch**: „für iPad angepasst" ist buchstäblich der
iOS-Bau, der auf Apple Silicon startet, und dort ist die Abfrage nicht
erfüllt. Klick-zum-Anhalten, Leertaste und die Pfeiltasten waren damit
genau dort abgeschaltet, wo sie gebraucht werden.

Jetzt beantwortet `FlixyPlatform.isMac` die Frage zur Laufzeit und kennt
beide Wege. Die Ansicht bleibt dabei eine einzige: `focusable(_:)` nimmt
den Schalter entgegen, die Tastenhandler geben sonst `.ignored` zurück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:40:43 +02:00
ScarriffleandClaude Opus 5 9a4b6b1e06 Ein Kreisel ist kein Pausenknopf
Während der Player auf Daten wartete, stand im mittleren Knopf das
Abspieldreieck und der Ladekreis lag dahinter. Das las sich als
„angehalten": man drückte auf Abspielen, und es geschah nichts — es lief
ja bereits, es kam nur nichts an.

Jetzt sitzt der Kreisel **im** Knopf, dort wo sonst das Symbol steht. Der
freistehende Kreisel in der Bildmitte bleibt für den Fall, dass die
Bedienelemente ausgeblendet sind. Zwei übereinander gibt es nicht mehr.

`togglePlayPause` zählt das Warten jetzt zum Laufen. Vorher führte ein
Tipp in diesem Moment zu `play()` auf etwas, das schon spielte.

Dazu auf dem Mac, wie im Browser auch:

- Ein Klick ins Bild hält an, statt erst die Bedienelemente zu holen. Die
  kommen dort von allein, sobald sich der Zeiger regt.
- Leertaste hält an, Pfeil links und rechts springen.

Mit dem Finger bleibt es beim Alten: ohne Zeiger gibt es keine Bewegung
ohne Berührung, und ein Tipp, der sofort anhält, nähme den einzigen Weg
zu Scrubber, Tonspur und Qualität.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:33:24 +02:00
ScarriffleandClaude Opus 5 3af6c3112f Das Web ist jetzt ein Projekt
Schritt 2 des Plans, fertig. Aus 1700 Zeilen in einer Datei sind zwölf
Module geworden: api, prefs, contrast, icons, util, types und die
Ansichten für Anmeldung, Startseite, Mediathek, Einstellungen, Kacheln
und Player. Alles typgeprüft, und die Prüfung läuft scharf —
noUncheckedIndexedAccess inbegriffen.

An einer Stelle hat der Compiler mich zu Recht aufgehalten: Object.fromEntries
verliert die Typen jedes einzelnen Feldes, und geblieben wäre nur ein
doppelter Cast, der die Prüfung stilllegt statt sie zu bestehen. Jetzt
steht es ausgeschrieben da.

hls.js wird erst beim Abspielen geladen. Es ist größer als der ganze Rest
zusammen, und wer nur stöbert, braucht es nie: der Start fällt damit von
632 auf 39 Kilobyte.

Geprüft mit einem erfundenen Medienserver dahinter: Seite kommt an,
gebaute Dateien werden ausgeliefert, Anfragen gehen weiter, und die
Startseite zeichnet Insel, Aufmacher und Reihen.

Die einzelne Datei bleibt liegen, bis das Projekt sich bewährt hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:31:56 +02:00
ScarriffleandClaude Opus 5 a04784ca15 Das Web bekommt ein Fundament und einen eigenen Server
Schritt 2 des Plans, erste Hälfte. Vite mit TypeScript, und ein Server
ohne eine einzige Abhängigkeit — er soll auf einem kleinen Container
laufen, nicht sein halbes Verzeichnis mitschleppen.

Der Server ist nicht Zierde. Er löst zwei Dinge, gegen die keine Zeile im
Frontend etwas ausrichten kann: Anfragen laufen über dieselbe Adresse wie
die Seite, womit CORS erledigt ist, auch beim Streamen. Und läuft der
Medienserver im Klartext, während die Seite verschlüsselt ausgeliefert
wird, blockiert der Browser sonst den Strom.

Geprüft: Weiterreichen mitsamt Autorisierung und richtig gesetztem Host,
Ausliefern gebauter Dateien, unbekannte Wege landen auf der Seite — und
ein Ausbruchsversuch mit "../../../etc/passwd" liefert die Seite statt
der Passwortdatei.

Der Körper wird durchgereicht statt gepuffert: ein Film ist ein Strom,
und ihn erst vollständig einzulesen hieße, ihn zwischenzuspeichern.

Die einzelne Datei bleibt vorerst, sie läuft ja.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:25:11 +02:00
ScarriffleandClaude Opus 5 87b9fae94b Die Fortschrittsmeldungen waren stumm
Sie hingen an einem verschluckten catch. Beim Suchen nach dem verlorenen
Fortschritt war deshalb nirgends zu sehen, ob sie überhaupt ankommen —
ohne await zu senden ist richtig, ohne Hinsehen nicht.

Jetzt steht in der Konsole, was angenommen und was abgelehnt wurde.

Dazu ein Hinweis auf die Regel des Servers, die den eigentlichen Fall
erklärt: Jellyfin merkt sich eine Fortsetzposition nur zwischen fünf und
neunzig Prozent der Laufzeit. Eine halbe Minute von einem Film liegt
darunter, und der Server verwirft sie bewusst — "kurz reingeschaut" ist
kein Weiterschauen. Wer das nicht weiß, sucht den Fehler bei sich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:02:01 +02:00
ScarriffleandClaude Opus 5 b51bb16de0 Der Fortschritt ging beim Schließen verloren
Beim Schließen wurde das Video abgeräumt, und dabei fiel currentTime auf
null — bevor die Stopp-Meldung hinausging. Jellyfin bekam "gestoppt bei
0:00" und löschte den Fortschritt. Der Film war zur Hälfte gesehen und
stand trotzdem nicht unter "Weiterschauen".

Jetzt wird die Stelle getrennt vom Element gehalten und die Meldung
gesendet, bevor abgeräumt wird. Nachgemessen: bei 45 Sekunden
geschlossen, 45 Sekunden gemeldet. Dazu PlayMethod und CanSeek, ohne die
Jellyfin die Meldung keiner Art von Wiedergabe zuordnet, und sofortige
Meldung bei Anhalten und Weiterlaufen — wer pausiert und weggeht, hatte
sonst bis zur nächsten Zehn-Sekunden-Marke nichts hinterlassen.

Und das eingefrorene Bild bei laufendem Ton nach einem
Anwendungswechsel: Der Browser hält das Entpacken an, während der Tab
verdeckt ist, und hls.js findet danach nicht von selbst in den Puffer
zurück. Jetzt wird beim Zurückkommen an derselben Stelle neu angestoßen,
mit einem winzigen Sprung — ohne den bleibt das zuletzt gezeigte Bild
stehen, obwohl längst wieder Segmente ankommen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:56:10 +02:00
ScarriffleandClaude Opus 5 fa8d9de2e9 Die Knöpfe hatten Flächen, die es in der App nie gab
Ich habe nach dem Code gebaut und dabei das Wesentliche übersehen: In
PlayerView gibt es genau ein "background", und das ist die Abdunklung
über dem ganzen Bild. Die Knöpfe selbst sind blanke weiße Symbole.

Meine runden, hinterlegten Flächen waren eine Erfindung — und der Grund,
warum der Qualitätsknopf schief aussah: ein Wort passt in keinen Kreis.
Jetzt steht "Original" als das da, was es ist, nämlich Text.

Dazu die Spultasten als Kreispfeile mit der Sekundenzahl darin, wie
gobackward.10 und goforward.30 sie zeichnen, statt als "−10" und "+30"
in Textform. Und der Abspielpfeil in 52 Punkt, der Größe aus der App.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:50:09 +02:00
ScarriffleandClaude Opus 5 6d8440ede8 Unten steht jetzt nur noch der Scrubber, wie in der App
Der strukturelle Unterschied, den ich übersehen hatte: in der iPad-App
liegt unten nichts als der Regler über die volle Breite, darunter Zeit
links und Restzeit rechts. Bei mir standen Zeit, Regler und Restzeit
nebeneinander — der Regler wirkte dadurch wie ein Formularfeld zwischen
zwei Beschriftungen — und darunter lag noch eine Werkzeugleiste mit
Lautstärkeregler und Tempo-Auswahlfeld, die es in der App gar nicht gibt.

Alles Einstellbare sitzt jetzt oben rechts beisammen: Tonspur,
Untertitel, Qualität, Geschwindigkeit, Ton, Vollbild. Das Tempo als
Liste wie die anderen; ein Auswahlfeld mitten in der Bedienleiste war das
einzige Bauteil, das nach Formular aussah. Die Lautstärke über Mausrad
auf dem Tonknopf und über die Pfeiltasten.

Und ein Fehler, der genau das verhinderte, was gewünscht war: Die
Bedienfläche lag als Ganzes über dem Bild und schluckte jeden Klick.
"Ins Bild tippen hält an" funktionierte damit nur, solange die Bedienung
unsichtbar war — also fast nie. Jetzt nehmen nur ihre Knöpfe Klicks
entgegen. Nachgemessen: Klick pausiert, Pfeil rechts spult 30 Sekunden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:44:41 +02:00
ScarriffleandClaude Opus 5 24e40f7403 Der Web-Player folgt jetzt dem Bauplan des iPad-Players
Mein Menü machte genau den Fehler, den der iPad-Code im Kommentar
ausschließt: alles in einer Liste. Dort steht, warum das falsch ist —
"die Tonspur stellt man einmal am Anfang um, die Untertitel mitten im
Film. Ein gemeinsames Blatt bedeutete jedes Mal Scrollen an der anderen
Liste vorbei."

Jetzt drei Knöpfe in der Kopfzeile, jeder mit seiner eigenen Liste
darunter, und jede Liste hängt sichtbar an ihrem Knopf. Die Tonspur
erscheint auch dann, wenn es nur eine gibt — welche läuft, will man
wissen, nicht nur ob man wechseln kann. Die Untertitel-Taste ist
gefüllt, solange welche laufen.

Die Qualität steht als Text im Knopf, nicht als Symbol. Auch das aus dem
iPad-Player übernommen: "Drei Schieberegler sagen nicht, was eingestellt
ist; '1080p' schon — und genau das ist die Frage, wegen der man hintippt."

Dazu eine gleichmäßige Abdunklung statt zweier Verläufe an den Rändern.
Die Verläufe ließen die Mitte frei — wo die Transportknöpfe stehen, die
damit auf hellen Szenen nichts hinter sich hatten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:53:17 +02:00
ScarriffleandClaude Opus 5 9da446b295 Qualität, Tonspur und Untertitel im Player
Sie standen bisher nur in den Einstellungen — also dort, wo man sie nicht
braucht. Man merkt beim Schauen, dass der Ton die falsche Sprache hat,
nicht vorher.

Jetzt ein Menü über der Bedienleiste, nicht als Blatt: man stellt hier
etwas ein, das man am Bild beurteilen will, und ein Blatt verdeckt genau
das. Die Auswahl kommt vom Server — welche Spuren es gibt, weiß man erst
nach dem Aushandeln, und seine Vorauswahl gilt beim ersten Lauf.

Anders als die App, die im Lauf umschaltet, muss der Browser neu
aushandeln: bei einem umgerechneten HLS-Strom bestimmt der Server beim
Aushandeln, was drin ist. Also wird neu geladen — und die Stelle
mitgenommen, sonst begänne jeder Sprachwechsel den Film von vorn.
Nachgemessen: bei 27:30 gewechselt, bei 27:30 weiter.

Dabei zwei Dinge mitgenommen, die sonst später weh getan hätten: der
Fehler-Horcher wird nur einmal angehängt (sonst meldete sich nach zwei
Wechseln ein einziger Fehler dreimal), und ein früherer HLS-Strom wird
abgeräumt, bevor der nächste anläuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:47:31 +02:00
ScarriffleandClaude Opus 5 66b963f2c5 Keine Emojis als Symbole — und der Rückfall tat das Gegenteil
Zwei Dinge.

Erstens: Symbole sind jetzt Vektoren, keine Schriftzeichen. Ein Lautsprecher
als Emoji wird auf jeder Plattform anders gezeichnet, trägt fremde Farben,
sitzt schief auf der Grundlinie und lässt sich weder in Strichstärke noch
Farbe anpassen — es sieht nach Platzhalter aus, weil es einer ist. Elf
Stellen umgestellt, die Pfade stehen an einer Stelle beisammen.

Zweitens, und das ist der Grund, warum nichts lief: Nennt der Server keine
eigene Adresse, fiel der Code auf "static=true" zurück — also auf die Datei,
wie sie auf der Platte liegt. Bei einem MKV mit DTS ist das für einen Browser
unlesbar, und er meldet bloß "Format nicht unterstützt". Der Rückfall tat
genau das Gegenteil dessen, was er sollte. Jetzt verlangt er master.m3u8,
also ausdrücklich einen HLS-Strom.

Damit so etwas nicht wieder im Dunkeln liegt, nennt die Fehleranzeige nun
auch, was der Server geantwortet hat und welche Adresse verwendet wurde —
der Zugangsschlüssel darin gekürzt.

Dazu: während des Ladens treten die Transportknöpfe zurück. Der Kreisel lag
vorher genau hinter dem größten von ihnen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:43:03 +02:00
ScarriffleandClaude Opus 5 1db70379d7 Ein Player, und Serien starten die nächste Folge
Zwei berechtigte Beschwerden auf einmal.

Das war kein Player, sondern das nackte <video controls> des Browsers in
einem Blatt — ein eingebettetes Vorschaufenster. Jetzt Vollbild mit
eigener Bedienung: Titel und Folge oben, Spulen und Anhalten in der
Mitte, Fortschritt mit Restzeit unten, dazu Lautstärke, Tempo und
Vollbild. Die Bedienung blendet nach drei Sekunden Ruhe aus und kommt
bei jeder Bewegung zurück; steht das Bild, bleibt sie stehen — sie
auszublenden, während niemand spielt, versteckt den einzigen Knopf, der
weiterhilft. Tastatur: Leertaste, Pfeile, Escape.

Dunkle Knöpfe statt hell durchscheinender: über einer sonnigen Szene
verschwanden helle vollständig. Und die Sekundenzahl steht als Text, statt
im Spulpfeil zu stecken — übereinander war beides unlesbar.

Und bei einer Serie "spielt man nicht ab" zu antworten war Unsinn. Ich
hatte den Knopf entfernt, statt ihn richtig zu machen. Jetzt heißt er
"Weiterschauen" und startet die Folge, bei der man stehengeblieben ist —
über dieselbe Frage, die auch die Startseite stellt. Gibt es keine, wird
es die erste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:38:53 +02:00
ScarriffleandClaude Opus 5 ecfa454136 Das Web spielt jetzt ab — und sagt, wenn es nicht geht
Es fehlte der erste Schritt. Die Stream-Adresse wurde selbst
zusammengebaut, und damit fehlten PlaySessionId, die ausgehandelte
MediaSourceId und die Adresse, die der Server eigentlich liefert. Im
Swift-Code steht seit jeher, warum das nicht geht: "eine selbst
zusammengebaute Adresse wäre eine Vermutung darüber."

Jetzt vier Schritte wie in der App: PlaybackInfo mit Geräteprofil, die
Quelle aus der Antwort wählen, die Adresse des Servers nehmen, die
Sitzungskennung mitführen.

Vor allem aber sind Fehler nicht mehr unsichtbar. Vorher endete jeder
Fehlschlag in einem stummen schwarzen Kasten — ein 401 sah aus wie ein
404 und wie ein Netzproblem. Geprüft: Ablehnung, fehlende Quelle und
nicht abspielbares Format nennen jetzt alle drei Ross und Reiter.

Serien und Staffeln haben keinen Abspielknopf mehr. Sie sind Ordner;
/Videos/{serienId} zeigt auf nichts. Wer über eine Serienkachel getestet
hat, konnte gar nichts starten.

Dazu Fortschrittsmeldung an /Sessions/Playing — ohne die erscheint der
Titel auf dem Server nie als "läuft gerade", und "Weiterschauen" bliebe
im Web für immer leer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:29:17 +02:00
ScarriffleandClaude Opus 5 14e1fdbe3f Web: Reihen ordnen und ausblenden
Phase 3 des Abgleichs. Dieselbe Rechnung wie Arrangement in der App,
samt ihrer wichtigsten Eigenschaft: unbekannte Reihen landen am Ende,
statt zu verschwinden. Sie zu verwerfen wäre die einfachere Regel — und
der Grund dafür, dass eine neu angelegte Mediathek nie aufgetaucht wäre.

Ausgeblendete stehen in den Einstellungen weiter unten in der Liste,
durchgestrichen. Ohne das könnte man sie nie wieder einblenden.

Knöpfe statt Ziehen: auf dem Zeigegerät ist Ziehen schöner, auf einem
Trackpad fummelig und mit der Tastatur gar nicht zu machen. Zwei Pfeile
können alle drei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:12:11 +02:00
ScarriffleandClaude Opus 5 2e15cdfddb Erst sammeln, dann schnellen
Ein reiner Weg von A nach B sah aus, als würde ein Kasten verschoben.
Jetzt zwei Züge: die Leiste zieht sich erst in sich zusammen und zieht
dann erst um — wie ein Gummiband, das sich sammelt, bevor es schnellt.

Welche Richtung sich zuerst sammelt, hängt davon ab, wohin es geht. Wer
nach oben zieht, wird flach, also klappt zuerst die Höhe zusammen; wer
an die Seite zieht, wird schmal, also zuerst die Breite. Die
Zwischenstation steht deshalb noch am alten Ort, aber schon im Zielmaß
der einen Seite.

Nachgemessen: bei 220 ms ist die Breite von 2.27 auf 1.38 gefallen und
die Position unverändert — sie sammelt sich, ohne zu wandern. Bei 380 ms
ist sie unterwegs und zieht sich auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:09:40 +02:00
ScarriffleandClaude Opus 5 9320092ebf Web: gleiche Gliederung, und eine Erfindung weniger
Phase 2 des Abgleichs. "Plakate oder Querformat" fliegt raus — das ist
keine Einstellung, sondern eine Entscheidung, die die App bewusst selbst
trifft: Angefangenes erkennt man am Szenenbild wieder, Neues sucht man
nach Plakaten aus. Ich hatte sie zur Wahl gemacht, ohne dass jemand
danach gefragt hätte.

Dafür die Abschnitte in der Reihenfolge der App — Darstellung,
Startseite, Server — und die eigene Akzentfarbe als Hex-Feld, mit
derselben Kontrastrechnung nach WCAG 2.1. Nachgerechnet: ein dunkles
Marineblau landet auf genau 4.5, dem Grenzwert, und eine Farbe, die ihn
ohnehin erreicht, bleibt unberührt. Gesagt wird es auch — eine Farbe
stillschweigend zu verändern wäre schlechter, als sie abzulehnen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:04:40 +02:00
ScarriffleandClaude Opus 5 df75fa98a5 Kachelgröße und Aufmacherhöhe gibt es jetzt auch in der App
Phase 1 des Einstellungs-Abgleichs. Beide standen bisher nur im Web,
obwohl sie nirgends hingehören, wo es die Sache nicht überall gibt.

Die Kachelgröße ist ein Faktor und keine Punktzahl: die Breiten von
Plakat und Querformat stehen in einem Verhältnis, das stimmen soll —
zwei getrennte Zahlen ließen sich gegeneinander verstellen, bis eine
Reihe zur anderen nicht mehr passt. Sie wirkt auf Startseite und
Mediathek, sonst sähe das eine anders aus als das andere.

Sie steht bei der Darstellung, nicht bei der Startseite: es geht ums
Aussehen, und sie gilt für beide Seiten. Die Aufmacherhöhe steht bei der
Startseite, direkt bei Quelle und Anzahl.

Voreingestellt ist genau das bisherige feste Verhalten — wer nichts
anfasst, merkt nichts. Gelesene Werte werden auf ihren Bereich begrenzt,
damit eine ältere Ablage keine Kachel über den halben Bildschirm ergibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:02:50 +02:00
ScarriffleandClaude Opus 5 653951c1d8 Die Leiste zieht um, statt zu verschwinden und wiederzukommen
Der erste Versuch klappte die Spalte nach oben weg und schob die Insel
seitwärts herein. Das sind zwei Leisten, von denen eine geht und eine
kommt — Mac und iPad lassen dieselbe umziehen, und man verfolgt sie
dabei mit den Augen.

Jetzt FLIP: alte Lage messen, umbauen, neue Lage messen, und das Element
vom alten in den neuen Zustand fahren lassen. Die Größe fährt mit, denn
die Insel ist oben breit und flach und an der Seite schmal und hoch; ein
Sprung in der Form mitten in einer ruhigen Bewegung fällt mehr auf als
gar keine Bewegung. Über CSS allein ginge das nicht — "right: auto" und
"bottom: auto" lassen sich nicht animieren, und genau die wechseln hier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:01:01 +02:00
ScarriffleandClaude Opus 5 2874a89a6e Die Leiste zieht jetzt um, statt zu springen
Nachgebaut nach der App: die Spalte klappt nach oben weg und kommt von
oben zurück, die Insel schiebt sich seitwärts hinaus und herein. Der
Inhalt rückt gleichzeitig nach — ohne das bliebe links ein Loch stehen,
bis die Leiste angekommen ist.

Dazwischen eine Pause von gut zwei Zehnteln: ohne sie tauscht der
Browser die Leiste in einem Bild aus, und vom Umzug ist nichts zu sehen.

Dazu gestaffelt einblendende Reihen und Kacheln, die sich beim
Überfahren leicht anheben. Wer "Bewegung reduzieren" eingeschaltet hat,
bekommt von alledem nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:42:33 +02:00
ScarriffleandClaude Opus 5 baf8003445 Einstellungen sollen überall dieselben sein
Plan für den Abgleich zwischen App und Web. Beim Bau des Webfrontends
sind zwei Einstellungen entstanden, die es in der App nicht gibt, und
eine dritte, die ihrer Gestaltung widerspricht: Plakate oder Querformat
entscheidet die App bewusst selbst, weil man Angefangenes am Szenenbild
wiedererkennt und Neues am Plakat aussucht.

Gleiche Gliederung, Werte pro Gerät — am Fernseher größere Kacheln als
am Laptop, ohne dass eins das andere umstellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:40:38 +02:00
ScarriffleandClaude Opus 5 80292d3351 Der Umschalter gehört in die Leiste, nicht in die Einstellungen
Wie auf dem iPad: ein Knopf in der Leiste selbst — an der Seite oben in
der Ecke, oben ganz links. Ihn in die Einstellungen zu verbannen hieß,
für einen Handgriff zwei Ebenen tief zu gehen.

Dazu die Einstellungen, die bisher aus vier Zeilen bestanden: Aufmacher
(Quelle, Anzahl, Höhe), Reihen (Weiterschauen zusammenlegen, Gesehenes
ausblenden, Plakate oder Querformat), Kachelgröße, Akzentfarbe, Server.
Sie wirken auch — die Startseite baut sich danach auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:36:43 +02:00
ScarriffleandClaude Opus 5 c3f9afdb07 Der Aufmacher zeigte monatelang denselben Film
Er war nun einmal der erste in "Weiterschauen", und genommen wurden
schlicht die ersten fünf der Liste. Jetzt wird gezogen — aber nicht bei
jedem Blick: der Zufall hängt an einem Zeitfenster von sechs Stunden.
Wer die App dreimal am Abend öffnet, findet sich zurecht; wer am
nächsten Morgen wiederkommt, sieht etwas anderes.

Titel ohne Hintergrundbild sind dabei nicht ausgeschlossen, sondern
ziehen ein schlechteres Los. Sie ganz auszusperren war zu hart — in
einer Mediathek, in der wenige Titel eines tragen, blieben immer
dieselben drei übrig.

Dieselbe Regel im Web, damit beide nicht auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:34:09 +02:00
ScarriffleandClaude Opus 5 daa836fd8c Das Aufmacherbild lag im falschen Format, und die Punkte fehlten
Drei Dinge aus einer Runde Rückmeldungen.

Die Detailseiten forderten ihr Bild in 900×1200 an — hochkant — und
zeigten es in einem 16:9-Rahmen; zu sehen war ein Streifen aus der
Mitte. Derselbe Fehler war im Aufmacher schon einmal behoben worden, nur
hier nicht. Beide gehen jetzt über HeroArtwork, wo die richtigen Maße
schon stehen.

Im Web hingen die Karussellpunkte neben dem Aufmacher, das CSS zielte
aber hinein — sie waren unsichtbar. Jetzt sind sie da, anklickbar, und
es gibt Pfeile, Wischen und Pfeiltasten dazu. Wer selbst blättert, hält
das Weiterwechseln an: sonst springt der Aufmacher genau in der Sekunde,
in der man nach "Abspielen" greift.

Dazu Seitenleiste und Einstellungen fürs Web — Akzentfarbe aus derselben
Palette wie die App, Navigation oben oder seitlich, und ein Regler für
die Kachelgröße. Das Filmlogo ist um zehn Prozent gewachsen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:30:19 +02:00
ScarriffleandClaude Opus 5 018eaa3308 Ein verpasster Bildabruf war endgültig
Auf einem großen Fenster blieb die zweite Reihe fast vollständig grau,
während die erste geladen hatte. Es lag weder am Server noch an
fehlenden Bildern: sechzig sichtbare Kacheln teilen sich zwölf
Verbindungen, und die hinteren stehen so lange in der Schlange, dass sie
in den Zeitablauf laufen. Oben fragt niemand ein zweites Mal — task(id:)
läuft einmal je Adresse.

Jetzt wird bis zu dreimal gefragt, mit wachsendem Abstand. Aber nur, wo
es sich lohnt: ein 404 heißt, dass es das Bild nicht gibt, und dreimal
zu fragen macht es nicht vorhanden. Wiederholt werden Zeitabläufe,
abgerissene Verbindungen, 5xx und 429.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:18:30 +02:00
ScarriffleandClaude Opus 5 cdef325dcd Flixy Web — erster Wurf
Eine Datei, die gegen Jellyfin spricht: Anmeldung, Startseite mit
Aufmacher und Reihen, Mediathek als Raster, Wiedergabe über HLS. Die
Endpunkte sind aus JellyfinKit übernommen, nicht geraten — inklusive der
Anführungszeichen in der Authorization-Zeile, ohne die Jellyfin die Zeile
falsch zerlegt.

Gestaltet aus denselben Werten wie die App: Flächen, Radien, Abstände aus
FlixyDesign. Die Navigationsinsel schwebt über einem randlosen Aufmacher
und fährt beim Scrollen weg — dieselbe Entscheidung wie dort.

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