10 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 68abd71074 Ein Bild, wo bisher nur eine Zahl stand
Wer zur Zeitleiste greift, sieht jetzt das Standbild der Stelle — im Web
beim Überfahren, in der App beim Ziehen.

**Erzeugt wird beim Abspielen, nicht auf Vorrat.** Ein Lauf über 453
Filme und 4213 Folgen wäre ein Abend ffmpeg und mehrere Gigabyte für
Titel, die niemand anfasst. Startet jemand einen Titel, stellt sich die
Datei hinten an; bis zum ersten Griff zur Leiste ist der Lauf durch.
Gemessen an einer erzeugten Datei: 191 ms für 250 Sekunden Film, ein
Zweistünder liegt damit bei etwa fünf Sekunden.

**Ein ffmpeg-Aufruf, nicht siebenhundert.** `fps=1/10,scale=320:-2,
tile=10x10` macht aus einem Durchlauf fertige Blätter zu hundert
Kacheln. Bezahlbar ist das durch `-skip_frame nokey` — dieselbe
Abkürzung wie beim Abtasten der Abspannhelligkeit: nur Keyframes
dekodieren. Dass die Vorschau dadurch um zwei Sekunden danebenliegen
kann, sieht niemand.

**Der Spieler lädt ein Blatt und verschiebt danach nur den Ausschnitt.**
Beim Fahren über die Leiste wird kein einziger Abruf mehr fällig. Die
Rechnung Sekunde → Kachel steht an drei Stellen — Server, Web, App —
und deshalb auch dreimal dieselbe; weicht eine ab, hält man die Vorschau
für ein unscharfes Bild statt für einen Fehler. Neun Prüfungen decken
die Ränder ab: Sekunde null, letzte Kachel eines Blattes, erste des
nächsten, und hinter dem Ende.

**Die Kachelmasse werden gemessen, nicht gerechnet.** `scale=320:-2`
hält das Seitenverhältnis, ein Breitbildfilm ergibt also flachere
Kacheln. Geraten wäre jede Kachel um ein paar Bildpunkte verschoben.

Aufgeräumt wird mit dem Transcode-Cache: der Namensanfang trägt Kennung
und Änderungszeit der Mediendatei, verwaiste Blätter sind daran ohne
Tabelle erkennbar. Der Ordner taucht unter „Speicher" auf.

**Und die Pfeiltasten spulen wieder.** Hatte die Zeitleiste den Fokus,
überliess der Spieler die Pfeile dem Browser — der verschiebt `value`
und löst `input` aus, aber `change` kommt bei Tastatureingabe nicht
verlässlich, also wurde nie gesprungen. Die Ausnahme fällt weg; Pfeil
heisst überall ±10 Sekunden. Nativ wären es ohnehin 100 Millisekunden
gewesen, weil `step` auf 0,1 steht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 17:05:55 +02:00
ScarriffleandClaude Opus 5 93f4dfd8e9 Ein frischer JSONDecoder, drei Zeilen neben der Warnung davor
Im Web stand am Abspann „Nächste Folge", in der App nur „Abspann
überspringen". Der Server war es nicht: beide fragen denselben Endpunkt,
und der ordnet nach Staffel und dann nach Nummer — er findet also auch
die erste Folge der nächsten Staffel.

Es war `nachbar`. Die Stelle baute sich einen frischen `JSONDecoder()`,
und der kennt das Datumsformat des Servers nicht. Solange die
Nachbarfolge weder `publishAt` noch `lastPlayedAt` mitbrachte, ging es
gut — mit eingeschalteter Prüfung bringt sie ein `publishAt`. Das `try?`
schluckte den Fehler, und von aussen sah es aus, als gäbe es keine
nächste Folge: kein Knopf, kein Hinweis, nichts.

In `FlaroClient` steht seit je ein eingerichteter Dekoder, ausdrücklich
nicht privat, „damit die Prüfungen denselben Dekoder benutzen" — und
direkt darunter die Warnung, dass ein frischer an genau diesem Format
scheitert. Diese eine Stelle las sie nicht.

Vier Prüfungen dazu, darunter die mit Datumsangaben: mit dem alten
Dekoder ist sie rot, mit dem neuen grün. Gegengeprüft, nicht behauptet.

