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>
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>
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>
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>
**`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>
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>
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>
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>
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>
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>
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>
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>
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>
Der zweite Server. Eigenes Target neben PlexKit, nicht darin — die beiden
teilen die neutrale Schicht und sonst nichts.
Die Anmeldung geht zuerst über **Quick Connect**: die App zeigt einen
sechsstelligen Code, den man in einem bereits angemeldeten Client
bestätigt. Damit gilt hier dieselbe Zusage wie bei Plex — das Passwort
erreicht Flixy nie. Benutzername und Passwort bleiben als Rückfall, falls
der Server Quick Connect abgeschaltet hat; dann hält die App das Passwort
für die Dauer einer Anfrage und speichert es nirgends.
Zwei Stellen, an denen sich eine Übersetzung verrechnet, und die deshalb
Tests haben:
Jellyfin zählt Zeiten in Ticks zu hundert Nanosekunden. Eine Null zu viel,
und aus 44 Minuten werden siebeneinhalb Stunden.
Und es meldet die **ungesehenen** Folgen, nicht die gesehenen. Ungeprüft
übernommen stünde bei einer fast fertigen Serie „9 von 24" statt „15 von
24".
Dass die Adressen den Token tragen, steht hier von Anfang an — bei Plex
hat genau das tagelang gekostet, weil AVPlayer keine Kopfzeilen mitschickt.
Zwei Fähigkeiten sind ausdrücklich nicht da: Sterne-Bewertungen kennt
Jellyfin nicht, nur Daumen hoch und runter. Und die Adresse einer
verkleinerten Fassung gibt es erst nach einer Anfrage — sie zu erfinden
hieße, eine Adresse anzubieten, die nicht antwortet.
Die Startseite ist nachsichtig gebaut: fällt „Als Nächstes" aus, bleibt
deshalb nicht die ganze Seite leer.
Die App bietet Jellyfin noch nicht an — das ist der nächste Schritt.
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>
`MediaItem` und die Typen darunter — die Form, in der die Oberfläche
Titel sieht, unabhängig davon, welcher Server sie geliefert hat.
Die Namen sind dort geblieben, wo sie ohnehin neutral waren: `title`,
`summary`, `year`, `index`. Umbenannt wurde, was Plex-Jargon ist.
`ratingKey` heißt `id`, `viewOffset` heißt `resumeMilliseconds`, und aus
dem „Großelternteil" ist die Serie geworden, die von Anfang an gemeint
war — ein Name, den man in jeder Ansicht im Kopf übersetzen musste.
Zwei Dinge, die dabei ehrlicher werden:
`isFavourite` gibt es, aber Plex meldet immer `false`. Ein Feld, das ein
Server nicht füllt, ist besser als eine Fähigkeit, die es nur auf einem
gibt und die sich nirgends setzen ließe.
Dateigrößen sind jetzt `Int64`. Ein Remux überschreitet vier Gigabyte
mühelos.
Die Übersetzung steht in **einer** Datei und geht in **eine** Richtung.
Die Drahtmodelle bleiben, wo sie sind — sie tragen Eigenheiten, die nur
Plex hat, und die gehören nicht nach oben.
Geprüft wird gegen echte Server-Antworten, nicht gegen erfundene Werte:
genau dort stehen die Eigenheiten, an denen eine Übersetzung scheitert.
Ein Test vergleicht außerdem beide Wege zum Wiedergabe-Profil — sagten
sie Unterschiedliches, spielte ein Titel nach dem Umbau plötzlich über
einen anderen Player.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Erster Schritt zu Jellyfin, und er besteht vor allem aus Wegnehmen.
`PlaybackDecider` nahm bisher Plex-Modelle entgegen und las daraus sechs
Felder. Das band die Entscheidung an einen bestimmten Server, obwohl an
der Frage „kann iOS das öffnen?" nichts serverspezifisch ist. Jetzt nimmt
er ein `MediaProfile` — Container, Codecs, Bitrate, ob die Datei
untersucht ist.
Wie ernst die Kopplung war, zeigte der Player selbst: für die
Offline-Wiedergabe baute er sich **erfundene** Plex-Werte zusammen — eine
Media mit Kennung 0, ein Part mit dem Dateinamen als Schlüssel,
zusammengesetzte Spurlisten —, nur damit die Signatur passte. Für eine
Datei, die längst auf dem Gerät lag und mit Plex nichts mehr zu tun
hatte. Dieser Hack ist weg.
Neu ist das Target `MediaKit`: Typen und Rechnungen, die für jeden Server
gelten, ohne eine einzige Abhängigkeit. Darin `MediaProfile` und die
`Duration`-Brücke, die bisher in PlexKit stand — „weil es die unterste
Schicht ist". Das stimmte, solange es nur einen Server gab.
Damit kennen Wiedergabe und Downloads Plex nicht mehr. Zwei der
Abhängigkeiten waren ohnehin tot; die dritte trug nur diese Brücke.
Die wichtigste Testtabelle des Projekts ist mitgewandert und dabei
kürzer geworden: dort steht jetzt, worauf es ankommt, statt vollständiger
Modelle mit Kennungen und Dateipfaden, von denen niemand etwas las.
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>
Die vier erfundenen Stufen weichen der Leiter, die der Server selbst
meldet: 64, 96, 208, 320, 720, 1500, 2000, 3000, 4000, 8000, 10000,
12000, 20000 kbps mit den Auflösungen, die Plex dafür vorsieht. Eine
eigene Leiter hieße, Stufen anzubieten, die der Transcoder nicht kennt —
er rundet dann still auf die nächste, und die Anzeige log.
Angeboten wird nur, was zur Quelle passt. Eine 720p-Datei auf 1080p
hochzurechnen bringt kein Bild dazu, und die Stufen sähen alle gleich aus.
Oben im Blatt steht, was tatsächlich über die Leitung kommt — gemessene
Bitrate, Auflösung, verworfene Bilder. Der Unterschied zur gewählten
Stufe ist keine Haarspalterei: die eine sagt, was erlaubt ist, die andere,
was ankommt. Ruckelt es trotz hoher Stufe, steht die Antwort dort.
„Nur Ton" gibt es, aber ehrlich beschriftet: Plex kennt für Videos keinen
reinen Tonstrom, also wird die unterste Sprosse geholt und das Bild
ausgeblendet.
Dazu Doppeltipp links und rechts für zehn Sekunden zurück und vor. Die
Reihenfolge der Gesten ist entscheidend — das einfache Tippen verschluckt
sonst das doppelte.
Die Kachel hängt nicht mehr in vier Ecken fest, sondern klebt links oder
rechts und ist in der Höhe frei. Vier Ecken sind schnell gebaut, aber im
Weg: steht sie über einem Knopf, bleiben drei Orte, die ebenso stören.
Größer wird sie durch Zusammen- und Auseinanderziehen; der Griff ist weg,
er saß zwangsläufig auf dem Bild und stritt sich mit dem Verschieben.
Und die Begriffe: „Motor" für engine sagt kein deutscher Entwickler.
Fachwörter, die im Deutschen englisch bleiben, bleiben jetzt englisch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Fehler steckte in der Abfrage, und er ist grob:
beginsAt> = jetzt − 1 Std.
endsAt< = jetzt + 6 Std.
Beide Grenzen schließen genau das aus, was man sehen will. Ein
Zweistünder, der um halb vier begann, war um Viertel nach fünf
unsichtbar — und angezeigt wurde stattdessen die nächste Sendung, die
durch den Filter passte. Deshalb wich die Anzeige von Plex' eigener ab.
Jetzt wird nur der **Anfang** eingegrenzt, mit sechs Stunden Vorlauf für
lange Sendungen, und keine Grenze auf das Ende. Zurück kommt, was den
Zeitraum berührt, statt was vollständig hineinpasst.
Gefiltert wird danach nur das Programm, nicht die Senderliste: ein Sender
ohne frische Daten soll „kein Programm" sagen, statt zu verschwinden.
Dazu zwei Wünsche aus dem Feldtest:
Die Kachel lässt sich in der Größe ziehen. Der Griff sitzt in der Ecke,
die zur Bildschirmmitte zeigt — am äußeren Rand läge er halb außerhalb
des Daumens. Gespeichert wird die begrenzte Breite, nicht die gezogene,
sonst bliebe eine einmal zu weit gezogene Kachel für immer zu groß.
Und eine Senderauswahl. Über zwanzig Sender zu zeichnen und ihr Programm
zu holen kostet Zeit, und die meisten sieht man nie an. Eine leere
Auswahl heißt weiterhin „alle" — sonst stünde man beim ersten Öffnen vor
einer leeren Liste.
Die Zeiten im Raster-Abzug liegen jetzt in der Zukunft. Mit den echten
Zeitstempeln von damals wäre er ab dem Tag nach der Aufnahme leer
gewesen, sobald auf Frische gefiltert wird.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wer im Fernsehen etwas sieht und nebenher nachsehen will, was sonst
läuft, musste bisher abbrechen. Plex' eigene Handy-Apps können das auch
nicht — kein Grund, es hier genauso zu machen.
Der eigentliche Umbau ist nicht die Kachel, sondern der Besitz. Die
Wiedergabe hing an der Sichtbarkeit der Ansicht, und zwar dreifach: das
Modell war `@State` im Player, `onDisappear` hat den Motor abgeräumt, und
die Bildebene entstand in `makeUIView` neu. Zurückgehen hieß deshalb:
Motor tot, Transcoder aus, beim Live-TV der Tuner freigegeben.
Jetzt gehört das Modell der Wurzelansicht, die immer da ist. Verkleinern
und Vergrößern wechseln nur noch die Darstellung; beendet wird die
Wiedergabe an genau einer Stelle, und zwar weil jemand es will.
Die Bildebene hält das Modell selbst — dasselbe Muster, das VLC seit
jeher verwendet. Dieselbe `UIView` wandert zwischen Kachel und Vollbild,
statt neu zu entstehen; sonst setzte das Bild bei jedem Wechsel neu an.
Bild-in-Bild wandert mit und bekommt endlich einen Delegaten: bisher war
es eine Einbahnstraße, aus der man nicht in die App zurückkam.
Die Kachel schwebt und lässt sich in jede Ecke ziehen, statt als Leiste
unten zu kleben — eine Leiste nähme jeder Liste Platz weg und müsste in
jedem Bildschirm bedacht werden. Die Eckenberechnung steht in
MiniPlayerPlacement und ist geprüft: in einer View wäre eine Kachel, die
halb aus dem Bild ragt, erst auf dem Gerät aufgefallen.
Der Pfeil oben verkleinert, ein neuer Knopf daneben beendet wirklich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Unterschied zwischen 400 und 200 war ein einziger Parameter:
…&protocol=hls&X-Plex-Token=… → 400
…&protocol=hls&X-Plex-Client-Profile-Name=Chrome&… → 200
Flixy hat immer nur `X-Plex-Client-Profile-Extra` geschickt. Das
**erweitert** ein Profil, es ersetzt keins — ohne Basis gibt es nichts zu
erweitern, und der Server kann keine Entscheidung bilden. Seine Antwort
darauf ist eine nackte 400 ohne jeden Hinweis.
Das betrifft alles, was durch den Transcoder geht: die Qualitätswahl im
Player, komprimierte Downloads und Live-TV. Bei Filmen fiel es nicht auf,
weil dort DirectPlay greift.
„Chrome" als Basis, weil dieses Profil auf jedem Server vorhanden ist und
genau das kann, was AVPlayer kann: HLS mit H.264 und AAC. Die Feinheiten
für iOS stehen weiterhin im Zusatz.
Gefunden wurde es, indem die Anfragen Angabe für Angabe aufgebaut wurden,
bis eine davon umschlug — nicht durch Nachdenken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die vollständige Sitzung zeigt es nebeneinander:
Tune-Antwort: …/sessions/df16c509…/B5080949-E767-…/index.m3u8
Sitzung selbst: …/sessions/df16c509…/2F3939E2-6196-…/index.m3u8
Dieselbe Adresse, aber mit verschiedenen Kennungen darin: die
Tune-Antwort setzt die Sitzungskennung ein, die Sitzung selbst die
Gerätekennung. Nur die zweite gibt es wirklich. Ich habe die erste
genommen — daher jedes Mal 404.
Gesehen habe ich das erst, als die Antwort nicht mehr bei 700 Zeichen
abgeschnitten war. Genau dort fing `Part` an.
Dieselbe Antwort trägt auch `TranscodeSession` — der Strom läuft bereits.
Es gab nie etwas umzurechnen, und der Transcoder hatte recht, jede dieser
Anfragen abzulehnen.
Die Abspieladresse kommt jetzt ausschließlich aus der Sitzung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die gebaute Adresse anzusehen hat es in einem Blick beantwortet:
path=/livetv/sessions/0043d829-… ← meine
path=%2Flivetv%2Fsessions%2F0043d829-… ← Plex
Plex' Client schickt alle Parameter durch `qs.stringify`, und das kodiert
alles. `URLComponents` lässt `/`, `:`, `&`, `=`, `+` und Klammern in
Werten stehen. Beim Server kam damit ein Pfad mit echten Schrägstrichen
an — und ein Geräteprofil, das an seinen `&` in zwei Dutzend eigene
Angaben zerfiel. Beides beantwortet er mit einer nackten 400.
Eine Regel für alle Werte, keine Ausnahmen. Damit löst sich nebenbei die
Kodierfrage, die mich mehrere Anläufe gekostet hat: eine Kennung, die
schon `%3A` enthält, wird dabei zu `%253A` — und ist genau einmal
kodiert. Die Sonderbehandlung für `path` und das Profil entfällt, und mit
ihr die Schalter, mit denen ich zwischen falschen Formen hin- und
hergeschaltet habe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`URLComponents` kodiert `&` und `=` innerhalb eines Abfragewerts nicht.
Das Geräteprofil steckt voll davon:
X-Plex-Client-Profile-Extra=add-transcode-target(type=videoProfile&
context=streaming&protocol=hls&container=mpegts&…)
Der Server hat das nicht als ein Profil gelesen, sondern als zwei Dutzend
eigene Angaben — darunter ein zweites `protocol=`. Eine widersprüchliche
Anfrage, und die Antwort darauf ist eine nackte 400 ohne Hinweis.
Der Fehler steckte in jeder Transcoder-Adresse, seit es sie gibt. Bei
Dateien fiel er nicht auf, weil dort meist DirectPlay greift und der
Transcoder gar nicht gefragt wird.
Kodiert wird der **ursprüngliche** Wert, nicht der bereits von
`URLComponents` behandelte: den ein zweites Mal zu kodieren macht aus
`%3D` ein `%253D` — derselbe Fehler, nur eine Ebene tiefer. Der Test
prüft beides: dass das Profil ein einziger Parameter bleibt und dass
`protocol` genau einmal vorkommt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nachgelesen im Quelltext des Plex-Clients, Funktion `video`:
path: server.resolveUrl(item.get("key")), // /livetv/sessions/1268be73-…
hasMDE: 1, mediaIndex: …, partIndex: …,
protocol: "hls", …
Zwei Dinge fehlten. Umgerechnet wird die **Sitzung, die der Server beim
Belegen angelegt hat** — nicht die Abspielliste darin, nicht die Sendung
beim Programmführer, und schon gar nicht die Kennung, die ich selbst
erzeuge. Genau die hatte ich mehrfach eingesetzt und mich über 404
gewundert.
Und `hasMDE=1` — „ich verstehe die Entscheidungsantwort". Plex' Client
schickt das bei jeder Wiedergabe mit. Es fehlte bei allen bisherigen
Versuchen, auch bei denen, die sonst richtig waren.
Der Schlüssel wird suchend gelesen und dabei von der Abspielliste
unterschieden: beide beginnen mit /livetv/sessions/, aber nur eine endet
auf .m3u8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Belegen des Tuners klappt, und heraus kommt:
/livetv/sessions/1b5bb998…/DBACCA6D…/index.m3u8?offset=-1.000000
Eine fertige HLS-Abspielliste. Ich habe sie an den Transcoder gegeben —
also darum gebeten, etwas umzurechnen, das schon fertig ist. Daher die
nackte 400. Sie wird jetzt unmittelbar abgerufen, mit Token und
Gerätekennung in der Adresse, weil der Systemplayer keine Kopfzeilen
mitschickt. Die eigene Abfrage der Sitzung bleibt dabei erhalten.
Dazu das Gegenstück, ebenfalls aus dem Quelltext des Plex-Clients:
`DELETE /media/subscriptions/{id}` gibt den Tuner wieder frei. Er ist ein
Stück Hardware — vier Stück gibt es, und ein nicht freigegebener fehlt
beim nächsten Einschalten. Das passiert jetzt beim Schließen des Players
und auch dann, wenn das Belegen zwar klappt, aber nichts Abspielbares
dabei herauskommt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aus dem Quelltext von Plex' eigenem Web-Client:
const o = `/livetv/dvrs/${t}/channels/${a}/tune`,
l = {method:"POST", headers:{"X-Plex-Session-Identifier": …}}
Genau diesen Pfad hatte ich seit mehreren Runden richtig. Jedes Mal per
GET abgefragt, jedes Mal 404 — und ein 404 sieht aus wie „gibt es nicht"
statt wie „nicht so". Also habe ich den Pfad verworfen und weitergesucht,
wieder und wieder, an einer Stelle, die von Anfang an stimmte.
Die Antwort auf das POST trägt den Vorgang mitsamt Metadaten-Eintrag, und
dort steht die abspielbare Teil-Adresse — nicht im Raster und nicht bei
der Sendung. Sie wird suchend gelesen statt an fester Stelle: der Vorgang
liegt je nach Serverfassung unterschiedlich tief, und ein falscher Weg
hinein sähe wieder aus wie „nichts da".
Die Kennung wird wie in `encodeURIComponent` kodiert — insbesondere
bleibt der Bindestrich stehen. `.alphanumerics` hätte aus
`6228f7f2-5fc705fa` ein `6228f7f2%2D5fc705fa` gemacht, und der Server
fände den Sender nicht.
Dass die Antwort in einer öffentlichen Quelle stand, während ich zwei
Tage lang Adressen erfunden habe, ist die eigentliche Lehre dieser Runde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aus der Antwort von /media/grabbers/devices/1:
{"channelKey":"6228f7f2364e02fa5ec8e51c-5fc706063f45c6002eab58a3",
"deviceIdentifier":"300",
"lineupIdentifier":"002"}
Der Programmführer kennt den Sender als `6228f7f2…-5fc706…`, der Tuner
als `300`. Die Zuordnung im Empfänger übersetzt zwischen beidem — und
genau diese Übersetzung habe ich nie benutzt. Jede Anfrage an den
Empfänger ging in der Sprache des Programmführers hinaus, und darauf gibt
es nur eine Antwort: 404.
Die Nummer am Empfänger steht jetzt an erster Stelle im Suchraum und im
Bericht. Der Empfänger ist ein HDHomeRun FLEX QUATRO mit vier Tunern; er
kennt Sender ausschließlich unter dieser Nummer.
Nebenbei aus derselben Ausgabe: /livetv/sessions antwortet mit size 0.
Die Route gibt es also — sie ist eine Liste. /livetv/sessions/<uuid> gibt
es nur, wenn die Sitzung vorher angelegt wurde, und deshalb war jeder
Abruf dort ein 404 ohne Aussagekraft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Bericht nennt zwei Zeilen, die alles erklären:
Media.protocol = livetv
ratingKey = plex%3A%2F%2Fepisode%2F6a9a1255715fa1c9375df709
Plex führt die Kennung selbst schon kodiert — auch im JSON, wo es dafür
keinen Grund gäbe. Als Abfragewert geschickt dekodiert der Server sie
einmal, bevor er sie als Pfad liest, und bekommt `plex://episode/…`. Das
ist kein gültiger Schlüssel, und die Antwort darauf war die nackte
„400 Bad Request"-Seite.
Für eine Datei in der Mediathek ist das kein Thema: `/library/metadata/123`
enthält nichts zu kodieren. Deshalb ist beides nötig, und welche Form
gilt, hängt daran, woher die Adresse stammt.
Ich habe in diesem Punkt inzwischen jede Variante einmal für die richtige
gehalten — entpackt, einfach, doppelt. Ein Test hält jetzt alle drei
Formen fest, damit die Frage nicht ein viertes Mal aufkommt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die gebaute Adresse ist sauber — nachgerechnet und ausgedruckt. Also
lehnt Plex selbst ab, und zwar mit Recht: eine Sendung beim
Cloud-Programmführer hat keine Datei, die sich umrechnen ließe. Die
nackte 400 war die Antwort auf eine unsinnige Bitte.
Deshalb wird die Sendung jetzt zuerst nachgeschlagen. Das Raster nennt
keine abspielbare Adresse; die Einzelabfrage beim Anbieter nennt
womöglich eine. Was dabei zurückkam, steht in jedem Fall in der Spur —
auch wenn nichts Abspielbares dabei war.
Dafür musste die Anfrage einen bereits kodierten Pfad durchreichen
können: `appending(path:)` kodiert das Prozent ein zweites Mal, und aus
`plex%3A%2F%2F` wird `plex%253A%252F%252F`.
Und der angebotene Endpunkt-Zettel des Programmanbieters ist eindeutig:
content, metadata, grid, timeline — nichts zum Einschalten. Der Empfänger
muss es also tun, und den kannte `tune` bisher gar nicht. Seine Adressen
stehen jetzt mit im Suchraum.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die nackte „400 Bad Request"-Seite war keine Plex-Meldung, sondern der
Webserver: er hat die Anfrage selbst nicht verstanden. Grund war mein
eigener Fix von vorhin — ich habe `%3A%2F%2F` entpackt, und damit stand
`plex://episode/…` im Klartext in der Anfrage.
Beide naheliegenden Wege liegen daneben, und ich habe beide durchgemacht:
unverändert durchgereicht kodiert URLComponents ein zweites Mal, aus %3A
wird %253A. Vorher entpackt bricht sich der Server die Adresse. Richtig
ist genau einmal — dafür wird `path` von Hand eingesetzt, nachdem alles
andere kodiert ist.
Dazu aus der Spur gelernt: jede Form von /livetv/sessions/… antwortet auf
diesem Server mit 404. Den Tuner belegt er offenbar selbst, sobald die
Sendung umgerechnet werden soll. Deshalb steht die Adresse der Sendung
jetzt an erster Stelle, und die Sitzungen kommen nur noch dahinter.
Als dritte Form ein Versuch ohne das Geräteprofil: dessen Angaben stecken
voller `&` und `=`, die in einem Abfragewert mehrdeutig sind. Trägt es
ohne, liegt es daran, und das steht dann in der Spur. Ebenso, was der
Programmanbieter überhaupt an Endpunkten angibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wieder HTTP 400, und wieder ohne Begründung. Der Grund, dass das dreimal
passieren konnte: eine gebaute Adresse galt schon als Erfolg. Ob der
Server sie überhaupt annimmt, stellte sich erst im Player heraus — und
dort steht nur „HTTP 400 (unhandled)".
Jetzt ruft die App die Adresse einmal selbst ab, ohne Plex-Kopfzeilen,
genau so wie AVPlayer es tut. Nur was mit 200 antwortet, geht an den
Player. Was nicht, steht mit dem Wortlaut des Servers in der Spur.
Weil weder der Schlüssel für die Sitzung noch die anschließend
umzurechnende Adresse für diesen Teil der Plex-Schnittstelle beschrieben
sind, werden die Kombinationen der Reihe nach durchprobiert — Adresse der
Sitzung, Adresse aus der Antwort, Adresse des Teils darin, je mit
mediaIndex -1 und 0, höchstens acht Versuche. Antwortet keiner, wird der
Transcoder wieder angehalten, statt für nichts weiterzurechnen.
Die Spur steht in einem Blatt statt in einem Dialog: die Antwort des
Servers ist mehrzeilig, und ein Dialog schneidet sie ab — dann stünde
dort wieder nur, dass es nicht ging.
Dazu ein Nebeneffekt, der beim zweiten Sender zugeschlagen hätte: der
Player kannte die Sitzungskennung des Live-Stroms nicht und hat den Tuner
beim Schließen nicht freigegeben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HTTP 400 vom Transcoder. Die Kennung dieses Geräts reist bei jeder
Anfrage als Kopfzeile mit — aber die Adresse des Stroms ruft nicht die
App ab, sondern AVPlayer, und der schickt keine Kopfzeilen mit. Bei
Dateien fällt das nicht auf; bei Live-TV schon: der Server legt die
Sitzung unter der Gerätekennung an und kann die Abholung ohne sie keiner
Sitzung zuordnen.
Also trägt die Transcoder-Adresse die Kennung jetzt selbst. Das gilt für
alle Ströme, nicht nur für Live — dort fiel es nur zuerst auf.
Dazu: umgerechnet wird die Adresse der Sitzung, nicht die des Teils
darin. Der Transcoder sucht sich den Teil selbst, deshalb steht dort -1.
Und der Entscheider unterscheidet jetzt zwischen „nein" und „keine
Antwort". Ein Schweigen als Ablehnung zu lesen hieße, eine
funktionierende Sendung zu blocken, bloß weil er Live-Adressen womöglich
gar nicht kennt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der übersprungene Schritt. Ich habe die Sendung direkt an den Transcoder
gegeben — ohne dass jemand den Tuner belegt hätte. Der Server fing an,
merkte, dass er nichts zu senden hat, und brach ab: „die Wiedergabe wurde
unterbrochen", ohne weitere Angabe.
Jetzt zwei Schritte. Erst eine Live-Sitzung eröffnen, dann die Adresse
dieser Sitzung umrechnen lassen — nicht die des Senders und nicht die der
Sendung. mediaIndex und partIndex sind dabei -1: ein laufender Sender hat
keine Fassungen zur Auswahl.
Unter welchem Schlüssel sich die Sitzung eröffnen lässt, unterscheidet
sich je nach Anbieter, deshalb werden die in Frage kommenden der Reihe
nach probiert — der Raster-Schlüssel zuerst, weil er den Sender genauso
bezeichnet wie die Liste, aus der er stammt.
Dazu zwei Stellen, an denen Gründe bisher verschwanden:
Vor dem Abspielen wird der Server gefragt, ob er den Strom überhaupt
zulässt. Er antwortet im Klartext; der Player meldet nur, dass es nicht
ging.
Und wenn der Motor scheitert, steht sein Grund ab jetzt auf dem
Bildschirm statt nur im Protokoll. „Die Wiedergabe wurde unterbrochen"
allein sagt weder, ob der Server ablehnt, noch ob der Motor das Format
nicht mag — und genau diese Unterscheidung hat diese Runde gekostet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fehler, alle im eigenen Code.
Die Sendezeiten stehen beim Programmanbieter im Medien-Block, nicht am
Eintrag darüber. Ich habe sie oben gesucht, nicht gefunden und jede
Sendung verworfen — übrig blieb „Kein Programm verfügbar" auf jedem
Sender, und ohne Sendungen fiel auch die Senderliste auf die Tunerliste
zurück.
Die Senderspalte im Raster fehlte ganz. Die Zeitachse ist mehrere tausend
Punkte breit, und der HStack hat den Nachbarn dafür auf null gedrückt.
Jetzt hat die Spalte Vorrang, und die Achse liegt als Auflage über einer
durchsichtigen Fläche — eine Auflage zählt bei der Größenrechnung nicht
mit. Der Abstand der Achsenbeschriftung gehört in die Spaltenbreite
hinein statt daneben, sonst steht die ganze Achse acht Punkte neben dem
Programm darunter.
Und das Einschalten: die Adresse wurde berechnet und verworfen, dann
bekam der Player eine Senderkennung, zu der er einen Film in der
Mediathek suchte. Den gibt es nicht. Jetzt reicht die Anfrage die fertige
Adresse durch, der Player spielt sie ohne Umweg, und meldet weder
Fortschritt noch sucht er Metadaten. Scheitert es doch, steht ab jetzt
eine Meldung da statt gar nichts.
Geprüft wurde das Raster diesmal mit den Augen: ein Gerüst im Simulator
zeichnet es mit erfundenen Sendern. Dafür bekommt es die Bildadresse als
Abschluss übergeben, statt sie aus der Sitzung zu lesen.
Dazu: Downloads zeigen jetzt an, wenn sie auf WLAN warten. Die
Hintergrund-Sitzung wartet in dem Fall von sich aus — kein Fehler, nur
Stillstand, und das sah bisher aus wie ein hängender Download.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Abzug vom Server zeigt beides schwarz auf weiß.
Erstens: /media/grabbers/devices/1/channels antwortet mit 278 Sendern —
unter dem Sammelnamen `DeviceChannel`. Die Hülle kannte nur `Channel` und
hat die vollständige Liste als leer verworfen.
Zweitens: das Programmraster antwortet, aber nicht auf `type=1`. Die 1
steht bei Plex für Film, und im Programm laufen auch Folgen, Sport und
Nachrichten — der Filter hat das Raster geleert. Ohne ihn kommen die
Sendungen samt Sendername, Nummer, Logo und Kennung.
Das Raster hat jetzt Vorrang vor der Tunerliste, auch wenn beide
antworten: nur dort gehören Sender und Programm zusammen, es enthält
ohnehin nur die eingeschalteten Sender, und die Tunerliste führt alle 278.
Einschalten läuft über die Adresse der laufenden Sendung beim
Programmanbieter — Plex behandelt eine laufende Sendung wie einen Film.
Die Adresse wird einmal entpackt, bevor sie in die Anfrage geht: sonst
kodiert URLComponents ein zweites Mal, und aus %3A wird %253A.
Die Tests stehen jetzt auf den echten Antworten aus dem Abzug, nicht auf
erfundenen: DeviceChannel mit `name` und `key`, Rastereinträge mit
channelVcn, channelID und einer Folge neben einem Film.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier geratene Adressen, vier 404. Statt ein fünftes Mal zu raten, klopft
die App jetzt in einem Durchgang und gleichzeitig ein Dutzend Adressen ab:
was /media/providers an Endpunkten angibt, welche Unterverzeichnisse
Programmanbieter und Empfänger in ihrer eigenen Antwort führen, die
bekannten Formen anderer Serverfassungen, die Senderliste des Tuners
selbst — und zuletzt das Programmraster.
Dabei zwei Fehlerquellen mitbehoben, die jede einzeln wie „keine Sender"
ausgesehen hätten: der Sendername steht je nach Tunertyp in title, name,
channelTitle oder callSign, und eine Antwort mit dreißig namenlosen
Einträgen sieht aus wie eine Senderliste, ist aber keine.
Bleibt es leer, steht jetzt jede abgeklopfte Adresse mit ihrer Antwort in
der Ansicht. Das beantwortet „liegt es am Server oder an der App" ohne
eine weitere Runde Rückfragen.
Dazu drei Sachen bei Serien:
- Groß steht, worum es geht — bei einer Folge ihr Titel, bei einer
Staffel der Name der Serie. „Staffel 10" als Überschrift sagte nicht,
welche Serie gemeint ist.
- Die Einordnung steht darunter: Serienname bei Folgen, Staffelnummer bei
Staffeln.
- Von der Folge führt ein Weg zur Staffel und zur Serie. Wer über
„Weiterschauen" bei einer Folge landete, saß dort bisher fest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei geratene Adressen für die Senderliste antworteten auf einem echten
Server alle mit 404. Statt ein viertes Mal zu raten, liest die App jetzt
zwei Dinge: was /media/providers an Endpunkten angibt, und das
Programmraster selbst — die Weboberfläche von Plex zeichnet ihren Guide
daraus, und jeder Rastereintrag trägt Sendername, Nummer, Logo und
Einschaltweg mit sich.
Damit kommen Sender und Programm aus einer Abfrage, und ihre Kennungen
passen zueinander. Getrennt geholt taten sie das nicht: das Programm
landete in keiner Zeile.
Dazu die Ansicht, für die das alles da ist — ein Programmraster mit
stehender Senderspalte, mitlaufender Zeitachse und einer Jetzt-Linie.
Antippen öffnet die Sendung; läuft sie gerade, schaltet ein Knopf um.
Die Geometrie steht in GuideLayout und ist geprüft: in einer View wäre
sie unsichtbar falsch. Die Trennlinien gehören in die Zeile statt
zwischen die Zeilen — Divider ist auf 3x-Schirmen keinen ganzen Punkt
hoch, und der Bruchteil summiert sich, bis die angetippte Zeile zum
Nachbarsender gehört.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der zweite Abzug zeigt den Pfad, der 404 ergab:
/tv.plex.providers.epg.cloud:2/lineups/
lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c
#Swisscom%20Blue%20TV%20%28Deutschschweiz%29%20%28521%20channels%29
/channels
`Lineup.id` heißt zwar Kennung, ist aber eine vollständige Adresse: mit
Schema, Doppelschrägstrich, Prozentzeichen und einem Anhang hinter der
Raute. Die habe ich roh in den Pfad gespleißt.
Gebraucht wird der nackte Teil in der Mitte —
`6228f7f2364e02fa5ec8e51c`. `PlexLineup.pathIdentifier` packt ihn aus,
vier Tests halten die Fälle fest.
Da die Pfadform zwischen Serverfassungen schwankt und dieser Teil der
API keine Beschreibung hat, werden drei Adressen der Reihe nach
probiert. Welche gegriffen hat, steht in der Diagnose — damit aus dem
Durchprobieren beim nächsten Mal Wissen wird. Der Strukturabzug deckt
jetzt alle drei ab.
173 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Strukturabzug vom Server hat es entschieden. ChannelMapping enthält
genau vier Felder — channelKey, deviceIdentifier, enabled,
lineupIdentifier — und keinen einzigen Namen. Es ist keine Senderliste,
sondern ein Verweis darauf: welche 28 von 521 Sendern der Swisscom-Liste
eingeschaltet sind.
Zweimal habe ich die Zuordnung für die Senderliste gehalten. Beim ersten
Mal blieb sie leer, beim zweiten hätten alle 28 Sender „—" geheißen und
dieselbe Kennung getragen — SwiftUI hätte bei doppelten Kennungen
ohnehin nur einen gezeigt.
Jetzt werden beide Quellen zusammengeführt: die Namen, Nummern und Logos
kommen von der Senderliste beim Programmanbieter, die Auswahl aus der
Zuordnung. Die Kennung des Anbieters steht als `epgIdentifier` direkt am
Empfänger — verlässlicher, als sie aus der Anbieterliste zu suchen.
Der Abgleich ist nachsichtig: je nach Anbieter entspricht der channelKey
der Kennung, dem Pfad oder der Sendernummer. Ein strenger Vergleich fände
in zwei von drei Fällen nichts, und „keiner passte zusammen" sieht aus
wie „keine Sender". Genau das unterscheidet die Meldung jetzt auch.
Der Strukturabzug in der Diagnose deckt jetzt beide Endpunkte ab — nach
einem Fehlschlag bleibt keine weitere Runde Raten übrig.
166 Tests, neun davon zu Zuordnung und Abgleich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Senderliste
Plex liefert die Sender verschachtelt in der Antwort auf /livetv/dvrs
mit — jeder Empfänger bringt seine eigenen unter `Device` → `Channel`.
Mein getrennter Endpunkt /livetv/dvrs/{id}/channels war eine Vermutung
und lieferte nichts. Bei einem Server mit über dreißig eingerichteten
Sendern stand deshalb „Keine Sender".
Die verschachtelte Liste hat jetzt Vorrang, der Endpunkt bleibt als
Rückfalllinie. `ChannelMapping` wird als ältere Schreibweise ebenfalls
gelesen, und der Empfänger-Schlüssel kommt je nach Serverfassung als
Zahl oder als Zeichenkette.
Bleibt die Liste leer, sagt die Ansicht jetzt welcher Weg was ergeben
hat. In einem Bereich ohne offizielle Beschreibung ist das die Angabe,
mit der sich der nächste Fehler finden lässt.
Schriftzüge
Plex liefert bei vielen Filmen ein `clearLogo` — den Schriftzug in
seiner eigenen Type, freigestellt. Der tritt jetzt im Aufmacher an die
Stelle des gesetzten Titels.
Dabei ein Fallstrick: /photo/:/transcode liefert JPEG, und JPEG kennt
keine Transparenz. Über den Bild-Transcoder geladen wäre aus dem
freigestellten Logo ein schwarzer Kasten geworden. Es wird deshalb
unverändert geladen. Fehlt der Schriftzug oder scheitert das Laden,
bleibt es beim Titel.
161 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picker-Werte leuchteten weiter in der Standardfarbe, während Schalter
und Symbole daneben schon die gewählte trugen. Zwei Ursachen, beide
behoben — welche davon im Einzelfall zog, ließ sich nicht nachstellen,
weil der Bildschirm hinter der Anmeldung liegt.
Erstens stand `.tint` im Scene-Body. Scene-Bodies werten Änderungen an
beobachteten Objekten nicht zuverlässig neu aus; nach einem Farbwechsel
blieb ein Teil der Oberfläche auf dem alten Wert stehen. Jetzt setzt
RootView Tönung und Akzent — eine View wertet zuverlässig neu aus.
Zweitens nahm der Vorgabewert der Umgebung blind die Standardfarbe.
Listen und Formulare zeichnen ihre Zeilen teilweise in einem eigenen
Darstellungskontext, in dem eigene Umgebungswerte verloren gehen; dort
erschien dann Mint statt der gewählten Farbe. Der Vorgabewert liest
jetzt die gespeicherte Auswahl.
Damit das nicht wieder auseinanderläuft, liegt die Speicherung an einer
Stelle: AccentOption.stored/store. Vorher hatte FlixyTheme eigene
Schlüssel — zwei Kopien derselben Namen driften auseinander, und zwar
unbemerkt.
Dazu setzen die Formulare die Tönung ausdrücklich, statt sich auf
Vererbung zu verlassen.
154 Tests, fünf davon zur Speicherung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alle Reihen zeigten Querformat-Kacheln. Für „Weiterschauen" ist das
richtig — man erkennt die Szene wieder, an der man aufgehört hat. Beim
Aussuchen sucht man dagegen nach Plakaten, und die sind hochkant.
Die Unterscheidung hängt an der Hub-Kennung, nicht am Titel: der kommt
in der Sprache des Servers, und die Startseite sähe sonst auf Englisch
anders aus als auf Deutsch, ohne dass jemand auf die Ursache käme.
`ondeck` zählt mit — ältere Serverfassungen nennen es so.
Bei Folgen in Hochkant-Reihen wird das Serienplakat genommen, nicht das
Szenenbild: ein 16:9-Bild hochkant beschnitten zeigt einen zufälligen
Ausschnitt und ist zum Wiedererkennen wertlos.
Plakate sind schmaler als Szenenbilder, damit bei gleicher Höhe mehr
nebeneinander passen — mehr Auswahl auf einen Blick ist der Zweck.
Vier Tests, darunter einer, der festhält, dass der übersetzte Titel die
Form nicht bestimmen darf.
149 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Player hing sich auf
Die Qualitätsauswahl war ein Menu. Schloss man es durch einen Tipp ins
Bild, blieb eine unsichtbare Ebene zurück, die alle weiteren Tipps
schluckte — die Bedienelemente kamen nicht mehr, pausieren ging nicht
mehr. Jetzt ein confirmationDialog, der sich selbst wegräumt. Nach dem
Schließen werden die Bedienelemente eingeblendet und die Ausblendfrist
neu gestartet.
Downloads galten als fertig, waren aber leer
Zwei Fehler übereinander. Erstens gilt ein URLSessionDownloadTask auch
bei 404 oder 500 als erfolgreich und legt die Fehlerseite als Datei ab.
Zweitens überschrieb ich danach die erwartete Größe mit der empfangenen
— womit die Anzeige auf hundert Prozent stand und der Beweis weg war.
873 MB waren in einer Sekunde „fertig", und beim Abspielen kam nur
„Wiedergabe wurde unterbrochen".
Jetzt wird der Statuscode geprüft, die erwartete Größe bleibt stehen,
und eine unplausible Datei wird verworfen statt verbucht: unter 100 KB
ist keine Videodatei, unter der Hälfte der angekündigten Größe ist eine
abgebrochene Übertragung. Sechs Tests dazu.
Stufenliste ergab keinen Sinn
Für einen 720p-Inhalt standen Original, Full HD und HD mit derselben
geschätzten Größe untereinander — das sah aus, als wäre die Schätzung
kaputt. Sie war es nicht, die Stufen waren es: hochrechnen bringt nur
eine größere Datei. Angeboten wird jetzt nur noch, was unter der
Auflösung der Datei liegt, und beim Original steht dabei, was es ist:
„Original · 720p · 873 MB".
Kopfleiste
Kein Hintergrund mehr statt eines weichgezeichneten — die Knöpfe
bringen ihre eigene Glasfläche mit. Der Titel ist weg: der Name der App
gehört auf den Homescreen, nicht über jeden Bildschirm.
145 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iPad meldete sich nie an
Die Ursache ist unangenehm speziell: Wer im Browser schon bei Plex
angemeldet ist, wird von app.plex.tv/auth sofort auf watch.plex.tv
weitergeschickt — OHNE dass der Code verknüpft wird. Man sieht sich
angemeldet, die App wartet vergeblich, und es gibt keinerlei Hinweis.
Jetzt wird plex.tv/link mit eingetragenem Code geöffnet. Diese Seite
tut nur eine Sache: den Code bestätigen. Die Gerätekennung steckt
ohnehin schon im PIN.
Dazu ein Ausweg: bleibt es nach fünfzehn Sekunden ohne Token, erscheint
der Code im Anmeldebildschirm zum Abtippen. Schließt man das Fenster
ohne Erfolg, bleibt er stehen statt abzumelden — er gilt weiter.
Download fragte nicht nach der Qualität
Jetzt Original, Full HD, HD oder Sparsam, jeweils mit geschätzter
Größe. Die Größe ist das eigentliche Entscheidungskriterium — „Full HD"
allein sagt nicht, ob es aufs Telefon passt.
Die komprimierten Stufen laufen über den Universal-Transcoder und sind
weiterhin NICHT am Server geprüft; steht so im Quelltext.
Schwarzes Bild ohne Anzeige
Der Player kannte nur „spielt" und „spielt nicht". Wartete er auf
Daten, stand man vor einem Standbild ohne Hinweis. Es gibt jetzt einen
eigenen Pufferzustand mit Anzeige.
Aufmacher war beschnitten
Er stand auf 3:4, Plex liefert als `art` aber 16:9 — mit scaledToFill
ging links und rechts je ein Viertel verloren, ohne dass man es dem
Bild ansah. Die Seitenverhältnisse liegen jetzt an einer Stelle in
FlixyAspectRatio.
Schwarze Leiste über dem Aufmacher
.ultraThinMaterial statt undurchsichtigem Grund: das Bild läuft
unscharf hinter der Leiste weiter, statt an einer harten Kante zu
enden. Weichgezeichnet, nicht abgedunkelt.
139 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>