**Intro überspringen gab es für Jellyfin nie.** Der Knopf, die Marken,
das automatische Überspringen, die Einstellungen dazu — alles stand seit
Langem im Player und auf dem Fernseher. Nur kam bei Jellyfin nie eine
Marke an: `JellyfinItem+Media.swift` setzte `markers: []`, weil Jellyfin
sie nicht am Titel liefert.
Seit 10.10 gibt es `/MediaSegments/{itemId}`, gefüllt von einem Anbieter
im Server wie dem Intro-Skipper. Die Anfrage läuft **neben** dem Abruf
des Titels, nicht danach — sonst wartete die Detailseite zweimal.
Zwei Dinge, die sonst eine Stunde kosten: `includeSegmentTypes` muss
**zweimal** stehen, nicht einmal mit Komma (mit Komma antwortet Jellyfin
mit null Treffern), und die Abfrage wirft nicht. Ein Server ohne Anbieter
antwortet leer, einer vor 10.10 mit einem 404 — und ein Film ohne Intro
ist kein Fehler, sondern der Normalfall.
Geprüft im Simulator: die Attrappe meldet jetzt Segmente, und im Player
steht „Intro überspringen".
---
**„Aus Weiterschauen entfernen".** Eine halbe Stunde gesehen und keine
Lust mehr — der Titel ist weder gesehen noch ungesehen, er soll nur nicht
ewig oben stehen. Beide Server können es, auf verschiedenen Wegen:
- **Plex** hat einen eigenen: `PUT /actions/removeFromContinueWatching`.
**Nicht** `/:/unscrobble` — das setzt zugleich auf ungesehen.
- **Jellyfin** hat keinen. Was es gibt, ist das Benutzerdatenblatt; die
Liste führt, was eine gemerkte Stelle grösser null hat. Also wird das
Blatt gelesen, bekommt eine Null und geht so, wie es war, zurück.
**Erst lesen, dann schreiben**, weil dort auch „gesehen", „Favorit" und
die Bewertung stehen — wer seinen Favoriten verliert, weil er einen
Film aus einer Liste genommen hat, sucht den Grund nie dort.
Der Eintrag erscheint nur, wenn es eine gemerkte Stelle gibt.
---
**Die Mediatheken für den Aufmacher taten zu wenig.** Die Regel „Reihen
ohne Mediathek bleiben immer drin" war freundlich gemeint und falsch: bei
„nur Filme" stand weiter eine Serie oben, weil sie aus „Weiterschauen"
kam. Gemessen und nicht vermutet — es lag nicht am Zeitfenster der
Auswahl, wie zuerst angenommen.
Jetzt gilt: ist etwas gewählt, speist sich der Aufmacher **nur** daraus.
Wer Weiterschauen oben haben will, wählt keine Mediathek. Der Hinweis
unter der Auswahl sagt das auch.
---
**Der „Bearbeiten"-Knopf stand über allem und tat dort nichts.** Er gilt
nur für die Reihen; jetzt steht er an deren Überschrift.
---
Die Attrappe kann drei Dinge mehr, sonst wäre nichts davon prüfbar:
Segmente, `parentId` bei den Neuzugängen (vorher stand in jeder Mediathek
dasselbe) und damit überhaupt eine Mediathekenauswahl, die man ansehen
kann.
iOS und tvOS bauen, 468 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mehrfachauswahl in den Einstellungen unter „Startseite": welche
Mediatheken den Aufmacher speisen dürfen. Haken statt Auswahlliste —
eine Liste könnte nur eine, und „keine" wäre dort ein Eintrag, den man
versehentlich trifft.
Dafür musste eine Reihe sagen können, woher sie kommt: `MediaRowGroup`
trägt jetzt eine `libraryID`. Jellyfin setzt sie an den „Neu in …"-Reihen
— die baut die App ohnehin je Mediathek.
Zwei Regeln, und beide sind Entscheidungen, keine Nebenwirkungen:
**Leer heisst alle.** Wer nichts ankreuzt, meint „mir ist es gleich",
nicht „nimm nichts". Ein leerer Aufmacher wäre der Fehler, den niemand
mit dieser Einstellung in Verbindung brächte.
**Reihen ohne Mediathek bleiben immer drin.** Weiterschauen, Als Nächstes
und Favoriten gehen quer durch alle, und aus welcher Mediathek eine
angefangene Folge stammt, sagt der Server an keiner Stelle. Sie
wegzuwerfen hiesse, jemandem das Angefangene zu nehmen, weil er
„Musikvideos" abgewählt hat. Der Hinweis unter der Auswahl sagt das auch
dem, der nicht in den Quelltext sieht.
Eingeschränkt wird **nur der Aufmacher**, nicht die Startseite: die
Reihe selbst bleibt stehen. Wer sie ganz loswerden will, blendet sie aus
— das ist eine andere Frage und hat ihren eigenen Schalter.
Fünf Tests an `HeroSelection.rows(_:limitedTo:)` halten die Regeln fest,
darunter der Fall einer gespeicherten Kennung, die es nicht mehr gibt.
**Plex bleibt aussen vor:** dort kommen die Reihen als fertige Hubs vom
Server, und `librarySectionID` wird in `PlexHub` nicht gelesen. Die
Auswahl schränkt dort also nichts ein. Nachzuholen, wenn Plex wieder
dran ist.
iOS und tvOS bauen, 468 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bisher entschied der Decider: steht nur die Schachtel im Weg, wird
umgepackt — stimmt ein Codec nicht, geht es an VLC. Das war beim Ton zu
grob gedacht.
Beim Umpacken nennt das Geräteprofil dem Server, was ankommen darf:
"VideoCodec": "h264,hevc"
"AudioCodec": "aac,ac3,eac3,mp3"
Trifft ein Strom eine dieser Angaben, kopiert der Server ihn; trifft er
sie nicht, rechnet er **genau diesen einen** um. Eine Matroska mit HEVC
und DTS braucht also kein VLC: das Bild geht Byte für Byte durch, und nur
die Tonspur wird nach AC3 oder AAC kodiert. Das kostet Tonqualität und
sonst nichts.
Die Bedingung heisst deshalb nicht mehr „nur die Schachtel", sondern
„das Bild bleibt": Container und Ton dürfen im Weg stehen, der
Videocodec nicht. Dazu musste die Prüfung der Schachtel aufhören, sich
immer zu stellen — eine MP4 mit DTS stand mit einem reinen Tonhindernis
da und fiel trotzdem durch die Liste umpackbarer Container.
**Der grössere Gewinn liegt auf dem Apple TV.** Dort gibt es VLC nicht,
und bisher landete eine Datei mit DTS-Ton beim vollen Neurechnen — Bild
inbegriffen, obwohl mit dem Bild nichts war. Jetzt bleibt das Bild
unberührt. Auf iPhone, iPad und Mac verhält sich derselbe Film jetzt
gleich, und zwar über AVPlayer: mit Bild-in-Bild, AirPlay und
Sperrbildschirm, die es auf dem VLC-Weg nie gab.
Geprüft im Simulator gegen die Attrappe mit `--vlc` (meldet DTS): der
Film läuft, und in der Leiste steht jetzt der AirPlay-Knopf, der vorher
fehlte — der sichtbarste Beleg dafür, dass AVPlayer spielt und nicht VLC.
„Umgepackt" und „umgepackt, Ton umgerechnet" sind in den technischen
Infos und in der Diagnose auseinandergehalten. Ein Weg, der den Ton neu
kodiert, darf nicht „ohne Neukodierung" heissen.
Was bleibt bei VLC: nicht unterstützte **Bild**codecs, allen voran AV1,
und Dateien, die der Server noch nicht untersucht hat. Dafür gäbe es nur
das volle Neurechnen — eine eigene Entscheidung, nicht diese.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Die Mediatheken sind Reiter geworden.** Sie standen unten auf der
Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen
vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem
die Vorschläge egal sind. Damit fällt die Reihe unten weg: zwei Wege zum
selben Raster sind auf einem Fernseher kein Komfort, sondern eine Frage,
welcher der richtige ist.
Die Leiste liest sich jetzt „Start · Filme · Serien · Gemischtes ·
Suchen" und wird von tvOS mittig gehalten, egal wie viele Mediatheken
dazukommen. Die Stelle der Suche bestimmt Apple, nicht diese Datei:
`role: .search` setzt sie dorthin, wo sie in jeder anderen App auch
steht, und gibt ihr die Lupe.
**Und eine Statuszeile fürs Regal.** Bleibt es leer, gibt es genau drei
Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, oder sie ist
da und das System fragt trotzdem nicht. Von aussen sehen alle drei gleich
aus — nämlich nach dem grossen Plakat aus dem Asset-Katalog. Nach zwei
Runden Vermuten steht unten links jetzt, welcher Fall es ist: „3 Reihen,
9 Einträge, zuletzt 21:55" oder eben „Keine App-Gruppe".
Nur im Entwicklerbau (`#if DEBUG`) und als Überlagerung, nicht als letzte
Zeile der Liste: dorthin müsste man erst an allen Reihen vorbeiscrollen,
und wer nachsieht, warum das Regal leer ist, soll es sofort sehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Backend konnte die Suche längst; es fehlte die Oberfläche.
**`.searchable`, keine eigene Tastatur.** tvOS macht daraus einen eigenen
Bildschirm mit Rastertastatur oben und Ergebnissen darunter — die
Bedienung, die jeder von Apples Apps kennt. Etwas Eigenes zu bauen hiesse,
sie nachzuahmen und dabei die Diktierfunktion zu verlieren.
**Herausgelöst statt kopiert**, wie schon bei Start und Mediathek:
`SearchFeed` trägt das Entprellen, die Mindestlänge und die zuletzt
gesuchten Begriffe. Drei Stellen, an denen es nicht offensichtlich ist —
und die dritte ist die wichtigste: geprüft wird auf `Task.isCancelled`
und nicht auf `CancellationError`, weil eine abgebrochene Netzanfrage als
Übertragungsfehler zurückkommt. Die naheliegende Prüfung lief ins Leere,
und jeder Tastendruck leerte kurz die Liste.
**Und erst jetzt eine Reiterleiste.** Bis hierher gab es genau einen
Bildschirm, und eine Leiste mit einem Eintrag wäre Zierrat gewesen. Der
Suchreiter trägt `role: .search`: tvOS stellt ihn dadurch ans Ende und
gibt ihm die Lupe, dieselbe Stelle wie überall sonst. Keinen dritten
Reiter für die Mediatheken — die stehen als Reihe auf der Startseite, und
ein Reiter verspräche einen eigenen Bereich, hinter dem dasselbe Raster
läge.
Der offene Reiter wird ausdrücklich gehalten, nicht dem System
überlassen: ein Link aus dem Regal über der App führt zur Startseite, und
stünde gerade die Suche offen, passierte sichtbar nichts.
Nebenbei bekommt die iOS-Suche einen Knopf zum Vergessen des Verlaufs.
Wischen zum Löschen gab es dort, am Fernseher gibt es das nicht — und
eine Liste, die sich nur auf einer Plattform leeren lässt, ist keine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
**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>
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>
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>
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>
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>
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>
`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>
**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>
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>
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>
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>
Der dritte Modus neben Raster und Senderliste — und der einzige, den es
nicht überall gibt. Bei Plex fehlt schlicht der Schalter dorthin, denn
supportsRecordings fragt die eigene Fähigkeit ab.
Vormerken sitzt im Blatt, das eine angetippte Sendung ohnehin schon
öffnet, statt in einem zweiten Kontextmenü daneben. Trägt die Sendung
bereits einen Serien-Timer, steht dort das statt der Knöpfe: zweimal
vormerken ist kein Wunsch, sondern ein Versehen.
Aufnahmen führen über die Detailseite statt unmittelbar in den Player.
Eine Aufnahme ist auf dem Server ein gewöhnlicher Eintrag, und erst dort
steht, in welchen Fassungen sie vorliegt — von der Liste aus zu starten
hieße, das zu raten.
Zwölf Schlüssel in DE und EN. Die Übersetzungsdatei ist dabei auf das
Format zurückgeschrieben worden, das auch im letzten Commit steht;
inhaltlich ist sie HEAD plus diese zwölf, nachgerechnet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keine Datei unter Views/LiveTV importiert noch PlexKit. Sender, Programm
und Einschalten laufen über den neutralen Vertrag, und supportsLiveTV
fragt endlich die Fähigkeit ab statt "server != nil" — womit die Zusage
in JellyfinBackend.capabilities zum ersten Mal stimmt.
Der Player holt die laufende Sendung jetzt für genau einen Sender nach.
Vorher ging dafür das Raster über alle Sender hinaus, weil der Plex-Weg
keinen schmaleren kannte; im neutralen Vertrag ist die Senderliste ein
Parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unter dem Folgentitel stand „S2 E1" als Text, und wer zur Serie wollte, musste
darunter eine Knopfreihe finden. Jetzt steht dort „Serienname · Staffel 2 ·
Episode 1", und Serienname wie Staffel führen dorthin, wohin sie zeigen. Der
Name steht ohnehin da — dann soll er auch der Weg sein.
`HeroBanner` nimmt dafür wahlweise eine fertige Zeile entgegen statt eines
Textes. Gebaut wird sie beim Aufrufer, weil nur der weiß, wohin ein Teil führt;
die Gestaltung kennt keine Ziele.
Die Knopfreihe bleibt bei Staffeln, wo sie der einzige Weg nach oben ist, und
entfällt bei Folgen.
Beim Entfernen eines Downloads gibt es keine Rückfrage mehr. Sie stand da, weil
„löschen" nach etwas Endgültigem klingt — es ist aber nur die heruntergeladene
Kopie, der Titel bleibt auf dem Server. Wer nach links wischt und auf „Löschen"
tippt, hat zweimal gesagt, was er will.
Und das Umschalten sieht jetzt nach Umzug aus: die Seitenleiste klappt nach
oben weg, die Leiste oben schiebt sich seitwärts herein — und zurück genauso.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Seitenleiste lässt sich wieder bearbeiten: Ziehen zum Umsortieren, Haken
zum Ein- und Ausblenden. Das Anordnen macht `Arrangement` aus MediaKit — dieselbe
Rechnung samt Tests, die schon die Reihen der Startseite ordnet, mit ihren
kniffligen Fällen: eine Mediathek, die neu auftaucht, und eine, die aus der
gespeicherten Reihenfolge verschwindet. Gespeichert wird über eine feste
Kennung **ohne** den Titel, damit eine umbenannte Mediathek ihren Platz behält.
Die Suche ist auf dem iPhone kein eigener Reiter mehr. Die Leiste unten hat
fünf Plätze, und einer davon für etwas, das man in einer Mediathek sucht, ist
einer zu viel — sie sitzt jetzt über der Mediathek, neben Filter und Ansicht.
Der Umschalter zwischen Seite und oben ließ sich auf dem Mac kaum treffen: ein
Knopf aus einem Zeichen mit Rand hat ohne `contentShape` nur das Zeichen als
Ziel. Beide Umschalter haben jetzt eine Fläche.
Die Leiste oben trägt dieselbe Schriftgröße wie die Seitenleiste. In
Beschriftungsgröße war sie auf einem großen Bildschirm winzig.
Und eine Maske: der Aufmacher greift nach links unter die Seitenleiste, scrollt
dabei aber, während die Leiste steht — über und unter ihr blieb je ein Streifen
Bild stehen. Die Maske sitzt im festen Teil und lässt genau zwei Flächen übrig:
alles ab der Inhaltskante, und die Fläche der Leiste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sie auszublenden reichte nicht: solange `mode` auf `tabSidebar` steht, gehört
die Seitenleiste zur Darstellung, und das System stellte sie wieder her — mit
eigenem Pfeil und eigenem Knopf, hinter unserer. Auf dem Simulator fiel das
nicht auf, auf dem Mac standen beide da.
Jetzt wird erst die Form auf `tabBar` gestellt und dann die Tableiste
ausgeblendet. Damit gibt es keine Seitenleiste mehr, die zurückkommen könnte.
Der Test setzt die Stellung der Leiste über ein Startargument, statt zu nehmen,
was ein voriger Lauf hinterlassen hat — er fing sonst beim falschen Bild an.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die System-Seitenleiste sitzt auf dem iPad als schwebende Fläche und auf dem
Mac angeklebt am Fensterrand. `preferredLayout = .overlap` ist die dafür
vorgesehene Bitte, und macOS verwirft sie stillschweigend — gemessen, nicht
vermutet.
Statt deshalb die ganze Navigation neu zu bauen, bleibt der `TabView` stehen
und verliert nur sein Kleid: `TabChromeSuppressor` blendet Seiten- und
Tableiste aus, gezeichnet wird darüber. Auswahl, ein eigener Navigationsstapel
je Eintrag, erhaltener Scrollstand beim Wechseln — all das macht weiter das
System. Dass das Ausblenden geht, war kein Ratschluss: derselbe Griff hatte die
Leiste vorher schon versuchsweise verschwinden lassen.
Neu sind `FlixySidebar` (schwebende Fläche mit eigenem Rand, Mediatheken
einklappbar, Konto unten) und `FlixyTopBar` (dieselben Einträge als Leiste
oben, Mediatheken als Menü, weil oben der Platz waagerecht begrenzt ist). Das
iPhone bleibt beim System: dort gibt es keine Seitenleiste, sondern die Leiste
unten, und die ist überall dieselbe.
Der Platz für die Leiste ist ein **Sicherheitsrand**, kein Abstand, und er
hängt je Bildschirm — weder `TabView` noch `NavigationStack` reichen ihn an
ihren Inhalt weiter. Damit bleibt eine Reihe fensterbreit und schiebt ihre
Kacheln beim Wischen hinter die Leiste, stellt aber im Ruhezustand keine
dorthin. Der Aufmacher darf über seinen Bereich hinaus; das `.clipped()` im
Karussell musste dafür weichen, es schnitt genau das ab.
Noch nicht dabei: „Bearbeiten" mit Ziehen und Ein-/Ausblenden. Die Einträge
stehen in fester Reihenfolge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fehler auf einmal, und alle drei meine.
**Die App kam gar nicht ins Netz.** Ein Catalyst-Programm läuft in der Sandbox
und braucht eine ausdrückliche Erlaubnis für ausgehende Verbindungen. Ohne sie
gibt es keine Fehlermeldung — der Ladekreis dreht sich einfach für immer, und
genau so sah es aus.
**In der Seitenleiste standen Schlüssel statt Wörtern** — „sidebar.downloads"
statt „Downloads". Ich hatte zwei Fassungen derselben Beschriftung gebaut, eine
mit `LocalizedStringKey` und eine mit `String` für Titel, die vom Server
kommen. Bei einem Literal darf Swift sich für beide entscheiden, und es
entschied sich nicht überall gleich. Jetzt gibt es eine Fassung; wer einen
fertigen Text hat, baut sein `Text` selbst.
**Das Fenster öffnete wieder in Notizzettelgröße.** Meine Größenangabe hing an
`os(macOS)` — unter Catalyst gilt aber `os(iOS)`, also griff sie nicht. Dort
hängt die Größe ohnehin an der Szene, nicht am Fenster.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Eine Taste zum Vergrößern** unten rechts auf der Kachel. Das Antippen der
Fläche vergrößert weiterhin, aber auf derselben Fläche liegen schon das Ziehen
und das Aufziehen — und was dabei danebengeht, sieht aus wie „reagiert nicht".
Ein Knopf geht nicht daneben.
**Ton und Untertitel getrennt.** Es sind zwei verschiedene Fragen: die Tonspur
stellt man einmal am Anfang um, die Untertitel mitten im Film. In einem
gemeinsamen Blatt scrollte man jedes Mal an der anderen Liste vorbei. Das
Untertitel-Symbol ist gefüllt, solange welche laufen — so sieht man es, ohne
das Blatt zu öffnen. Und eine Auswahl schließt das Blatt jetzt selbst.
**AirPlay.** Nicht nachgebaut, sondern die Systemstelle: sie kennt Geräte, die
eine App nie aufzählen könnte. Der Knopf erscheint nur beim Systemplayer — VLC
zeichnet selbst und kann kein AirPlay, und ein Knopf, der dort ins Leere
führte, wäre schlimmer als keiner.
**Die Qualität steht als Text da**, nicht als drei Schieberegler: „1080p",
„720p", „Auto". Bei „automatisch" steht dort, was gerade tatsächlich ankommt —
das weiß nur der Player, und ihn zu fragen ist ehrlicher, als „Auto"
hinzuschreiben und die Antwort schuldig zu bleiben.
Dazu die Einstellungen mit farbigen Symbolen und einer Kopfkarte je Bereich.
Das ist nicht nur Zierde: in einer Liste aus zehn gleich aussehenden Zeilen
findet das Auge nichts wieder, und „das mit dem blauen Zahnrad" ist eine
brauchbare Beschreibung. Die Farben sind fest und ausdrücklich **nicht** die
Akzentfarbe — trüge alles dieselbe, unterschiede sie nichts mehr.
Und der Aufmacher läuft jetzt unter der Seitenleiste weiter
(`backgroundExtensionEffect`): die Ränder werden gespiegelt und weich
fortgeschrieben, während Titel und Knöpfe stehen bleiben. Vorher endete das
Bild an der Kante der Leiste, und beim Weiterwandern schob sich das nächste an
einer harten Linie ins Bild.
263 Tests grün. Player-Knöpfe und Einstellungen angesehen; den Aufmacher hinter
der Leiste konnte ich an den einfarbigen Flächen der Attrappe nicht sicher
beurteilen — der Simulator ist dabei erneut abgestürzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Simulator läuft wieder. Ich habe alles durchgeklickt, was ich dir in den
letzten beiden Runden ungeprüft gegeben habe. Es war nicht in Ordnung.
**Zwei Navigationsleisten übereinander.** Auf dem iPhone hat die Leiste unten
fünf Plätze; alles darüber schiebt das System in ein „Mehr"-Verzeichnis — und
dessen Einträge bekamen zwei Zurück-Pfeile, einen vom System und einen von
uns. Statt das zu flicken, gibt es jetzt die **Bibliothek**, die du ohnehin
wolltest: auf dem iPhone ein Eintrag, hinter dem alle Mediatheken stehen, auf
dem iPad weiter jede einzeln in der Seitenleiste. Damit bleibt die Leiste bei
fünf Einträgen, und das „Mehr" entfällt.
**Kacheln in der Mediathek öffneten nichts.** Kein Fehler, keine Meldung —
Antippen führte einfach ins Leere. Der Grund: in einem Stapel dürfen nicht
beide Arten von Verknüpfung nebeneinander vorkommen. Sobald ein Bildschirm
über eine **Ansichts**-Verknüpfung geschoben wurde, fanden die
**Wert**-Verknüpfungen darin ihr Ziel nicht mehr. Jetzt ist alles wertbasiert,
und das Ziel für Detailseiten steht **einmal je Stapel** an dessen Wurzel statt
in jedem Bildschirm einzeln.
**Der Lupenknopf in der Kopfzeile zeigte auf ein Ziel, das es nie gab.** Kein
Bildschirm hat je ein `SearchRoute`-Ziel angemeldet — der Knopf tat seit jeher
nichts. Er ist weg; die Suche hat ihren eigenen Platz in der Leiste.
**Der Download-Dialog zeigte Schlüssel statt Namen** — „quality.hd" statt
„720p". Die drei Übersetzungen fehlten schlicht.
**Die Kontokarte wuchs aus dem Bild heraus.** Die Leiste unten in der
Seitenleiste hat eine feste Höhe; meine aufklappende Karte schob die Zeile mit
dem Namen nach unten hinaus. Jetzt hängt sie als Popover darüber — die Zeile
bleibt stehen, wie bei JellyTV.
Verkleinerte Downloads bei Jellyfin laufen: Stufen erscheinen, die Adresse
stimmt, die Datei kommt an. Dass die Attrappe nur 28 kB liefert und die App
das als „keine Videodatei" zurückweist, ist richtig so — die Prüfung tut, was
sie soll.
Zwei Gruben in der Attrappe nebenbei gestopft: sie kannte `/stream.mp4` nicht
und meldete ihr Testbild als 360p, womit es gar keine kleineren Stufen geben
konnte.
263 Tests grün. Diesmal alles angesehen: iPhone und iPad, Einstellungen,
Bibliothek, Detailseite, Download-Dialog, Kontokarte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deine Einstellungs-Bilder sind eine lange Liste. Ich habe sie durchgegangen
und getrennt: was etwas steuert, ist gebaut; was eine Einstellung für eine
Sache wäre, die es nicht gibt, ist nicht gebaut. Ein Schalter, der nichts
bewirkt, ist schlimmer als kein Schalter.
**Die Einstellungen sind jetzt eine Übersicht.** Vorher stand alles
untereinander auf einem Bildschirm — Serverangaben, Qualität, Wiedergabe, drei
Verweise, Downloads, Version, Abmelden. Das war beim Bauen bequem und beim
Suchen anstrengend. Jetzt führt jede Zeile in einen Bildschirm, der zu einer
Frage gehört: Server, Erscheinungsbild, Startseite, Wiedergabe, Ton &
Untertitel, Downloads, Diagnose.
**Neu, und jedes davon greift irgendwo:**
- Zurück- und Vorspulen getrennt einstellbar. Die Knöpfe im Player und die
Doppeltipp-Flächen tragen danach auch das passende Symbol.
- Startgeschwindigkeit — die Engine kann es längst, gefragt wurde nie.
- „Im Querformat sperren" abschaltbar. Für die allermeisten Filme richtig, für
den, der im Bett auf der Seite liegt, das Gegenteil.
- „Gewählte Qualität merken": die Stufe gilt dann auch für den nächsten Titel.
Aufbewahrt wird eine **Zahl**, keine Sprosse — welche Sprossen es gibt, hängt
von der Quelle ab, und eine 720p-Datei kennt keine Zwanzig-Megabit-Stufe.
Dann kommt die nächstgelegene, nicht „automatisch".
- „Bitrate im Qualitätsmenü zeigen".
- Gleichzeitige Downloads. Der Wert gab es schon in der Engine, nur ohne
Bedienung und ohne dass er einen Neustart überlebte.
- Aufmacher: wie viele Titel, und ob sie aus Angefangenem oder nur aus
Neuzugängen kommen.
- „Weiterschauen und Als Nächstes zusammenlegen" und „Gesehenes aus
Neuzugängen ausblenden" — beides in der Ansicht, nicht im Server: das sind
Fragen der Darstellung, und so gelten sie für Plex und Jellyfin gleich.
**Verkleinert herunterladen geht jetzt auch bei Jellyfin.** Da hatte ich zu
früh aufgegeben: ich hatte nur nach der Adresse zum *Abspielen* gesucht, und
die ist eine Abspielliste mit hunderten Schnipseln — als Datei unbrauchbar.
`/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als eine Datei heraus. Die
Größe bleibt eine Schätzung; eine Umrechnung, die erst beim Abruf beginnt,
kennt ihre eigene Länge nicht.
**Die Kontozeile ist jetzt eine Karte, die nach oben aufklappt**, kein
Systemmenü mehr. Ein Menü legt sich als eigene Ebene über alles und springt
dorthin, wo Platz ist; die Karte bleibt, wo sie ist, und lässt die Zeile mit
dem Namen stehen — man sieht also weiter, um wessen Konto es geht.
**Nicht gebaut, mit Grund:** Trailer im Karussell, Poster- und Sammlungsstil,
Untertitel-Aussehen, sekundäre Untertitel, lokale Intros, Modular Home,
App-Symbol. Für all das fehlt das, was die Einstellung steuern würde — es gibt
in Flixy weder Trailer noch Sammlungen, und der Player zeichnet Untertitel
nicht selbst.
263 Tests grün. **Nicht angesehen**: der Simulator-Tunnel ist weiterhin
abgestürzt und ließ sich nicht wieder aufbauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Dinge aus deinen Einstellungs-Bildern.
**Das Konto sitzt jetzt unten in der Seitenleiste**, nicht als gleichrangiger
Eintrag zwischen den Mediatheken. Dort sucht man es, und von dort führt der
Weg in die Einstellungen und zum Abmelden. Auch das ist eine Systemstelle —
`tabViewSidebarBottomBar` —, also dieselbe Zeile, an der sie bei JellyTV
steht. Auf dem iPhone gibt es sie nicht; dort bleibt es beim eigenen Eintrag.
Der Buchstabe auf der Scheibe ist kein Platzhalter für ein Bild, das noch
kommt: Jellyfin gibt kein Profilbild heraus, solange keines hinterlegt ist,
und Plex kennt den Kontonamen am Server gar nicht. Bei Plex steht deshalb der
Servername oben und darunter nichts — statt zweimal dasselbe.
**Ton- und Untertitelsprache lassen sich jetzt einstellen.** Beim Durchsehen
deiner Bilder ist mir aufgefallen, dass es diese Vorliebe längst gibt: der
Player liest sie bei jeder Wiedergabe aus und wählt die passende Spur. Nur
äußern konnte man sie nirgends — sie stand in den Voreinstellungen und blieb
dort für immer leer. Eine Vorliebe, die niemand äußern kann, ist keine.
Fünfzehn Sprachen, keine vollständige Liste: ein Rad mit zweihundert
Einträgen, um „Deutsch" zu finden, wäre keine Hilfe. Was fehlt, kommt später
mit einem Suchfeld dazu.
Gebaut und übersetzt, aber **nicht angesehen**: der Simulator-Tunnel ist
mitten im Prüfen abgestürzt und ließ sich nicht wieder aufbauen. Dass es die
Systemstelle gibt, beweist der Compiler; wie sie aussieht, habe ich diesmal
nicht gesehen.
258 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Dinge aus dem letzten Durchgang.
**Die Startseite hatte zu wenig anzubieten.** Es gab eine Reihe „Zuletzt
hinzugefügt" für alles zusammen. Jetzt gibt es eine je Mediathek — „Neu in
Filme" und „Neu in Serien" sind zwei verschiedene Fragen, und wer nach einem
Film sucht, blättert in einer gemeinsamen Reihe an Folgen vorbei. Dazu
Favoriten, die es bei Jellyfin ohnehin gibt. Die drei Abfragen laufen
nebeneinander; nur die Mediatheken werden streng geholt, weil sie die Probe
darauf sind, ob der Server überhaupt antwortet.
**Die Seitenleiste ist jetzt echtes Glas**, nicht das alte Material: im
ausfahrbaren Blatt über `glassEffect`, in der geteilten Ansicht auf dem iPad
vom System selbst — dort lag mein eigener Hintergrund darüber und machte ihn
stumpf. Die zum Inhalt zeigende Kante ist gerundet, damit das Blatt sichtbar
über der Seite liegt statt daneben zu stehen.
**Die Navigation kann nach oben.** Eine Einstellung unter Erscheinungsbild,
seitlich oder oben. Oben stehen alle Einträge gleichzeitig da, als Glaskapseln
in einer waagerecht scrollenden Reihe — die Zahl der Mediatheken kommt vom
Server, und zusammengequetschte Beschriftungen wären die schlechtere Antwort
als eine Reihe, die weiterläuft.
Angesehen auf iPhone und iPad: beide Formen, das Glas in beiden Leisten, die
neuen Reihen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bis hierher konnte man sich bei Jellyfin anmelden und stöbern, aber nichts
abspielen. Der Player war die letzte große Stelle, die Plex unmittelbar
kannte.
Drei Mitglieder kommen dafür in den Vertrag: eine Wiedergabe vorbereiten,
sie beenden, den Fortschritt melden. Die Entscheidung **ob** umgerechnet
wird, fällt weiterhin vorher im `PlaybackDecider` — der Server führt sie nur
aus. Genau diese Trennung fehlte: der Player hat Plex' Transcoder
angesprochen und konnte deshalb mit nichts anderem reden.
`MediaBackendError` bekommt einen siebten Fall: `refused(reason:)`. Der
Server hat verstanden und sagt nein, in eigenen Worten. Bei Plex war genau
dieser Satz die einzige brauchbare Spur, und er ging tagelang verloren, weil
es dafür keinen Fall gab.
Bei Jellyfin kommt die Adresse einer umgerechneten Fassung vom Server selbst
(`PlaybackInfo` → `TranscodingUrl`), nicht von uns. Er weiß, welche Spuren er
kopieren kann und welche er neu rechnen muss.
Drei Dinge, die beim Hinsehen auffielen:
- **`startTimeTicks` gehört nicht in die Anfrage.** Sonst entscheiden zwei
Stellen über den Startpunkt: der Server ließe den Strom dort beginnen, und
der Player spränge noch einmal um dieselbe Strecke vor. Bei einundzwanzig
Minuten Fortschritt landet man bei zweiundvierzig.
- **Der erste Bericht braucht einen eigenen Endpunkt.** Ohne ihn erscheint
der Titel bei Jellyfin nirgends als „läuft gerade". Er fiel zunächst aus,
weil er die Laufzeit verlangte — und die steht erst Sekunden nach dem
Öffnen fest. Jetzt tut es für die erste Meldung die des Servers.
- **In der Download-Adresse standen Titel und Fassung vertauscht.** Bei einem
Film mit einer Fassung sind beide Kennungen dieselbe und nichts fällt auf;
bei zweien schon. `MediaFile` sagt jetzt, zu welchem Titel sie gehört.
Live-TV bleibt bei Plex und wird als Einziges noch unmittelbar durchgereicht.
Ein Tuner ist ein Stück Hardware, und die fünfhundert Zeilen, die es gekostet
hat, ihn zu bändigen, gehören in keinen gemeinsamen Vertrag.
Nachgesehen, nicht nur gebaut: gegen die Attrappe läuft ein echtes Video, der
Beginn wird gemeldet, der Fortschritt kommt in Ticks mit Sitzung und Fassung
an. Was die Attrappe **nicht** beantworten kann, ist, ob ein echter Jellyfin
das Geräteprofil annimmt und eine Umrechnung anbietet — das entscheidet erst
ein richtiger Server.
257 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier Dinge aus dem ersten Blick auf einen echten Jellyfin-Server.
**„Zuletzt hinzugefügt" waren nicht die Neuzugänge**, sondern eine
alphabetische Liste. Die Reihe kommt jetzt von `/Users/{id}/Items/Latest` —
dem Endpunkt, den Jellyfin dafür hat. Das behebt zwei Fehler auf einmal: die
Sortierung nach Anlegedatum kam alphabetisch zurück, und selbst wenn sie
gegriffen hätte, wäre sie falsch gewesen. Das Anlegedatum einer Serie ist der
Tag, an dem sie in die Mediathek kam; eine Serie, die seit zwei Jahren dort
steht und gestern eine neue Folge bekommen hat, wäre nie aufgetaucht. Die
Antwort ist dort ein nacktes Feld ohne `Items`-Hülle — dafür gibt es einen
Test, sonst bliebe die Startseite eines Tages stumm leer.
**Der Aufmacher war einer und blieb es.** Jetzt sind es bis zu fünf, die alle
sieben Sekunden weiterwandern und sich auch von Hand durchblättern lassen.
Der Zeitgeber hängt an der aktuellen Seite und fängt bei jedem Wechsel von
vorn an — sonst rutscht der Aufmacher genau dann weiter, wenn man sich einen
Titel ansieht. Wer Bewegung abgestellt hat, blättert selbst. Titel ohne
Hintergrundbild werden übersprungen, solange es andere gibt: eine graue
Fläche mit Text ist keine Bühne.
**Die Seitenleiste ist durchscheinend** statt undurchsichtig, und der
Schleier darunter dunkelt schwächer ab — ein voller Schleier hätte aus dem
Milchglas wieder eine Wand gemacht.
**Der Suchlauf ist aus der Mediathek verschwunden.** Er stand im
Dreipunktmenü, einen Tipp neben „Filter" und „Ansicht wechseln", und
beschäftigt den Server minutenlang. Er steht jetzt in den Einstellungen neben
der Mediathek, zu der er gehört.
Dazu eine Jellyfin-Attrappe unter Tools/: ein Server, der gerade genug kann,
dass sich die Oberfläche ansehen lässt. Damit ist diese Runde tatsächlich
angeschaut worden — Anmeldung per Code, Startseite, wandernder Aufmacher,
Seitenleiste, Einstellungen — ohne einen echten Server anzufassen.
Was weiter aussteht: die Seitenleiste oben statt seitlich, und ihre Einträge
selbst zusammenzustellen. Das ist die nächste Runde, nicht diese.
250 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Anmeldebildschirm kannte nur Plex. JellyfinKit war fertig, aber von der
App aus nicht erreichbar — „ich sehe nix von Jellyfin".
Quick Connect steht vorn: der Server nennt einen sechsstelligen Code, den man
in einem bereits angemeldeten Client bestätigt. Das Passwort erreicht Flixy
dabei nie, dieselbe Zusage wie bei Plex. Benutzername und Passwort sind der
Rückfall für Server, auf denen Quick Connect abgeschaltet ist; das Passwort
wird dann für die Dauer einer Anfrage gehalten und danach weggeworfen.
Die Adresse wird nicht erraten, sondern durchprobiert. „192.168.1.50" kann
alles Mögliche heißen, und die Reihenfolge ist der Kern von JellyfinAddress:
eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein Domänenname fast
immer. Andersherum wartet man bei jedem Server ohne Zertifikat erst auf eine
Zeitüberschreitung.
Die Sitzung hält von hier an einen Server hinter dem neutralen Vertrag, nicht
mehr einen Plex-Server. Die Ansichten haben das nicht gemerkt — genau dafür
war MediaKit da. Dabei fällt ab, was Jellyfin kann und Plex nicht: Favoriten
stehen im Aktionsmenü, sobald der Server sie kennt, Sterne nur dort, wo es
sie gibt.
Was noch nicht geht und deshalb auch nicht behauptet wird: Wiedergabe und
Live-TV sprechen weiter unmittelbar mit Plex. Bei Jellyfin verschwindet der
Live-TV-Eintrag aus der Seitenleiste, und der Player sagt, dass die
Wiedergabe der nächste Schritt ist, statt „Server nicht erreichbar" zu
melden — er ist ja erreichbar.
Nebenbei zwei Stellen, die der Übersetzer aus Plex hinterlassen hat:
`catch is CancellationError` griff seit den getippten Fehlern nie mehr, weil
eine abgebrochene Netzanfrage als Übertragungsfehler zurückkommt; und das
Ersatz-Hintergrundbild einer Folge war toter Code, weil die linke Seite des
`??` gar nicht optional war.
TokenStore wandert nach MediaKit — Tokens sind nichts Plex-Eigenes. Die
Konten sind nach Server getrennt, damit ein späteres Umschalten nicht jedes
Mal eine neue Anmeldung kostet; „Abmelden" räumt trotzdem beide ab.
248 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit
`MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für
einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre.
Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle`
wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und
`TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um
davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten.
`Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung
gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die
konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein
zweites Mal falsch — zu schreiben.
Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie
waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
Bildschirm eine andere Erklärung.
Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die
fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist
aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört
nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das
jetzt ausdrücklich, damit es niemand später „aufräumt".
Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit
Plex. Sie folgen als Nächstes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein frisch angeforderter Stream beginnt vorn, puffert und springt erst
danach an die Stelle, an der man war. Zehn Sekunden lang lief sichtbar
der Filmanfang — und der Eindruck, die Stelle sei weg und man müsse sie
wiederfinden.
Der Umstieg wird jetzt abgedeckt, solange der Player noch nicht dort ist,
und die Abdeckung nennt die Stelle ausdrücklich. Genau darum geht es:
nicht, dass es schneller wird, sondern dass niemand denkt, er habe etwas
verloren.
Aufgehoben wird sie, sobald der Player innerhalb von zwei Sekunden um die
Stelle liegt. Dazu eine Notbremse nach dreißig Sekunden — springt er nie
dorthin, bliebe sonst für immer eine schwarze Fläche über dem Bild.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Server antwortet auf eine angeforderte Umrechnung mit „direkte
Wiedergabe nicht verfügbar; Umwandlung ok" — Code 1001. Meine Prüfung
ließ nur 1000 durch, also ausschließlich DirectPlay. Damit blockierte die
App ausgerechnet den Fall, den sie selbst angefordert hatte: wer im
Player eine Stufe wählt, will umgerechnet bekommen, und die einzige
richtige Antwort darauf kam als Fehler an. Es spielte gar nichts mehr.
1000 heißt „direkt abspielbar", 1001 „direkt nicht, umrechnen schon",
2000 „Umwandlung in Ordnung" — alles Zusagen. Abgelehnt wird erst ab
3000, etwa bei fehlender Berechtigung oder überlastetem Transcoder.
Vier Tests halten die Codes fest, damit die Grenze nicht wieder zur
Auslegungssache wird.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Decider rechnet nur um, wenn die Datei die Grenze überschreitet. Für
den Normalfall ist das richtig — für eine ausdrückliche Wahl im Player
nicht: wer für eine 8-Mbit-Datei „12 Mbit" wählt, bekam DirectPlay, weil
die Datei ja darunter liegt. Gemeint ist aber „rechne es mir auf diese
Stufe um", nicht „gib mir die Datei, falls sie so vorliegt".
Die Richtlinie kann eine Umrechnung jetzt erzwingen, und die Wahl im
Player tut das.
Dazu verschluckte `select(quality:)` den eigentlichen Grund: lehnte der
Server ab und sagte im Klartext warum, wurde diese Meldung danach durch
das allgemeine „keine abspielbare Datei" ersetzt — der eine brauchbare
Hinweis war damit weg. Jetzt bleibt stehen, was der Server gesagt hat.
Und das Menü war zu viel. Vorn steht die kurze Liste — automatisch,
Original, je eine brauchbare Stufe pro Auflösung, nur Ton. Wer eine
bestimmte Bitrate erzwingen muss, klappt „Alle Stufen" auf; im Alltag
will man „eine Stufe kleiner" und nicht die Wahl zwischen zehn und zwölf
Megabit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>