Die übrigen frischen Dekoder im Quelltext lesen nur, was dieselbe App
vorher selbst geschrieben hat — Zwischenspeicher, Abzüge, Offline-
Angaben. Die laufen rund und bleiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 08:39:31 +02:00
ScarriffleandClaude Opus 5 acba683c94 Eine Folge weiter, eine zurück — ohne den Spieler zu verlassen
Wer eine Serie schaut, will blättern können. Bisher gab es genau einen
Knopf dafür, und der erschien erst am Ende: „Nächste Folge", wenn der
Abspann durchgelaufen war. Zurück ging gar nicht — nur über die
Serienseite, und die beendet die Wiedergabe.

Jetzt stehen beide Knöpfe in der Bedienleiste, in der App wie im Web,
links und rechts vom Abspielknopf. Nur bei Serien: bei einem Film
antwortet der Server mit 404, bei der letzten Folge mit 204, und beides
heisst hier „kein Knopf". Sie erscheinen also von selbst nur dort, wo
sie etwas tun.

Der Server kannte nur den Weg vorwärts. Jetzt gibt es `/previous`
daneben, und beide Richtungen kommen aus derselben Rechnung — als zwei
Endpunkte abgeschrieben liefen sie auseinander, sobald einer etwas
dazubekommt. Jellyfin hat kein Gegenstück zu „NextUp"; dort entsteht
die vorige Folge aus der Folgenliste der Serie.

Beim Wechsel gilt weiter, was für die Serie an Sprache eingestellt ist.
Ohne das spränge die nächste Folge auf die erste Tonspur der Datei
zurück — bei deutschen Veröffentlichungen also auf Deutsch, mitten in
einer Serie, die man im Original schaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 21:11:09 +02:00
ScarriffleandClaude Opus 5 8f0568cb6c Ohne Server stand man vor der Tür, mit dem Film in der Tasche
Fiel der Server aus, endete der Start bei einem Fehlerbildschirm mit
zwei Knöpfen: „Erneut versuchen" und „Abmelden". Beide nützen im
Flugzeug nichts — und dahinter lagen die Dateien, die man genau für
diesen Fall heruntergeladen hatte.

Jetzt geht die App trotzdem auf: Token und Konto liegen auf dem Gerät,
die Mediathekenliste auch, und die Startseite kommt neuerdings aus
demselben Speicher. Oben die Downloads, darunter der Stand vom letzten
Mal, ausgegraut, mit einem Satz, der sagt warum. Die Detailseite nimmt
den Titel vom Gerät, wenn der Server schweigt, und der Abspielknopf
bleibt an, weil die Datei ja da ist.

Und was man offline geschaut hat, zählt: Fortschritt, den der Server
nicht annimmt, wird zurückgelegt und nachgereicht, sobald er wieder
antwortet. Dafür sagt `reportProgress` jetzt, ob die Meldung ankam.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 19:41:22 +02:00
ScarriffleandClaude Opus 5 84612335ec Sprachwahl in der App — bei Serien und bei Filmen
Auf der Serienseite stand ein Menü für den Ton, gefüllt mit zwanzig
Sprachen, von denen die Serie zwei hatte. Für Untertitel gab es keines,
und auf der Filmseite überhaupt nichts.

Jetzt beides an beiden Orten, und zwar nur mit dem, was es wirklich gibt:

Bei einer **Serie** über den neuen Endpunkt — die Schnittmenge über alle
Folgen. Was dadurch wegfällt, steht als Satz darunter, sonst sähe eine
gekürzte Liste aus wie die ganze Wahrheit.

Bei einem **Film** aus den Spuren der Datei. Kein Serverweg nötig: dort
ist die Datei der Titel, und ihre Spuren stehen in der Antwort, die die
Seite ohnehin geholt hat.

Ton und Untertitel teilen sich einen Typ. Sie unterscheiden sich in genau
zwei Punkten — woher der Wert kommt und ob es „Keine" gibt; ein zweiter
Typ wäre dieselbe Datei mit zwei geänderten Zeilen. „Keine" gibt es nur
beim Untertitel: ein Film ohne Tonspur ist kein Wunsch, sondern ein
Fehler.

Die Untertitelsprache liegt je Titel, in einem Topf für Serien und Filme
— eine Filmkennung kollidiert mit keiner Serie, und zwei Wörterbücher für
dieselbe Frage liefen auseinander. Der Spieler liest sie jetzt: vorher
kannte er nur die allgemeine, und eine Serie im Original mit deutschen
Untertiteln liess sich nicht einstellen, ohne sie überall einzuschalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 21:56:15 +02:00
ScarriffleandClaude Opus 5 1bcf6f12e5 Die App wartete auf eine fertige Datei, statt zu streamen
Gleichzeitig im Web und in der App auf Abspielen getippt: das Web lief,
die App lud dreissig Sekunden. Das war keine Ladezeit, sondern die Zeit,
die der Server braucht, um den ganzen Film einmal durchzuschreiben.

`preparePlayback` kannte nur `/files/{id}/download`. Muss die Datei
umgepackt werden, fragte es im Sekundentakt nach, bis die vollständige
MP4 auf der Platte lag — Grenze zehn Minuten. Der Endpunkt, den das Web
benutzt, ist am Server längst da; die App wusste nur nichts davon.

Jetzt zuerst `/files/{id}/stream`. Das erste Stück ist beim Umpacken in
deutlich unter einer Sekunde da. Der alte Weg bleibt als Rückfall: er ist
langsam, kommt aber ohne Sitzung aus, und eine späte Wiedergabe ist
besser als keine.

**Der Strom beginnt dort, wo man weiterschauen wollte**, und zählt
trotzdem ab null. Ohne das mitzurechnen stünde bei „Fortsetzen ab 1:32"
eine Null auf der Uhr — und genau die ginge als Fortschritt an den
Server. Also: die Zeit der Engine plus Vorlauf ist die Zeit des Films,
und der Spieler springt nicht noch einmal dorthin, wo der Server schon
steht.

**Ein Sprung aus dem Strom heraus baut ihn neu auf.** Die Abspielliste
wächst von der Einstiegsstelle vorwärts; davor und dahinter gibt es
nichts, und ein Sprung ins Leere liesse den Spieler mit einem Kreisel
stehen. Innerhalb des Erzeugten springt er weiterhin selbst — dafür eine
neue Sitzung zu starten hiesse, Sekunden zu warten, um fünf Sekunden
zurückzugehen.

**Und die Sitzung wird geschlossen.** `endPlayback` war eine leere
Methode; das passte, solange es keine Sitzung gab. Mit dem Strom läuft
sonst ein ffmpeg weiter und schreibt den Film zu Ende, während längst
niemand mehr zusieht.

Die Laufzeit kommt weiter aus den Metadaten und nicht aus der Engine:
eine wachsende Liste meldet anfangs ein paar Sekunden, und als Filmlänge
genommen stünde der Regler sofort am Ende.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 14:22:18 +02:00
ScarriffleandClaude Opus 5 c5ab200846 Die Startseite holte ihre Reihen im Gänsemarsch
Die Weboberfläche holt alles, was auf der Startseite steht, in einem
einzigen `Promise.all` — und sie holt sogar mehr davon: Vorschlag und
saisonale Reihe kennt die App gar nicht. Trotzdem war sie spürbar
langsamer. Der Grund stand in einer Schleife:

    for mediathek in mediatheken {
        ... await client.send(...)
    }

Ein `await` im Schleifenrumpf heisst: Rundreise, warten, nächste
Rundreise. Bei vier Mediatheken viermal hintereinander, nachdem
„Fortsetzen" und „Als Nächstes" längst da waren. Dieselbe Schleife stand
in beiden Hintergründen, für Flaro wie für Jellyfin.

Jetzt laufen sie nebeneinander, in einer `TaskGroup`. Die Reihenfolge der
Mediatheken bleibt erhalten — nach den Antworten zu ordnen hiesse, dass
die Reihen nach jedem Start woanders stünden. Ein Actor steht dem nicht
im Weg: jede Anfrage gibt ihn beim Warten frei, genau wie die beiden
`async let` darüber es schon tun.

Nebenbei: ein Fehler bei einer Mediathek nimmt die anderen nicht mehr
mit. Eine Startseite mit drei von vier Reihen ist besser als keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 13:55:24 +02:00
ScarriffleandClaude Opus 5 e30158b84c Eine Sekunde heisst nicht „Weiterschauen"
Die Schwelle aus den Einstellungen wirkt serverseitig, also sofort überall:
`/home/continue` liest die gespeicherte Spalte. Eine Stelle blieb trotzdem
widersprüchlich — die **Beschriftung**.

Web und Apps entschieden „Weiterschauen" gegen „Abspielen" selbst, allein
daran, ob eine Stelle über null steht. Nach einer Sekunde stand der Titel
damit nicht mehr in der Reihe, hiess aber weiter „Weiterschauen", trug
einen Fortschrittsbalken unter der Kachel und bot „Aus Weiterschauen
entfernen" an.

Der Server hatte diese Frage längst beantwortet. `ItemStateResponse` bringt
sie jetzt mit; `MediaItem.isResumable` und `.resumeProgress` sind die
einzigen Stellen, die sie noch stellen. Im Simulator nachgeprüft: nach
einer Sekunde heisst der Knopf „Abspielen", „Von vorn" fehlt, und der
Balken unter der Kachel ist weg.

Kennt ein Server das Feld nicht — Jellyfin, oder ein älterer Flaro —, gilt
wie früher die blosse Stelle.

**Und das Profilbild steht in den Apps.** `FlaroAccount` führt es mit,
`/auth/me` bringt es bei jedem Start frisch, und der Buchstabenkreis tritt
zurück, sobald eines da ist. Jellyfin bleibt beim Buchstaben — dort gibt es
kein Profilbild, und das ist keine Lücke, sondern der Stand der Dinge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 17:30:38 +02:00
ScarriffleandClaude Opus 5 10a5070e58 Eine Folge ist weder ein Film noch eine Serie
Die App meldete auf der Startseite „Dieser Titel ist auf dem Server nicht
mehr vorhanden" — über eine Folge, die sie selbst gerade anzeigte.

Gemessen im Quelltext: `item(id:)` probiert `/movies/{id}`, dann
`/series/{id}`, und wirft sonst `.notFound`. In „Weiterschauen" steht
eine Folge mit ihrer eigenen Kennung, und einen Endpunkt für eine
einzelne Folge gab es gar nicht. Jetzt gibt es `GET /api/v1/episodes/{id}`
— mit `SeriesId`, denn ohne die Serie fehlen der Folge Titel und Bild.
Im Simulator nachgestellt und nachgeprüft: die Seite steht.

**Zuletzt hinzugefügt.** Die App zeigte „Filme" und „Serien" — den Anfang
der Mediathek in der Reihenfolge der Datenbank, also immer denselben.
Jetzt je Mediathek eine Reihe „Neu in …", sortiert nach Zugang. Die
Kennungen beginnen nun mit `latest`, und daran hängen zwei Einstellungen,
die an einem Flaro-Server deshalb nichts taten: „Gesehenes in Neuzugängen
ausblenden" und „Aufmacher nur aus Neuzugängen".

**Geplante Titel.** `MediaItem` kennt jetzt `availableFrom`. Aufmacher und
Detailseite zeigen statt des Knopfes „Bald ist es soweit" mit dem Termin.
Die Kachel spielte trotzdem los — im Simulator kam der Spieler bis „Wird
vorbereitet …" —, also zählt ein geplanter Titel jetzt wie eine Sammlung:
nichts zu starten, der Tipp führt auf die Seite.

**Und das Bild im Aufmacher.** Bei einer angefangenen Folge stand dort ihr
Standbild statt des Bildes der Serie; wer der Serie eines ausgesucht
hatte, fand ein anderes vor. Die Kachel behält das Standbild — sie zeigt,
wo man steht —, der Aufmacher bekommt `SeriesBackdropImage`.

**Die Punkte im Karussell waren 26 × 3 Pixel.** Von drei Klicks auf
denselben Punkt traf nur der auf der Mittellinie; fünf Pixel daneben
geschah nichts. Der Knopf ist jetzt 27 Pixel hoch, sichtbar bleiben 3.

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

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

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

---

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

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

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

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

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

---

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 22:00:02 +02:00