Commit Graph
100 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 c13d1e78d2 Eine Mediathek für alles, was in keinem Katalog steht
Urlaubsaufnahmen, Mitschnitte, Vorträge. Jedes Verfahren, das bei Filmen
richtig ist, wäre hier falsch: „Sommer 2019 Teil 2" ist kein Film von 2019,
zwei Aufnahmen desselben Tages sind nicht zwei Fassungen derselben Sache,
und in keinem Katalog steht, was da liegt.

Die neue Art `Videos` nimmt den Dateinamen als Titel, zerlegt nichts, legt
nichts zusammen und fragt niemanden. Der Pfad ist dabei der
Vergleichsschlüssel, nicht der Name — zwei Dateien „Ausflug.mp4" in zwei
Ordnern sind zwei Ausflüge.

**Der Unterordner wird zur Kategorie.** Statt eine eigene Gruppierung zu
bauen, bekommt jedes Video den Namen seines Ordners als Genre. Damit
greifen Filter, Kategorienansicht und Suche, ohne dass es in der Oberfläche
etwas Neues gibt. Eine zweite Gruppierung daneben hiesse: zwei Wege für
dasselbe, und einer davon wäre überall sonst ungepflegt.

Die Anreicherung übergeht jetzt alles, was `MatchLocked` trägt. Das galt
bisher nur für von Hand korrigierte Zuordnungen und gilt damit auch hier —
eine Suche nach „Abend am Feuer" fände irgendeinen fremden Film, und der
stünde danach mit Plakat und Beschreibung in der Urlaubsmediathek.

Geprüft: vier Dateien in zwei Ordnern ergeben vier Titel mit ihren
Dateinamen, keine Jahre, keinen fremden Treffer — und genau zwei
Kategorien, denn was direkt in der Wurzel liegt, bekommt keine.

742 Tests grün, davon zwei neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:36:06 +02:00
ScarriffleandClaude Opus 5 af2b844d65 IMDb, Rotten Tomatoes und Metacritic neben der TMDB-Wertung
IMDbs eigene Schnittstelle kostet Geld, Rotten Tomatoes vergibt Zugänge nur
an Partner, Letterboxd hat gar keine offene. OMDb liefert alle drei in
einer Antwort, über die IMDb-Kennung, die ohnehin schon an jedem Film steht.

**Eine Tabelle und kein Feld je Quelle.** `Movie.Rating` ist ein Skalar
ohne Quellenangabe, und das war richtig, solange es eine Quelle gab. Für
drei weitere wären es drei Spalten — und bei der vierten wieder eine
Migration. Jetzt ist jede Quelle eine Zeile, je Titel und Quelle eine, mit
eindeutigem Index darauf: ein zweiter Lauf schreibt um statt anzuhäufen.

**Zwei Werte je Bewertung, und beide gebraucht.** Der normierte Wert zum
Vergleichen und Sortieren, die Anzeige zum Hinschreiben. Rotten Tomatoes in
„8,7 von 10" umzurechnen wäre eine Lüge über die Quelle — dort steht 87 %,
und genau so kennt man es.

**Wieder keine zweite Metadatenquelle.** `IBewertungsquelle` liefert
Bewertungen, sonst nichts. Bei Titel und Beschreibung wären zwei Anbieter
zwei Wahrheiten; bei einer Bewertung ist jede Quelle eine eigene Zahl, und
da sind mehrere kein Widerspruch, sondern der Sinn der Sache.

**Ohne Kennung wird nicht gefragt.** OMDb kennt nur den Weg über die
IMDb-Kennung; nach einem Titel zu suchen hiesse, denselben Zuordnungsfehler
ein zweites Mal zu riskieren. Und auf Unbekanntes antwortet OMDb mit 200
und `Response: "False"` — das als Treffer durchzulassen legte eine leere
Bewertung ab, die danach als „keine Wertung" dastünde.

Angezeigt wird mit Kürzel statt Stern: vier gleich aussehende Zahlen
nebeneinander sagen nicht, woher sie kommen. Kein Logo — die Marken von
IMDb und Rotten Tomatoes mitzuliefern hiesse, fremde Zeichen auszuliefern.

Kein mitgelieferter Schlüssel, aus demselben Grund wie bei fanart.tv: OMDb
begrenzt auf tausend Abrufe am Tag je Schlüssel. Einzutragen unter
Einstellungen → Übersicht → Metadaten.

740 Tests grün, davon acht neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:26:58 +02:00
ScarriffleandClaude Opus 5 97edb49564 Eine zweite Bildquelle, weil TMDB nicht alles hat
„365 Tage" stand mit polnischem Plakat da. Nachgesehen: TMDB hat zu diesem
Film 26 Plakate und kein einziges deutsches. Die Auswahl von gestern nimmt
seither das sprachlose statt des polnischen — erfinden kann sie keines.

fanart.tv ist die Quelle, bei der jedes Plakat seine Sprache mitführt, und
die Gemeinschaft dort pflegt landessprachliche Fassungen. OMDb wäre keine
Hilfe gewesen: es liefert das Plakat von IMDb, und das ist in aller Regel
das amerikanische.

**Ausdrücklich keine zweite Metadatenquelle.** `IBildquelle` liefert
Bilder, sonst nichts. Titel, Beschreibung und Besetzung kommen weiterhin
von einer Stelle — zwei Anbieter für dieselbe Angabe wären zwei Wahrheiten.

**Gefragt wird nur, wenn es lohnt.** `MovieMetadata` trägt jetzt die
Sprache des gewählten Plakats mit. Hat TMDB schon eines in der
eingestellten Sprache, geht keine Anfrage hinaus — bei achthundert Titeln
ist das der Unterschied zwischen „läuft nebenher" und „hängt am fremden
Dienst". Und nur ein landessprachliches sticht: ein sprachloses von
fanart.tv ist nicht besser als ein sprachloses von TMDB.

**Die Rangfolge liegt jetzt im Kern**, nicht im TMDB-Client. Sie gilt für
alle Quellen, sonst kämen die Bilder zweier Anbieter in zwei Reihenfolgen
zusammen. Im Bilderdialog stehen beide Quellen gemeinsam geordnet: ein
deutsches Plakat von fanart.tv vor einem sprachlosen von TMDB.

**Nur Filme.** Der Serienweg von fanart.tv verlangt eine TVDB-Kennung, und
die führt Flaro nicht. Eine halbe Lösung, die bei Serien stillschweigend
nichts tut, wäre schlechter als eine, die sagt, worauf sie sich beschränkt.

**Kein mitgelieferter Schlüssel.** Bei TMDB gibt es einen als Rückfall;
fanart.tv verlangt einen persönlichen, und einen fremden mitzuliefern
hiesse, ihn fremd zu benutzen. Ohne eigenen Schlüssel geht keine Anfrage
hinaus — geprüft. Einzutragen unter Einstellungen → Übersicht → Metadaten.

Dass „00" dort „ohne Sprache" heisst, wird beim Lesen übersetzt: sonst
führte die gemeinsame Rangfolge ein sprachloses Plakat als Sprache „00"
und setzte es hinter Englisch statt davor.

732 Tests grün, davon sieben neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:18:27 +02:00
ScarriffleandClaude Opus 5 2bd13f0f19 Breitbild sass oben statt mittig
Ein 21:9-Film war in der Breite richtig und in der Höhe nicht: der schwarze
Rand lag ganz unten statt gleichmässig oben und unten. Im Fenster fällt das
kaum auf, im Vollbild sofort.

Gemessen an einem 2560×1080-Film in einem 1024×768-Fenster:

    Hülle        768 hoch
    Videokasten  459 hoch, bei oben 0

Woran es lag: die Hülle ist ein Raster mit `place-items: center`. Damit
wird das Videoelement nicht gedehnt, und sein `height: 100%` löst sich
gegen eine Zeile auf, die ihre Höhe wiederum aus dem Element nimmt — die
Prozentangabe hatte nichts, woran sie sich halten konnte. Herausgekommen
ist ein Kasten, der weder die volle Höhe hatte noch mittig sass.

Jetzt liegt das Videoelement im grossen Spieler absolut über der ganzen
Hülle. `object-fit: contain` rechnet damit gegen den vollen Bildschirm und
legt den Rand von selbst zu gleichen Teilen oben und unten — in beiden
Richtungen, nicht nur in der einen, die vorher stimmte.

Nachher dieselbe Messung: Kasten 1024 × 768, oben 0.

Die kleine Leiste bleibt unberührt — sie positioniert dasselbe Element
ohnehin absolut und mit eigenen Massen; deshalb das `:not(.pl-klein)`.
Nachgemessen: 128 × 70, `object-fit: cover`, unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:07:31 +02:00
ScarriffleandClaude Opus 5 144470a413 Im Browser gibt es keine Datei mehr, nur den Strom
Auf der Detailseite stand ein Knopf „Herunterladen", der die Datei als MP4
herausgab. Das ist etwas anderes, als einen Strom zu zeigen: wer eine Datei
bekommt, hat sie und kann sie weitergeben — und das soll dieser Server
nicht anbieten.

Weg sind der Knopf, das Bereitstellen davor und die beiden Stellen in der
Schnittstelle, die nur dafür da waren.

**Der Endpunkt bleibt.** Die Apps holen sich eine Mitnahme über
`/api/v1/files/{id}/download`, und dort ist sie an ein Gerät gebunden statt
an einen Ordner. Das heisst aber auch: wer angemeldet ist und die Adresse
kennt, kommt weiterhin an die Datei. Soll das zu — etwa als Recht je
Benutzer oder als Schalter für den ganzen Server —, ist das ein eigener
Schritt und keine Zeile in dieser Änderung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:07:20 +02:00
ScarriffleandClaude Opus 5 532473b1f9 Ein Plan für das, was an keinem Aufruf hängt
Sichern, Datenbank aufräumen, verwaiste Bilder wegwerfen — alle drei Tage,
alle sieben Tage, von selbst.

**Der erste Hintergrunddienst in diesem Server, und das mit Bedacht.** An
drei Stellen steht sinngemäss, ein eigener Dienst wäre Aufwand ohne
Gegenwert, und für das Wegräumen abgelaufener Gerätecodes stimmt das:
solche Arbeit hängt an einem Aufruf, der ohnehin kommt. Eine Sicherung
hängt an nichts. Sie soll alle drei Tage geschehen, auch wenn niemand etwas
anklickt, und dafür gibt es eine Uhr. Alle fünfzehn Minuten eine Frage an
eine Tabelle mit einer Handvoll Zeilen; ist nichts fällig — der Normalfall
—, kostet es nichts.

Der letzte Lauf steht in der Datenbank, nicht im Arbeitsspeicher. Ein
Server, der jede Nacht neu startet, sicherte sonst jeden Morgen — oder,
bei einem Zähler, der bei null anfängt, nie. Auch ein Fehlschlag wird
vermerkt, sonst versuchte der Plan es alle fünfzehn Minuten wieder und
eine volle Platte erzeugte ein volles Protokoll.

**Die Sicherung ist `VACUUM INTO`, nicht Datei kopieren.** Die Datenbank
läuft im WAL-Modus: neben `flaro.db` stehen `-wal` und `-shm`, und wer nur
die erste Datei kopiert, bekommt einen Stand von vor Stunden oder einen
halben. `VACUUM INTO` schreibt eine vollständige, aufgeräumte Kopie im
laufenden Betrieb. Der Zielordner ist einstellbar und geht als gebundener
Wert in die Anweisung — eine Einstellung soll keine SQL-Anweisung werden.

Die Prüfung verlässt sich nicht darauf, dass eine Datei entstanden ist: sie
öffnet die Kopie als Datenbank und zählt die Zeilen. Eine Sicherung, die
sich nicht öffnen lässt, ist schlimmer als keine — man verlässt sich
darauf und merkt es erst, wenn es zu spät ist.

**Bilder nur, wenn verwaist.** Den ganzen Bildspeicher zu leeren gibt es
als Knopf und ist gefahrlos, kostet danach aber tausende Abrufe bei TMDB.
Hier fliegt nur, worauf kein Titel mehr zeigt — mit einer Stunde
Schonfrist, weil ein Bild abgelegt wird, bevor der Verweis darauf
gespeichert ist. Ohne diese Frist räumte ein Lauf mitten im Scan genau das
weg, was gerade geholt wurde.

**Eine eigene Seite „Wartung".** Die drei Kratzplatzzahlen standen unter
„Wiedergabe" — dort ist ihr Anlass, aber nicht ihr Thema. Sie ziehen mit
um: alles auf dieser Seite ist etwas, das der Server ohne Zutun erledigt,
und man sieht es an einem Ort nach. Je Aufgabe steht da, wann sie zuletzt
lief und was herauskam, dazu ein Knopf „Jetzt ausführen" — wer gleich etwas
Grosses vorhat, will die Kopie jetzt und nicht in drei Tagen.

725 Tests grün, davon elf neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 21:02:42 +02:00
ScarriffleandClaude Opus 5 c4af182d31 Der Kratzplatz räumt sich auf
39,1 GB in FLARO_TRANSCODE, und das war kein Normalzustand, sondern ein
Leck: für jede Mitnahme baut der Server eine MP4 in Quellgrösse und löschte
sie nie. Die einzige Löschung, die es gab, räumte ältere Fassungen
derselben Datei weg. Stromordner verschwanden nur beim geordneten
Herunterfahren, Untertitel gar nicht.

**Zuerst: fertig heisst fertig.** ffmpeg schrieb direkt auf den Endnamen.
Nach einem harten Abbruch lag dort eine abgeschnittene Datei, `IsReady`
meldete „fertig", und sie ging so hinaus — und ein Aufräumer konnte
vollständig und abgeschnitten nicht unterscheiden. Gebaut wird jetzt unter
`.teil`, umbenannt erst nach Erfolg. Dabei muss das Format ausdrücklich
gesetzt werden: ffmpeg wählt den Muxer an der Endung und antwortet auf
`.teil` mit „Unable to choose an output format". Das hätte jedes Umpacken
zerlegt; die Prüfung hat es gefunden, bevor es jemand zu spüren bekam.

**Zwei Stufen für die Pakete.** Verwaiste gehen bedingungslos: unleserlicher
Name, fremde Bauart, kein Eintrag in der Mediathek, oder der Zeitstempel im
Namen passt nicht mehr. Der Rest fällt unter Obergrenze (20 GB) und Alter
(7 Tage), Ältestes zuerst.

Beides, und nicht eines: ein Alter allein löst den Fall nicht, denn die 39
GB waren gültige, aktuelle Dateien, jede für sich berechtigt. Eine
Obergrenze allein ist zu grob — ein Topf knapp darunter, in dem seit Wochen
nichts gebraucht wurde, besetzt ein Volume, das anderes braucht.

„Zuletzt gebraucht" ist die Änderungszeit der Paketdatei selbst, bei jeder
Auslieferung neu gestempelt. Die Zugriffszeit wäre das Naheliegende, nur
führt sie nicht jedes Dateisystem — mit `noatime` steht sie still, und der
Aufräumer hielte alles für uralt.

Ein laufender Download wird nie gelöscht: ein Benutzungszähler hält die
Datei fest, solange die Antwort läuft, dazu eine Schonfrist von 30 Minuten.
Der Zähler ist auch unter Linux nötig — der laufende Strom überlebt ein
Entfernen, die Fortsetzung einer Bereichsanfrage nicht, und der Client finge
von vorn an.

**Stromordner** werden beim Start bedingungslos geleert — nach einem Start
gibt es keine einzige Sitzung, also ist jeder Ordner verwaist. Im Betrieb
nur die ohne passende Sitzung, deren Änderungszeit älter ist als die
Geduld; das fängt auch den Fall, dass `Abraeumen` an einem IOException
scheitert und der Ordner für immer liegenbleibt.

**Untertitel** nur, wenn sie verwaist sind. Keine Obergrenze: eine .vtt ist
Kilobytes, und eine gültige wird wieder gebraucht — sie nach Platz zu
löschen hiesse, Minuten Lesearbeit gegen Megabytes zu tauschen. Eine leere
bleibt ausdrücklich liegen: sie ist das Ergebnis „diese Spur hat keinen
Text", und wer sie wegräumt, lässt bei jedem Start eine vierzig Gigabyte
grosse Datei neu durchlesen.

**Vier Anlässe, kein Dienst.** Start, vor jedem Umpacken, nach einem Scan,
auf Knopfdruck — wie alles Wiederkehrende in diesem Server. Einer zur Zeit;
wer nicht hineinkommt, geht weiter.

Gemessen am laufenden Server mit Attrappen:

    vorher    packages 17408 KB (4), untertitel 5 KB (3), 1 Stromordner
    nachher   14 MB freigegeben, 4 Dateien
              gültiges Paket blieb, verwaistes/veraltetes/Teilstück weg,
              verwaiste .vtt weg, gültige blieben, Stromordner weg

    Obergrenze 1 MB, Datei in der Schonfrist   -> bleibt
    Obergrenze 1 MB, Schonfrist vorbei         -> 3072 KB freigegeben

In der Übersicht steht die Belegung je Unterordner mit Dateizahl und
daneben „Jetzt aufräumen"; die drei Zahlen stehen unter Wiedergabe, wo
Transcoder-Stufe und Schwellen schon stehen — der Kratzplatz ist, was die
Wiedergabe hinterlässt.

714 Tests grün, davon 36 neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 19:07:52 +02:00
ScarriffleandClaude Opus 5 1b4784f1db Zwei Fassungen dürfen gewollt sein, und die Startseite darf nicht warten
**Mehrfachfassungen lassen sich abhaken.** Zwei Dateien an einem Film sind
meist zwei Fassungen und völlig in Ordnung. In der Liste standen sie
trotzdem für immer: sie rechnet jedes Mal frisch, und es gab keinen Zustand.
Wer absichtlich zwei Fassungen hält, konnte die Gruppe nicht loswerden — und
unter den gewollten ging das Ungewollte unter.

Abgehakt wird eine Dateizahl, kein Zeitpunkt. Ein Zeitstempel müsste gegen
`MediaFile.ModifiedAt` verglichen werden, und eine alte Datei, frisch
hineinkopiert, behält ihre alte Zeit und käme nie wieder hoch. Die Anzahl
ist genau die Frage: kommt eine dritte Fassung dazu, meldet sich die Gruppe
von selbst zurück. Gerechnet wird sie auf dem Server — ein Client mit
veralteter Liste hakte sonst eine Gruppe mit zwei Dateien ab, die inzwischen
drei hat.

„Ignorierte zeigen" führt zurück, wie bei den erledigten Meldungen; ohne das
wäre das Abhaken unumkehrbar und niemand wüsste, was er weggeklickt hat.

**Die Startseite wartete dreimal nacheinander.** Erst die Serverauskunft,
dann Konto und Mediatheken, dann erst die Startseite — drei Rundreisen,
bevor das erste Bild in Auftrag ging. Liegt eine Sitzung im Speicher, laufen
die ersten beiden jetzt zugleich los. Nur dann: ohne Sitzung liefen
`/auth/me` und `/libraries` vor der Antwort auf `needsSetup` in ein 401, und
die Sitzungsprüfung kann daraufhin abmelden.

Gemessen am laufenden Server: `users/me`, `system/info` und `libraries`
starten alle bei 37 ms, die Startseitenabfragen folgen bei 73 ms. Vorher
lagen drei Runden dazwischen.

**Sieben Hintergrundbilder auf einmal waren sechs zu viel.** Jede Folie des
Aufmachers bekam sofort ihr `w1280`-Bild; sichtbar ist immer nur eine. Jetzt
lädt die sichtbare sofort und der Rest in der Leerlaufzeit — und wer
weiterschaltet, bevor das lief, bekommt sein Bild dann.

**Die Schrift kam von Google.** `display=swap` rettete den Text, nicht den
ersten Aufbau: ein Stylesheet blockiert das Zeichnen, und ein Heimserver
ohne Internet oder mit träger Namensauflösung wartete davor. Inter liegt
jetzt als zwei Dateien von zusammen sechzig Kilobyte im Projekt, variabel
für alle Schnitte, mit `unicode-range` getrennt nach latin und latin-ext.
Gemessen: null Anfragen an fonts.googleapis.com. Lizenztext liegt dabei.

**Und mein Untertitelvorrat bremste mit.** Er startete sofort beim
Hochfahren und las die ganze Mediathek — von derselben Platte, von der die
Startseite ihre Bilder holt. Jetzt beginnt er eine Minute später und stellt
zurück, solange eine Wiedergabe läuft. Die Datei bleibt dabei vorn in der
Reihe; verworfen wird nichts.

678 Tests grün, davon zwei neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 17:56:41 +02:00
ScarriffleandClaude Opus 5 2809821c9b Vier Fehler, und bei zweien log der Kommentar daneben
**Der Einstellungsknopf lag hinter der Abspielleiste.** Gemessen: `.pl`
steht auf Stufe 150, `#chrome` auf gar keiner, und die kleine Leiste spannte
über die ganze Breite — genau über die unteren 72 Bildpunkte der
Seitenleiste, wo „Einstellungen" klebt. Weil der Spieler bei jedem Wechsel
der Adresse minimiert, stand sie zuverlässig im Weg, sobald man dorthin
wollte.

Die Leiste beginnt jetzt hinter der Seitenleiste. Nicht über die Stufen
gelöst, und das ist der Punkt: `#chrome` höher zu setzen hiesse, die
Seitenleiste läge auch über dem grossen Spieler — es ist dieselbe Hülle.
Dass diese Leiste Platz wegnimmt statt sich darüberzulegen, stand für
Inhalt, Hinweise und Auswahlleiste ohnehin schon da; die Seitenleiste war
die vergessene vierte. Gemessen, 1400 Bildpunkte breit: Leiste ab 240,
Knopf bei 0, keine Überschneidung, und ein Treffertest in der Knopfmitte
trifft den Knopf.

**Ein Klick auf eine Kategorie kam nie an.** Dort sprang der Code in die
Mediathek und suchte danach `.filterzeile select`, um den Wert zu setzen.
Die Filterzeile hat kein `<select>` — `auswahl()` baut einen Knopf, mit
Absicht und begründet. Der Zweig lief also nie, und man landete in der
ungefilterten Mediathek. Der Kommentar daneben behauptete das Gegenteil.

Das Genre steht jetzt in der Adresse: `#/m/{id}/mediathek/Horror`. Das
löst zugleich das zweite Problem — die Mediathek wird unter ihrem Weg
gemerkt, und stünde das Genre nur im Kopf, zeigte ein zweiter Besuch die
Liste des ersten. Geprüft: Klick setzt den Weg und die Pille, Neuladen
behält beides, Zurück führt auf die Kategorien, Vorwärts wieder hinein.

**Zurückgezogene Einladungen blieben für immer stehen.** Der Knopfblock
steckte ganz in `if (!e.revokedAt)`, eine erledigte Zeile hatte also gar
keinen Knopf. Die Serverbegründung — eine Zeile wegzuwerfen hiesse,
hinterher nicht mehr zu wissen, dass es den Link je gab — bleibt richtig,
sie gilt nur nicht ewig: eine Liste, aus der nichts verschwindet, wird
unlesbar, und dann sieht man die eine Zeile nicht mehr, um die es ging.

Deshalb kein umgedeutetes DELETE, sondern `?purge=true`, und das greift nur
bei einem Link, der ohnehin keine Tür mehr öffnet. Ein gültiger wird mit
einem deutschen Satz abgewiesen.

**Poster in der falschen Sprache.** „365 Tage" stand mit polnischem Plakat
da. Kein fehlender Anbieter: für den Schriftzug gab es die sprachgewichtete
Wahl längst, fürs Plakat wurde das blanke `poster_path` der Hauptantwort
genommen — bei TMDB das Bild der Originalsprache, wenn in der gewünschten
keines eingetragen ist. Die sortierte Liste lag im selben Abruf schon vor
und wurde ausschliesslich für den Schriftzug benutzt.

Plakat und Hintergrund nehmen jetzt dieselbe Rangfolge: eingestellte
Sprache, dann sprachlos, dann Englisch. Sprachlos vor Englisch ist keine
Verlegenheit — ein Bild ohne Sprachkennung trägt keinen Text, und ein
Plakat ohne Titel ist für einen deutschen Bestand besser als dasselbe mit
englischem. Im Bilderdialog stehen die Kandidaten jetzt ebenso sortiert.
Bestehende Titel ändern sich beim nächsten Auffrischen; von Hand gesetzte
Plakate bleiben unangetastet.

676 Tests grün, davon zehn neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 17:46:46 +02:00
ScarriffleandClaude Opus 5 c6df9fdab0 Die Schriftgrösse war am falschen Ende grosszügig
60 bis 300 Prozent waren es, und gebraucht wird der Weg nach unten: schon
die Vorgabe des Browsers ist auf einem grossen Bildschirm eher zu gross als
zu klein, und 300 Prozent füllt das halbe Bild. Jetzt 30 bis 120 Prozent,
mit unveränderter 100-Prozent-Marke.

Dazu Fünferschritte statt Zehner. Auf einer Spanne von 90 statt 240 Prozent
wiegt ein Zehnerschritt doppelt so schwer; bei 35 Prozent ist der
Unterschied zu 45 Prozent kein Feinschliff mehr, sondern ein Sprung.

Ein gemerkter Wert von früher wird beim Lesen gekappt — geprüft: 190 im
Speicher ergibt Regler 120 und `font-size: 120%`. Und 30 Prozent ist am
Bild nachgesehen klein, aber lesbar; als Untergrenze taugt es.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 06:59:15 +02:00
ScarriffleandClaude Opus 5 963e5a74ae Untertitel so gross und so bunt, wie man sie braucht
Die Voreinstellung des Browsers ist weisse Schrift auf schwarzem Kasten, in
einer Grösse, die er aus der Höhe des Videos rechnet. Auf einem grossen
Bildschirm aus drei Metern Abstand ist das zu klein, und wer schlecht sieht,
braucht es anders.

Einstellbar sind Grösse (60 bis 300 Prozent in Zehnerschritten),
Schriftfarbe (Weiss, Hellgelb, Hellblau, Schwarz) und Hintergrund
(Halbdunkel, Schwarz, Grau, Ohne).

Gestaltet wird mit `::cue`, nicht mit eigenen Kästen. Der Browser zeichnet
die Einblendungen selbst — Position, Umbruch, Zeilenabstand und die
Platzangaben aus der Datei. Das nachzubauen hiesse, all das noch einmal zu
schreiben und dabei schlechter zu werden.

Gemessen an einem Video mit echter Spur, Bild für Bild: 200 Prozent in
Hellgelb ohne Kasten und 140 Prozent in Hellblau auf Grau kommen beide so
heraus, wie sie gemeint sind.

Ohne Kasten setzt es von selbst einen Schatten. Weisse Schrift in einer
hellen Szene ist sonst weg, und das wäre eine Einstellung, die sich selbst
unbrauchbar macht. Mit Kasten wäre derselbe Schatten nur Unschärfe.

Die Einstellung sitzt im Untertitelmenü, abgetrennt unter den Spuren — wie
der Tonversatz im Tonmenü. Ob die Schrift gross genug ist, sieht man nur an
einem laufenden Film, in diesem Licht, aus diesem Abstand; wer dafür die
Wiedergabe verlassen muss, stellt zweimal falsch ein, bevor er einmal
richtig liegt.

Gemerkt wird im Browser, nicht am Konto: wie gross Schrift sein muss, hängt
am Bildschirm und am Abstand davor. Dasselbe Konto am Telefon und am
Fernseher will zwei verschiedene Grössen.

Farbfelder statt einer Liste mit Farbnamen — „Hellblau" muss man lesen und
sich vorstellen, ein Feld sieht man. Das gewählte trägt einen Ring aussen
herum und keinen Haken darin: ein Haken wäre auf Weiss unsichtbar und auf
Schwarz erst recht. „Ohne Hintergrund" zeigt ein Karomuster, weil ein
leeres Feld nach Weiss aussähe.

Gelesen wird nur, was bekannt ist. Ein fremder Eintrag im Speicher darf
nicht zu beliebigem CSS in der Seite werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 06:43:26 +02:00
ScarriffleandClaude Opus 5 40f41f0cee Zwei Tonspuren sind eine Wahl, kein Chor
„Auf der App höre ich einfach alle Audiospuren gleichzeitig."

MP4 ordnet Spuren derselben Art einer `alternate_group` zu, und ein
Abspieler nimmt daraus genau eine. ffmpeg leitet diese Gruppe aus der
Markierung der Quelle ab — sind dort zwei Tonspuren als Vorgabe markiert,
hält es sie für Verschiedenes und legt sie in getrennte Gruppen. Dann ist
jede für sich eingeschaltet, und man hört beide übereinander. Bei Rips aus
freier Wildbahn ist die doppelte Markierung eher Regel als Ausnahme; in
Matroska stört sie niemanden, in MP4 ist sie der Unterschied zwischen Wahl
und Chor.

Gemessen an einer Quelle mit zwei markierten Tonspuren, am Spurkopf der
fertigen MP4:

    vorher    Spur 2: an,  Gruppe 1
              Spur 3: an,  Gruppe 2     <- beide laufen
    nachher   Spur 2: an,  Gruppe 1
              Spur 3: aus, Gruppe 1     <- eine zur Wahl

Das Umpacken schreibt jetzt genau eine Vorgabe je Spurart: die, die es in
der Quelle schon war, sonst die erste. Auch die Nullen gehören
hingeschrieben — ffmpeg übernähme sonst die Markierung der Quelle für alle
übrigen, und genau die ist ja das Problem. „Erzwungen" wird dabei
ausdrücklich erhalten, weil `-disposition` die ganze Markierung ersetzt
statt sie zu ergänzen.

Untertitel bekommen dieselbe Behandlung. War in der Quelle keiner
vorgesehen, bleibt es dabei — sonst erschienen auf einmal zwei übereinander.

Der Name im Zwischenspeicher trägt jetzt die Bauart mit. Ohne das bliebe
auf einem NAS, das schon umgepackt hat, die fehlerhafte Datei liegen und
würde weiter ausgeliefert, obwohl der Server es längst besser kann.

Geprüft an der echten Datei, am `tkhd` und nicht an der Disposition: was
ein Abspieler tut, hängt an „enabled" und `alternate_group`, und ffprobe
zeigt davon nur einen Teil. Ohne die Korrektur fällt die Prüfung um
(Gruppe 1 gegen 2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-08 06:32:25 +02:00
ScarriffleandClaude Opus 5 1531163bc1 Die Untertitel holen, bevor jemand sie einschaltet
Zwei Minuten „wird geholt", und noch immer kein Text. Die Ablage von vorhin
war richtig und reichte nicht: beim ersten Mal musste immer noch jemand
warten, und zwar der, der gerade einen Film anschaut.

Das Lesen selbst lässt sich nicht beschleunigen. Untertitelpakete liegen
über die ganze Datei verteilt; sie zu finden heisst, alles zu lesen. Bei
vierzig Gigabyte auf einem NAS sind das Minuten, und daran ändert kein
Schalter etwas. Also nicht schneller lesen, sondern früher.

Der Vorrat arbeitet die Mediathek jetzt von sich aus durch: beim Start des
Servers und nach jedem Scan, die zuletzt hinzugekommenen zuerst. Eine Datei
nach der anderen, mit zwei Sekunden Pause dazwischen — das Lesen ist von
der Platte begrenzt, und eine laufende Wiedergabe liest von derselben
Platte und ist wichtiger. Wer einen Film startet, drängelt sich vor: seine
Datei ist als Nächstes dran statt als Dreissigste.

Einmal je Datei, nie wieder: der Name der Ablage trägt den
Änderungszeitpunkt. Eine ersetzte Datei bekommt dadurch von selbst einen
neuen Namen, eine unveränderte wird nie zweimal gelesen.

Gemessen am Prüfserver mit leerer Ablage:

    Start, ohne jeden Abruf   -> „1 Datei(en) werden im Hintergrund
                                  gelesen", danach „2 von 2 Spur(en)"
    Klick auf „Deutsch"       -> 153 ms bis zum Text, 10 Einblendungen,
                                  die erste bei 0 bis 3 s

Landet jemand trotzdem im Wartefall, gibt der Spieler nicht mehr nach zwei
Minuten auf, sondern fragt zehn Minuten lang nach — und sagt dazu, dass es
einmalig ist.

Die Auswahl der Textspuren stand zweimal im Quelltext und steht jetzt
einmal: im Vorrat. Zwei Listen, von denen eine das Menü füllt und die
andere bestimmt, was geholt wird, laufen sonst irgendwann auseinander.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 22:14:20 +02:00
ScarriffleandClaude Opus 5 c2fd2a86fd Einmal aus der Datei holen, nicht bei jedem Abruf
„Diese Untertitelspur liess sich nicht laden", und auf der zweiten Spur kam
gar nichts. Nicht das Format war schuld und auch nicht das Verschieben,
sondern die Zeit: jeder Abruf startete ffmpeg, ffmpeg musste die ganze
Datei durchlesen, und nach dreissig Sekunden wurde abgebrochen. An einem
Prüffilm von fünfzehn Megabyte fällt das nie auf; bei einem Dreistünder auf
einem NAS reicht es nie.

Schlimmer wurde es durch meine eigene Korrektur von gestern: seit die
Adresse den Einstiegspunkt mitführt (`?ab=`), ändert sie sich bei jedem
Sprung — der Browser konnte nichts mehr aus seinem Zwischenspeicher nehmen,
und jeder Sprung stiess einen neuen Durchlauf an.

Jetzt:

- Ein ffmpeg-Durchlauf holt alle Textspuren auf einmal. Das Lesen der Datei
  ist der teure Teil und wird nicht mehr je Spur bezahlt.
- Das Ergebnis bleibt liegen, ungeschoben; das Verschieben ist eine
  Zeilenersetzung und kostet nichts. Gemessen: erster Abruf 0,42 s, jeder
  weitere 2 bis 3 ms — und der zweite Sprung in derselben Datei trifft
  dieselbe Ablage statt eines neuen Durchlaufs.
- Angestossen wird beim Start des Stroms, nicht erst beim Einschalten. Wer
  nach einer halben Stunde Untertitel will, wartet dann gar nicht.
- Geschrieben wird unter `.teil` und erst danach umbenannt. Eine
  abgebrochene Datei am richtigen Namen gälte sonst für immer als fertig.
- Die Geduld steigt von 30 Sekunden auf 30 Minuten. Sie ist jetzt eine
  Obergrenze gegen einen hängenden Prozess, keine Wartezeit mehr.

Und der Spieler kann endlich unterscheiden, was los ist. Ein `<track>`
meldet jeden Misserfolg gleich — „es gibt sie nicht", „sie enthält keinen
Text" und „der Server holt sie gerade" sahen identisch aus. Geholt wird
deshalb selbst, mit Token statt Keks (gemessen: ohne Keks kam jeder Abruf
mit 401 zurück, ohne dass irgendwo etwas stand), und bei 503 fragt der
Spieler nach, statt aufzugeben. Der Text geht als Blob an den `<track>` —
dieselbe Anzeige, ein ehrlicher Weg dahin.

Dazu eine Unsauberkeit im Verschieben: die Trennzeile eines weggefallenen
Blocks blieb stehen. Gemessen am echten Film 52 Leerzeilen vor dem ersten
Satz.

Geprüft im Spieler, kalter Vorrat, Einstieg bei Sekunde 500: Blob am
`<track>`, zehn Einblendungen, die erste bei 0 bis 3 s mit „Deutsch bei
500 s".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 22:00:06 +02:00
ScarriffleandClaude Opus 5 339a6c07d0 Ein Client darf einen Stand fortschreiben, nicht zerstören
Ein Film stand bei 46 Minuten; nach einem Neuladen bot die Detailseite nur
noch „Abspielen" an. Gemessen am laufenden Server, was eine einzige Meldung
„Stelle 500 s, Laufzeit 60 s" anrichtet:

    Stelle        500 s  ->   60 s
    Laufzeit      600 s  ->   60 s
    gesehen        nein  ->   ja
    Weiterschauen    ja  ->   nein

Der Server beschnitt die Stelle auf die gemeldete Laufzeit, ersetzte die
richtige Laufzeit durch sie und setzte das Häkchen. Ein Film war damit weg.

Und 60 ist keine erfundene Zahl. Wer einen gerechneten Strom abspielt, misst
an einer Playlist, die noch wächst, und sieht anfangs nur einen Stummel.
Genau den meldete die App als Laufzeit — und sie meldet beim Öffnen
ausserdem die Zeit null, bevor sie überhaupt gesprungen ist.

Drei Stellen, dieselbe Regel:

- Server: die gemessene Laufzeit schlägt die gemeldete. Die Datei zuerst,
  nicht der gemerkte Wert — stünde dort schon ein Stummel, bliebe er sonst
  für immer stehen. So heilt sich ein verdorbener Eintrag von selbst.
- Server: eine gemeldete Null löscht keine gemerkte Stelle. Wer wirklich
  von vorn anfängt, verliert nichts: zehn Sekunden später meldet derselbe
  Spieler die 10, und die zählt. Zurückspulen bleibt unberührt.
- App: dieselbe Rangfolge bei der Laufzeit, und beim Öffnen wird die
  Stelle gemeldet, an der der Spieler einsteigen will, statt der Null.

Im Webspieler kam eine eigene Null dazu: bei Direktwiedergabe gibt der
Server `startSeconds: 0` zurück und der Browser springt selbst, sobald die
Metadaten da sind. Kommen sie nie, weil er mit der Datei nicht zurechtkommt,
blieb `filmzeit()` auf null — und der Rückfall aufs Umkodieren, der
ausdrücklich „an der Stelle weiter, an der es hing" meint, fing von vorn an.
Der ausstehende Sprung zählt jetzt mit.

Fünf neue Prüfungen halten die gemessenen Zahlen fest, damit die Regel nicht
wieder umkippt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 21:36:35 +02:00
ScarriffleandClaude Opus 5 51d1db4641 Zwei Uhren, und niemand hatte sie verglichen
Untertitel zeigten auf keiner Spur Text. Der Grund: die Zeitstempel einer
WebVTT-Spur zählen ab Dateianfang, der Strom zählt ab null. Wer bei Minute
30 einstieg, bekam jeden Satz dreissig Minuten in die Zukunft geschoben —
und weil ein Strom nur bis zum Filmende reicht, erschien überhaupt keiner
mehr.

Gemessen im Spieler, vorher: „Fortsetzen ab 8:37" geklickt, deutsche Spur
gewählt, Standanzeige 8:38 — angezeigt wurde der Satz von Sekunde 0.
Versatz 515 Sekunden, also genau der Einstiegspunkt.

Nachher, derselbe Weg: Standanzeige 9:02, angezeigt „Deutsch bei 540 s".

Verschoben wird auf dem Server, nicht im Browser: dort hiesse es, die Spur
selbst zu zerlegen und als VTTCue neu aufzubauen — ein eigener Parser für
ein Format, das der Browser schon kann. Hier ist es eine Zeilenersetzung,
und sie lässt sich prüfen. Was vor dem Einstieg endet, fällt samt Text weg;
was darüber hinwegreicht, fängt bei null an.

Zwei Kleinigkeiten lagen daneben und sind mit erledigt:

- `srclang` bekam „ger".slice(0, 2) und damit „ge" — kein gültiges Kürzel.
  Jetzt bleibt das Feld leer, wenn das Kürzel nicht zweistellig ist.
- Scheiterte das Laden einer Spur, geschah nichts Sichtbares. Jetzt sagt
  es der Spieler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 21:28:39 +02:00
ScarriffleandClaude Opus 5 57fff63516 Die Überschrift des Versatzes brach um
„TONVERSATZ — OHNE VERSATZ" am Stück passte nicht in das schmale Menü und
stand auf zwei Zeilen — und sah damit nach zwei Dingen aus. Jetzt steht
der Name links und der eingestellte Wert rechts in derselben Zeile, der
Wert hell und ohne Versalien, damit man sieht, welches davon die
Einstellung ist. Die Liste ist dafür 230 statt 180 Bildpunkte breit —
„Ton 3 s früher" muss daneben passen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:44:13 +02:00
ScarriffleandClaude Opus 5 2f7768136b Der Tonversatz ist ein Regler, abgetrennt unter den Spuren
Drei Anläufe, und du hattest jedes Mal recht.

**Er gehört ins Tonmenü** — aber sichtbar nicht zu den Spuren. Jetzt
trennt ihn ein Strich ab, darüber steht „TONVERSATZ — Ohne Versatz" als
Überschrift, darunter ein Regler. Drei Mittel, und jedes einzelne würde
schon sagen, dass das keine Sprache ist.

**Ein Regler statt Auswahlpunkten.** Feste Stufen hiessen: für jeden Wert
ein Eintrag, und die standen zwischen den Sprachen wie weitere Sprachen.
Gezogen wird von −3 bis +3 Sekunden in Hundertern; die Mitte ist als
Strich markiert, sonst trifft man die Null nur zufällig. Gerechnet wird
erst beim Loslassen, nicht beim Ziehen — jede Änderung baut den Strom neu
auf, und das tut man nicht zwanzigmal beim Hinschieben. Daneben
„Zurücksetzen", sobald etwas eingestellt ist.

**Und der Lautsprecher ist zurück.** Hier stand ein eigenes Zeichen, damit
er nicht zweimal in der Leiste auftaucht — links Stummschalten, rechts die
Tonspur. Die Wellenform erkannte aber niemand als „Ton". Zwei Lautsprecher
sind das kleinere Übel, zumal der linke beim Stummschalten
durchgestrichen wird und damit anders aussieht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:42:50 +02:00
ScarriffleandClaude Opus 5 dc9d1cea85 Meine Rundung war eine Klinge, und ich bin hineingelaufen
Du hast recht, und zwar doppelt.

**Der Versatz stand im falschen Menü.** „Später (+100 ms)" unter den
Tonspuren liest sich wie der Name einer Spur. Jetzt hat er einen eigenen
Knopf in der Leiste, mit eigenem Zeichen und festen Stufen von „Ton 1 s
früher" bis „Ton 1 s später", dazwischen „Ohne Versatz" — mit Haken, wie
bei der Qualität. Im Tonmenü stehen wieder nur Sprachen.

**Und das Schlimmere: die Rundung aufs Schlüsselbild hat es verschlechtert.**
Sie musste auf die Millisekunde stimmen, sonst nimmt ffmpeg das
Schlüsselbild davor. Gemessen an einer Datei mit Schlüsselbildern alle
fünf Sekunden:

    -ss 15.015  →  Bild ab 0,000 | Ton ab 0,004
    -ss 15.014  →  Bild ab 0,000 | Ton ab 4,983

Eine Millisekunde. Und die Zeile, die den Wert an ffmpeg gibt, rundete auf
drei Nachkommastellen — bei 23,976 Bildern je Sekunde liegen
Schlüsselbilder auf Werten wie 3,336667, und die überleben das nicht
zuverlässig. Damit konnte jede Fortsetzung schlechter werden als vorher.
Das war mein Fehler, eingebaut in E8GSRE.

**Der richtige Hebel ist `-noaccurate_seek`**, und er braucht gar keine
Genauigkeit. Damit schneidet ffmpeg auch den Ton nicht auf die Sekunde,
sondern lässt ihn beim selben Schlüsselbild anfangen wie das kopierte
Bild. Gemessen, dieselbe Datei, fünf verschiedene Sprungwerte quer durch
die Bildgruppe:

    15,014 → 0,015 | 17 → 0,004 | 19,9 → 0,004 | 12,5 → 0,015 | 3,2 → 0,011

Statt bis zu fünf Sekunden also überall unter zwanzig Millisekunden, und
zwar ohne dass irgendein Wert getroffen werden muss. Fünf Testfälle quer
durch die Bildgruppe halten das jetzt fest; vorher prüfte der Sprungtest
nur, ob überhaupt Segmente entstehen.

Die Schlüsselbildsuche bleibt — aber nur noch, um dem Spieler eine
ehrliche Uhr zu geben. Vergreift sie sich, geht die Uhr falsch; Bild und
Ton bleiben zusammen. Vorher hing die Ausrichtung daran, und das war der
schlechte Tausch.

Und die Sekundenangaben gehen jetzt mit sechs Nachkommastellen hinaus
statt mit drei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:24:21 +02:00
ScarriffleandClaude Opus 5 6ad4deb634 Fünf Sekunden Bild ohne Ton — jetzt weiss ich, woher
Deine Angabe war der Schlüssel: du hast **mitten im Film die Tonspur
umgeschaltet**. Dabei baut der Spieler den Strom an der aktuellen Stelle
neu auf, und damit greift der Sprung — den ich gestern nur mit
Spielzeugdateien gemessen hatte.

Also eine Datei gebaut, die einem Film gleicht: 23,976 Bilder je Sekunde,
Schlüsselbild alle fünf Sekunden, zwei Tonspuren, Deutsch als Stereo-AAC
und Englisch als 5.1 in EAC3. Gemessen, erster Zeitstempel je Spur:

| | Bild ab | Ton ab |
|---|---|---|
| Spur 2, ab Sekunde 0 | 0,021 | 0,000 |
| Spur 2, ab 17 (mitten in der Bildgruppe) | 0,000 | **1,964** |
| Spur 2, ab 15 (knapp vor dem Schlüsselbild) | 0,000 | **4,969** |

Fast fünf Sekunden Bild ohne Ton. Zueinander sind beide dabei richtig
getaktet — aber eine solche Lücke am Anfang darf jeder Spieler deuten, wie
er will, und hls.js schiebt den Ton dann nach vorn. Genau das hast du
gehört.

Die Rundung auf das Schlüsselbild, die seit E8GSRE drin ist, nimmt das
weg: dieselbe Datei, Sprung auf 17, gerundet auf 15,015 — **Bild ab 0,000,
Ton ab 0,004**. Du läufst noch auf 5YZ63F; deshalb steht im Protokoll auch
keine ffmpeg-Warnung, die gibt es erst ab E8GSRE.

**Die Tonspur lässt sich jetzt vor dem Start wählen.** Auf der
Detailseite steht neben „Abspielen" ein Knopf „Ton: Deutsch"; das Menü
zeigt alle Spuren der Datei. Gemerkt wird dabei die **Sprache**, nicht die
Spurnummer — welche Nummer Englisch trägt, ist von Film zu Film
verschieden, dass du Englisch willst, ist es nicht. Beim nächsten Film
steht die passende Spur damit schon da, und das Umschalten mitten im Film
entfällt. Wer doch im Spieler wechselt, ändert dieselbe Merkstelle.

**Und ein Fehler aus deinem Protokoll**, der nichts damit zu tun hat:
„UNIQUE constraint failed: Sehtage.UserId, Sehtage.Tag, Sehtage.MovieId".
Das ist ein Wettlauf im Sehprotokoll von vorgestern — der Spieler meldet
alle zehn Sekunden, ein Sprung meldet sofort, und zwei Meldungen können
sich überholen. Dann fanden beide keine Zeile, beide legten eine an. Jetzt
wird in der Datenbank hochgezählt statt im Speicher, und wer dabei zu spät
kommt, zählt beim zweiten Versuch mit. Mit Test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:09:46 +02:00
ScarriffleandClaude Opus 5 cf272f20aa Ton und Bild: ein Versatz zum Einstellen, und zwei gemessene Fehler weniger
Du meldest Ton, der drei, vier Sekunden neben dem Bild liegt.
**Nachstellen konnte ich es nicht** — und das sage ich lieber, als eine
Vermutung als Befund zu verkaufen.

Gemessen habe ich alle vier Wege, jeweils mit ffprobe am Ergebnis, erster
Zeitstempel je Spur:

| Weg | Bild ab | Ton ab |
|---|---|---|
| Umpacken, ab 0 | 0,021 | 0,000 |
| Umpacken, ab 12 | 0,000 | 1,979 |
| Umkodieren, ab 0 | 0,021 | 0,000 |
| Umkodieren, ab 12 | 0,021 | 0,000 |

Alle vier sind **zueinander richtig**. Auch eine Quelle mit einem echten
Loch in der Tonspur blieb ausgerichtet. Die naheliegende Erklärung — `-ss`
vor `-i` mit kopiertem Bild und neu gerechnetem Ton — ist damit widerlegt.

**Zwei echte Fehler hat dieselbe Messung aber gefunden**, und beide sehen
aus wie „um Sekunden daneben":

Zeile zwei der Tabelle. Beim Umpacken fängt das Bild beim Schlüsselbild
*vor* dem Sprung an, der Ton genau beim Sprung — bei Sekunde 12 also zwei
Sekunden Bild **ohne Ton** am Anfang. Zueinander stimmen beide, aber jeder
Spieler darf diese Lücke deuten, wie er will. Und der Server meldete dem
Spieler „beginnt bei 12", obwohl er bei 10 beginnt: die Uhr, die
Zeitleiste und der gespeicherte Stand lagen um diesen Unterschied daneben.

Beides geht weg, indem der Sprung **auf das Schlüsselbild gerundet** wird
und derselbe Wert zurückgemeldet wird. Dann fangen Bild und Ton zusammen
an, es gibt keine Stille davor, und die Uhr stimmt. Beim Umkodieren
entfällt das Runden — dort schneidet das neu gerechnete Bild genau.

**Und: ffmpeg durfte bisher nichts sagen.** `-loglevel error` versteckt
genau die Meldungen, die einen Tonversatz ankündigen, und der
Fehlerkanal wurde bei Rückgabewert 0 weggeworfen. Jetzt steht er auf
`warning`, und Warnungen landen auch bei einem geglückten Lauf im
Protokoll. Wenn es bei dir wieder auftritt, steht diesmal etwas da.

**Der Versatz zum Einstellen** sitzt im Tonmenü des Spielers: „Später"
und „Früher" in Hundertmillisekundenschritten, bis ±5 s, dazu
„Zurücksetzen". Gemerkt wird er **je Datei in diesem Browser** — ein
Versatz ist meist eine Eigenschaft der Datei, kann aber auch an der
Anlage liegen, und über Bluetooth stimmt etwas anderes als am Fernseher.

Verschoben wird serverseitig: die Datei wird ein zweites Mal mit
`-itsoffset` geöffnet und von dort nur der Ton genommen. Mit einer
Eingabe verschöbe sich beides gemeinsam. Der Preis: der Strom startet neu,
und eine durchgereichte Datei muss dafür umgepackt werden — ohne ffmpeg
lässt sich nichts verschieben.

Zwei neue Läufe halten das fest: dass Bild und Ton zusammen anfangen, und
dass ein halbe-Sekunde-Versatz den Ton um eine halbe Sekunde verschiebt
und das Bild gar nicht. Der Sprungtest daneben lief bisher genau durch
diesen Fall und sah nur nach, ob überhaupt Segmente entstehen.

Ein Test musste umgeschrieben werden: er stand auf „Einstiegspunkt ist
genau 2" und hielt damit die falsche Meldung fest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 19:37:21 +02:00
ScarriffleandClaude Opus 5 3fe0013be1 Die Auflösung wechselt den Text, statt einen Tooltip zu öffnen
Gestern wanderte der genaue Wert in den Tooltip. Ein Tooltip kommt
verzögert, legt sich über die Nachbarzeile und existiert am Finger gar
nicht. Jetzt steht in Ruhe „Full HD" da, und beim Überfahren tritt
„1080p · 1920 × 800" an dieselbe Stelle.

**Beide liegen übereinander, nicht nacheinander.** Das Feld ist so breit
wie der breitere der beiden Texte — sonst rückte beim Überfahren die halbe
Tabelle. Gerastert statt absolut gesetzt, damit die Zeilenhöhe von selbst
stimmt.

Ohne Zeiger gibt es kein Überfahren: dort steht das Genaue dauerhaft
daneben, statt unerreichbar zu sein.

Gemessen: in Ruhe „Full HD" bei Deckung 1 und „1080p · 1920 × 800" bei 0,
unter dem Zeiger umgekehrt — und kein `title` mehr am Element.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 18:32:21 +02:00
ScarriffleandClaude Opus 5 cdea8e3b0a Die Höhe des Aufmachers ist jetzt einstellbar
Vier Stufen unter Einstellungen → Verwaltung → Übersicht: niedrig, mittel,
hoch, sehr hoch. Mittel ist, was gestern fest verdrahtet war.

**Keine Pixelzahl.** Eine einzustellen hiesse, sie an jedem Bildschirm neu
einzustellen — der Aufmacher misst sich an der Fensterhöhe, nicht an einer
festen Grösse. Gemessen bei 800 Bildpunkten Fensterhöhe: 496, 576, 656,
736. Oben ist Schluss, bevor von der ersten Plakatreihe nichts mehr übrig
bleibt; sie soll zeigen, dass unten noch etwas kommt. Am Telefon bleibt es
flacher, dort wäre auch das weg.

**Sie reist mit der Systemauskunft mit**, nicht in einer eigenen Abfrage.
Die holt der Client ohnehin als Erstes, noch vor der Anmeldung; käme die
Höhe später, stünde der Aufmacher einen Augenblick in der Vorgabe und
spränge dann — genau das, was eine einstellbare Höhe vermeiden soll.
Gesetzt wird sie als Marke am `<html>`, das Regelwerk hängt vier Werte
daran.

Serverweit und nur für Admins, dieselbe Linie wie bei den
Wiedergaberegeln: eine Entscheidung des Gastgebers, nicht des Gastes.
Unbekanntes in der Spalte wird zur Vorgabe statt zu einem Fehler — eine
Startseite, die an einem alten Eintrag scheitert, wäre die schlechtere
Antwort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 18:32:10 +02:00
ScarriffleandClaude Opus 5 a7a4419626 Adminrecht geben und nehmen — und einen, dem es niemand nimmt
Ein Konto liess sich anlegen und danach nie wieder ändern: kein Weg zum
Administrator, keiner zurück, keiner zum Löschen. Mit Einladungslinks
füllte sich der Server also in genau eine Richtung.

**Warum es einen Besitzer braucht.** Sobald Administratoren einander das
Recht entziehen können, gibt es einen Zustand ohne jeden Administrator —
und aus dem führt kein Weg zurück ausser dem an der Datenbank. Ein Konto,
das ausgenommen ist, schliesst ihn aus. Es ist das erste, also das, mit
dem eingerichtet wurde; die Migration trägt das auf bestehenden Servern
nach, und weil die Kennungen zeitlich geordnet vergeben werden, ist der
Älteste schlicht der mit der kleinsten.

Weitergeben lässt er sich: vom Besitzer, an einen Administrator, mit
Rückfrage. Nur an einen Administrator, sonst wäre der Server einen Klick
von einem Besitzer entfernt, der die Verwaltung gar nicht sieht.

**Drei Türen sind zugemauert**, und jede hat ihren Test: dem Besitzer das
Recht nehmen, den Besitzer löschen, sich selbst das eigene Recht nehmen.
Das letzte ist die unauffälligste — wer sich im Eifer entmachtet, steht
vor einer Verwaltung, die er nicht mehr öffnen kann, und das Zurücknehmen
braucht genau die.

Gemessen und als Test festgehalten: ein frisch vergebenes Adminrecht gilt
**sofort**, ohne neue Anmeldung. Die Rolle wird je Anfrage aus der
Datenbank gelesen und nicht aus dem Token — ich hatte das Gegenteil
befürchtet.

Löschen nimmt mit, was am Konto hing: Geräte, Sehstände, Sehprotokoll,
Meldungen, offene Einladungen. Alles hängt mit `Cascade` daran. Zahlen
über jemanden zu führen, den es nicht mehr gibt, wäre die schlechtere
Hälfte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 18:31:05 +02:00
ScarriffleandClaude Opus 5 d5e21d663f Einladen, statt sich für jemanden ein Passwort auszudenken
Ein Konto von Hand anzulegen heisst: sich für einen anderen ein Passwort
ausdenken und es ihm durchsagen — über denselben Kanal, über den auch ein
Link gegangen wäre. Jetzt gibt es den Link. Wer ihn öffnet, wählt Namen
und Passwort selbst und ist danach angemeldet.

Gültigkeit und Anzahl wählst du beim Erstellen: einen Tag bis einen Monat,
einmal bis unbegrenzt. **Eine Rolle wählst du nicht** — wer eingeladen
wird, ist gewöhnlicher Benutzer. Ein Feld, über das ein Link Adminrechte
verschenken könnte, gibt es gar nicht, und damit auch keinen Weg, es
versehentlich zu setzen.

**Der Link steht genau einmal da.** Gespeichert ist nur sein
Fingerabdruck, wie beim Gerätetoken; aus der Liste lässt sich keiner mehr
bauen. Eine verlorene Einladung wird deshalb nicht wiedergefunden, sondern
neu gemacht. Und gebaut wird die Adresse im Browser: der Server weiss
nicht, unter welchem Namen er von aussen erreichbar ist, und hinter einem
Reverse Proxy rät er falsch.

**Das Rennen beim Einlösen.** Zwei Leute, die gleichzeitig denselben
einmaligen Link öffnen, läsen beide „null benutzt" und schrieben beide
„eins" — und hätten beide ein Konto. Der Zähler geht deshalb mit einer
Bedingung hoch, die die Gültigkeit noch einmal prüft; wer dabei keine
Zeile trifft, war der Zweite. Das Ganze in einer Transaktion, damit ein
Tippfehler im Namen den Link nicht verbrennt. Beides steht als Test da —
zwei gleichzeitige Einlösungen ergeben genau ein Konto.

**Angelegt wird, angemeldet wird danach gewöhnlich.** Derselbe Zweischritt
wie beim Einrichten, und der Grund ist nicht Bequemlichkeit: die Anmeldung
setzt den Sitzungskeks, und ohne den lädt kein Plakat — Bilder weisen sich
über ihn aus, nicht über das Token. Gemessen beim Durchspielen: nach dem
Einlösen kommt ein Bild mit 200 zurück.

**Die Weiche liegt im Hochfahren, nicht im Router.** Der Router läuft nie,
wenn niemand angemeldet ist — ein Ausgeloggter mit Einladungslink sähe
sonst die Anmeldekarte und landete nach dem Anmelden auf der Startseite,
ohne je erfahren zu haben, wozu er eingeladen war. Nach dem Einrichten,
damit ein nagelneuer Server zuerst seinen Administrator bekommt.

Wer schon angemeldet ist, bekommt kein Formular, sondern den Hinweis und
zwei Wege. Und die Gründe einer Ablehnung sind verschieden benannt:
abgelaufen, zurückgezogen, schon benutzt. Verschwiegener wäre ein
einheitliches „gilt nicht" — und dann fragt der Eingeladene nach, und der
Gastgeber rät mit.

Nebenbei korrigiert: die Erkundung meinte, dem Benutzernamen fehle ein
eindeutiger Index. Er hat einen, seit der ersten Migration, sogar mit
`NOCASE`. Das Namensrennen war also nie offen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 17:23:25 +02:00
ScarriffleandClaude Opus 5 c65356c16f 4K, Full HD, HD, SD — die Zahl steht beim Überfahren
„1080p" ist eine Angabe für den, der eine Stufe einstellt. Wer sich fragt,
ob es sich lohnt, dafür den Fernseher anzumachen, liest „Full HD". Die
Zeile sagt jetzt das Wort, der Tooltip den genauen Wert und die gemessene
Grösse: `1080p · 1920 × 800`.

**Es gab zwei Leitern, und sie waren sich uneinig.** `Benennung.AusHoehe`
machte aus 2160 ein „2160p", `FileNames.FassungAusHoehe` ein „4K" — auf
derselben Detailseite stand beides übereinander, ein paar Zeilen
auseinander. Die zweite ist weg; sie hatte ausserdem nur die Höhe und
vergab sich damit beim Kinoformat.

Gemessen an zwei echten Dateien: 1920×800 heisst „Full HD" (nur an der
Höhe gemessen hiesse es „HD", und daneben stünde ein 1280×720 mit
demselben Wort), 720×576 heisst „SD".

**1440p fällt unter Full HD.** „QHD" kennt kaum jemand, und ein fünftes
Wort für etwas zwischen zwei bekannten hilft beim Entscheiden nicht.
„8K" steht nur da, damit eine 8K-Datei nicht als 4K gemeldet wird.

**Der Client zerlegt nichts mehr.** Er las die Stufe bisher mit
`display.split(' · ')[0]` heraus — richtig, solange vor der Auflösung nie
etwas anderes steht. Jetzt kommen Wort und genauer Wert als eigene Felder.

**Die Fassung wird nicht mehr gespeichert, sondern gerechnet.** Sie stand
in der Spalte und war damit das, was beim letzten Scan galt: ein neues
Wort hätte keine bestehende Zeile erreicht. Jetzt hält die Spalte nur
noch, was im Dateinamen stand („Mobil"), und die Messung kommt frisch
dazu. Eine Migration räumt die alten gerechneten Werte weg.

**Nicht angefasst:** was eine *gewählte* Stufe ist statt einer gemessenen
— die Umkodierleiter, der Qualitätswähler im Spieler, die Stufen in den
Apps. Wer eine Stufe wählt, wählt eine Zahl.

Die Apps rechnen sich das Wort ohnehin selbst aus und sagen seit jeher
„4K" und „Full HD"; der Server ist ihnen jetzt angeglichen, nicht
umgekehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 17:13:23 +02:00
ScarriffleandClaude Opus 5 168fcdfb4c Der Aufmacher war dir zu niedrig
`min(62vh, 560px)` → `min(72vh, 640px)`. Bei 800 Bildpunkten Fensterhöhe
gemessen: 576 statt 496, und die erste Plakatreihe lugt mit ihrem Beginn
bei 648 weiterhin über den Falz — sie soll ja zeigen, dass unten noch
etwas kommt.

Am Telefon bleibt es bei `min(52vh, 420px)`: dort ist der Aufmacher im
Verhältnis zum Bildschirm ohnehin höher.

Ein Satz musste mit, und der ist tragend: die Begründung, warum das
Aufklappen der Inhaltsangabe nicht schaukelt, nennt die feste Höhe
wörtlich. Die Zahl wird falsch, das Argument bleibt richtig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 17:13:06 +02:00
ScarriffleandClaude Opus 5 8e9da1355c Der Server schrieb deutsche Sätze an eine Stelle, die niemand liest
„Die Anfrage war so nicht zu gebrauchen" — das ist der Ersatzsatz des
Browsers für eine Ablehnung ohne Begründung. Dabei stand die Begründung
da: „Benutzername darf nicht leer sein, das Passwort braucht mindestens 8
Zeichen." Nur eben in `errors`, und beide Clients lesen `detail`.

Zwei Formen waren betroffen. `Results.ValidationProblem` legt den Satz
unter `errors` ab, `Results.BadRequest(new { fehler = … })` unter
`fehler` — in beiden Fällen kommt er nirgends an. Zusammen 36 Stellen,
darunter jede Zeile, die ich gestern für die Meldungen geschrieben habe.
Dass die Clients `title` und `errors` auslassen, ist dabei richtig: dort
stehen sonst die englischen Prüftexte des Rahmenwerks, und „Not Found" ist
keine Auskunft.

Jetzt eine Form, hinter `Einwand.Bitte` / `.Fehlt` / `.Belegt`. Dass es
drei benannte Türen sind statt eines Aufrufs mit Zahl, hat einen Grund:
man sieht am Wort, ob der Server um eine bessere Eingabe bittet, etwas
nicht findet oder auf etwas Bestehendes stösst.

**Auffallen konnte es nicht**, und das ist der eigentliche Fund. Die
vorhandenen Tests prüften den Rumpf als Zeichenkette — der Satz *stand* ja
drin, nur im falschen Feld. Der neue Test prüft deshalb, in welchem Feld,
und ein Wächter am Quelltext schlägt an, wenn eine der beiden alten Formen
zurückkommt. Er hat beim ersten Lauf gleich eine Stelle gefunden, die mein
Suchmuster übersehen hatte.

Dazu drei Kleinigkeiten, die beim Lesen auffielen:

**Passwörter wurden getrimmt.** Der Dialog schnitt jedem Feld die Ränder
ab, auch dem Passwort — `login.ts` sendet es ungeschoren. Ein Leerzeichen
am Ende, und das angelegte Konto war für immer unbetretbar.

**Beim Einrichten stapelten sich die Fehlerzeilen**, weil `.error`
weggeräumt wurde und `.fehler` entsteht. Drei Fehlversuche, drei rote
Kästen.

**Der Benutzerdialog versprach acht Zeichen und prüfte nichts.** Jetzt
sagt er es selbst, ohne den Umweg über die Leitung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 17:12:57 +02:00
ScarriffleandClaude Opus 5 7cd48f5528 Wer etwas sieht, kann es jetzt auch sagen
Wer einen Server für andere betreibt, erfährt von Fehlern zuletzt: dass
die deutsche Tonspur nach einer halben Stunde aussetzt, merkt der, der den
Film sieht, und nicht der, der ihn eingeräumt hat. Bisher gab es dafür
keinen Weg ausser dem Zuruf im Wohnzimmer.

Gemeldet wird an einem Film, einer Serie, einer einzelnen Staffel und
einer einzelnen Folge: Art (Bild, Ton, Untertitel, Angaben, Sonstiges),
eine freiwillige Stelle wie 0:34:12 und der Kommentar. Pflicht ist nur der
Kommentar — die beiden anderen machen aus einer Liste von Sätzen etwas,
das man sortieren und nachstellen kann.

**Eine Staffel gibt es als Zeile nicht**, und das prägt den Entwurf. Sie
ist Serie plus Nummer, so wie überall sonst in diesem Server; die Meldung
trägt deshalb drei Fremdschlüssel und daneben eine Staffelnummer. Dass die
Nummer auf eine Staffel zeigt, die es gibt, prüft der Endpunkt beim
Schreiben — die Datenbank kann es nicht, es gibt nichts, worauf sie zeigen
könnte.

**Die Rechteprüfung steht nirgends.** Wer einen Titel nicht sehen darf,
findet ihn beim Melden nicht — das erledigt der Freigabefilter, ohne ein
einziges `Where`. Elegant und genau deshalb leicht zu zerstören, darum
steht dafür ein Test da.

**Die eigene Liste überlebt fremde Entscheidungen.** Titel werden
nachgeschlagen, nicht verbunden: zieht ein Administrator einen Titel
zurück, nachdem jemand ihn gemeldet hat, bleibt die Meldung beim Melder
stehen — nur ohne Namen. Ein Join hätte sie verschwinden lassen, und das
wäre jemandes eigene Eingabe, weggenommen wegen einer fremden Entscheidung.
`IgnoreQueryFilters` wäre die bequeme Abkürzung und gäbe den Namen eines
unveröffentlichten Titels über einen Weg heraus, an den beim nächsten Mal
niemand denkt.

**Erledigt heisst abgehakt, nicht gelöscht**, als Zeitpunkt und nicht als
Haken — wie `PublishAt` und `ChangedSince`. Daneben steht „Wieder öffnen";
ein Fehlgriff wäre sonst endgültig. Das Archiv wird erst auf Zuruf geholt:
es wächst nur, und für den seltenen Blick dauernd zu zahlen lohnt nicht.

**Der Abschnitt steht neben der Prüfliste, nicht darin.** Die versteckt
sich, sobald nichts auf Freigabe wartet — das ist der Normalfall, und die
Meldungen verschwänden mit ihr. Umgekehrt bleibt der Meldungsabschnitt
stehen, auch wenn nichts drinsteht: ein Kanal, der sich versteckt, solange
ihn niemand benutzt hat, bringt niemandem bei, dass es ihn gibt.

Die Zahl an der Leiste holt sich nur, wer sie nicht ohnehin hat: steht „Zu
prüfen" offen, meldet der Abschnitt sie selbst und nach jedem Abhaken.

Nebenbei zwei Dinge am Dialog, die hier auffielen. Es gab **kein
mehrzeiliges Feld** — und das Einsammeln der Werte hätte ein `<textarea>`
still verschluckt: es ist kein `HTMLInputElement` und wäre ins Auffangnetz
für Auswahlfelder gelaufen, also als `undefined` hinausgegangen. Der
Server hätte „ohne Beschreibung" geantwortet, auf einem Dialog, in dem
sichtbar etwas steht. Und `dialog()` meldete sich als einziger nicht beim
Seitenwechsel an: ein Dialog von der Detailseite überlebte den
Zurück-Knopf und stand danach über der Mediathek.

Neun Testfälle auf dem Server, zwölf Läufe. Für den Web-Client gibt es in
diesem Projekt keine Tests; dort ist von Hand geprüft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 22:18:14 +02:00
ScarriffleandClaude Opus 5 02978e482c Die Knöpfe bleiben, die Inhaltsangabe klappt auf
Ich hatte „die Beschreibung nur beim Hovern" gelesen und daraufhin
Beschreibung *und* Knöpfe ausgeblendet. Die Knöpfe gehören immer dahin: in
Ruhe steht oben, was läuft und was man damit tun kann.

Die Inhaltsangabe dagegen steht erst da, wenn man es will. Zwei Sekunden
auf dem Titel stehenbleiben, dann fährt er hoch und darunter klappt sie
auf; wer nur vorbeifährt, sieht nichts. Gibt es keine, passiert gar
nichts — der Absatz fällt dann ganz aus dem Fluss.

**Angefasst wird am Titel, losgelassen am ganzen Aufmacher.** Das ist die
ganze Vorsicht an dieser Stelle. Beim Aufklappen wächst der Block nach
oben, der Titel rückt um 77 Bildpunkte weg und steht nicht mehr unter dem
Zeiger. Hinge das Zuklappen an seinem `:hover`, wäre das die erste Hälfte
einer Schaukel. Der Aufmacher selbst kann das nicht: gemessen, offen wie
zu, 476 px hoch ab 48 — auf den Bildpunkt derselbe Kasten.

**Die Höhe wird gemessen, nicht geraten.** Nach `auto` lässt sich nicht
überblenden, und eine feste Obergrenze wäre bei einer einzeiligen Angabe
zu drei Vierteln Leerlauf. Gemessen wird bei jedem Folienwechsel und bei
jeder Fensteränderung; `scrollHeight` ginge nicht, der meldet bei einem
`-webkit-box` die ungeklammerte Höhe.

**Der Rasterabstand wandert mit.** Gemessen: zugeklappt stehen zwischen
Eckdaten und Knopfzeile 16 px — mit Inhaltsangabe wie ohne, und genau so
viele wie vorher. Aufgeklappt sind es 12 über und 16 unter dem Text, und
die Knopfzeile steht still (Unterkante 492 in beiden Zuständen).

Nebenbei kommt das Abbruchsignal endlich an: `hero()` nahm eines
entgegen, aber `main.ts` baute die Startseite ohne. Sie wird gemerkt — ihr
Neun-Sekunden-Takt lief also weiter, während man längst woanders war.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 22:05:44 +02:00
ScarriffleandClaude Opus 5 ac9faacefb Im Aufmacher steht in Ruhe nur, was läuft
Inhaltsangabe und Knöpfe blenden sich beim Überfahren ein und wieder aus.
In Ruhe bleibt oben nur, *was* läuft — Schriftzug oder Name, Jahr,
Laufzeit, Freigabe. Was man damit tun kann und worum es geht, kommt auf
Zuruf. So ist das Bild in Ruhe ein Bild und nicht eine Wand aus Text
darüber.

**Langsamer herein als hinaus**, 340 gegen 220 Millisekunden: das
Erscheinen soll man sehen, das Verschwinden nicht abwarten müssen. Die
Pfeile gehen denselben Takt mit; sie standen auf 150 ms und blitzten
dagegen auf.

**Der Platz bleibt reserviert.** Mit `display: none` spränge beim ersten
Überfahren der ganze Block nach oben; geschaltet wird nur die Deckung.
Gemessen an derselben Folie: 349,2 px hoch mit Deckung 0, 349,2 px mit
Deckung 1.

**Der Terminhinweis bleibt stehen.** „Ab Di., 6. Okt., 18:00 Uhr" ist
keine Handlung, sondern die Auskunft, warum hier nichts zu drücken ist —
die darf nicht erst auf Zuruf kommen. Darum trifft die Regel `.btn` und
nicht die ganze Knopfzeile.

**Der Fokus zählt wie der Zeiger.** Ein Knopf, der unsichtbar den Fokus
bekommt, ist eine Sackgasse. Gemessen, mit abgeschalteten Übergängen:
in Ruhe alles auf 0; Fokus im Aufmacher — Text 1, beide Knöpfe 1, beide
Pfeile 0,72; nach dem Verlassen wieder 0.

`hover: hover` wie bei den Pfeilen: ohne Zeiger gäbe es kein Überfahren,
und „Abspielen" wäre nicht mehr erreichbar. Dort steht alles wie immer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 21:15:39 +02:00
ScarriffleandClaude Opus 5 3a0afa918d Die Pfeile im Aufmacher warten, bis man hinzeigt
Sie liegen über dem Bild und damit über allem, was dort steht. Bei einem
einzeiligen Titel geht das gut. „Pirates of the Caribbean — Salazars
Rache" bricht um, die erste Zeile rückt nach oben — und der linke Pfeil
sitzt auf dem ersten Buchstaben.

Ständig sichtbar ist er dort ein Fleck, auf Zuruf ein Knopf. Also
erscheinen beide erst beim Überfahren des Aufmachers, in 150 ms, und
verschwinden wieder.

`hover: hover` ist dabei die ganze Vorsicht: auf einem Tablett gibt es
kein Überfahren, und ein Knopf, der nur dabei erscheint, wäre dort
schlicht nicht erreichbar. Dort bleibt alles, wie es war — unter 860
Bildpunkten gibt es die Pfeile ohnehin nicht, da wird gewischt.

Gemessen: in Ruhe Deckung 0, kein Zeigerziel. Zeiger über dem Aufmacher:
0,72 und anklickbar. Zeiger auf dem Pfeil: 1, dunklerer Grund, und der
Klick blättert tatsächlich weiter (Tastatur-Tabulator vom Knopf „Mehr"
aus: Deckung 1, `:focus-visible` greift, Umriss 2 px) — `pointer-events:
none` hält nur den Zeiger ab, nicht den Fokus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 21:03:30 +02:00
ScarriffleandClaude Opus 5 ffc4bbe597 Ein Viertel des Hintergrundbildes lag hinter der Seitenleiste
Es stand auf `inset: 0` und lag damit auch hinter Seiten- und Kopfleiste —
beide undurchsichtig. Bei 1280×800 gemessen: 18,8 % der Breite, 7 % der
Höhe, zusammen 24,4 % der Bildfläche, die niemand je zu sehen bekam.

Solange alles weichgezeichnet war, fiel das nicht auf; ein verwischter
Rand sieht wie jeder andere aus. Scharf beginnt der Inhalt mitten im
Motiv — bei den Bergrettern mit einem angeschnittenen Kopf direkt an der
Kante der Seitenleiste.

Jetzt sitzt das Bild in dem Feld, in dem es auch zu sehen ist, und wird
darin ganz zugeschnitten: links die Seitenleistenbreite, oben die
Kopfleistenhöhe. Der Kontrast bleibt, wo er war — 3,95∶1 scharf statt
3,97∶1, derselbe Zuschnitt in einem anderen Feld.

Am Telefon gibt es keine Seitenleiste; dort nimmt die Navigationszeile
oben noch einen Streifen weg, solange man ganz oben steht, und ist nach
dem ersten Bildlauf weg.

Die beiden älteren Begründungen über der Regel standen noch da und sagten
inzwischen das Gegenteil — die eine erklärte, warum das Bild über die
volle Breite gehört. Sie sind zu einer zusammengefasst, die den ganzen Weg
erzählt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 20:16:17 +02:00
ScarriffleandClaude Opus 5 3c7c0d8799 Der Hintergrund wird erst weich, wenn etwas darüberläuft
Er war es immer: 28 Bildpunkte Weichzeichnung über die ganze Detailseite,
damit der Text darüber lesbar bleibt. Oben steht aber gar nichts darüber —
dort ist das Bild das Beste, was die Seite zu bieten hat, und es lag als
Farbschlieren da. Unscharf gehört es erst, wenn wirklich etwas darüber
hinwegläuft: die Folgen, die Besetzung, die Medienangaben.

Jetzt folgt die Weichzeichnung dem Bildlauf, über etwa siebzig Prozent
einer Bildschirmhöhe und quadratisch statt geradlinig — die ersten
Bildpunkte sollen nichts tun, sonst wäre „scharf, solange nichts
darüberliegt" nach einem Fingerbreit schon dahin. Gerundet auf ganze
Bildpunkte, weil jeder neue Wert eine bildschirmfüllende Fläche neu
zeichnen lässt; über die ganze Strecke sind das 28 Male. Gemessen über 58
Bilder beim Hinunterrollen: 8,3 ms im Mittel, kein Bild über 9,2 ms.

**Und die Deckung geht mit, das ist der unscheinbare Teil.** Eine
Weichzeichnung verrührt die hellen Stellen mit den dunklen; ein scharfes
Bild hat die weisse Stelle genau dort, wo die Zeile steht. Gemessen an der
dunklen Vorlage von „Dark": vorher 4,07∶1, scharf bei derselben Deckung
nur noch 2,44∶1. An einer hellen — Hubschrauber vor blauem Himmel —
vorher 2,75∶1. Mit 0,28 scharf und 0,45 weich stehen beide wieder bei rund
4,0∶1, die helle sogar besser als zuvor.

Der Zuhörer räumt sich selbst ab: die Detailseite wird nicht gemerkt, und
für ungemerkte Ansichten ruft `einhaengen` kein Abbruchsignal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 19:53:25 +02:00
ScarriffleandClaude Opus 5 6dc7be9822 Ein eigenes Bild bringt auch seinen Ausschnitt mit
Hochladen konnte man Plakat, Hintergrund und Schriftzug schon lange — der
Knopf stand da, der Endpunkt nahm an. Nur ankommen tat nichts: die Ablage
baute einen Schlüssel, den sie selbst nicht wiederfand (heute früh
repariert). Was blieb, waren drei Lücken, die man erst sieht, wenn der Weg
überhaupt funktioniert.

**Der Ausschnitt.** Ein Plakat steht hochkant im Verhältnis 2∶3, ein
Hintergrund liegt quer. Was man aussucht, passt selten von selbst — und
den Rest beschnitt bisher der Browser beim Anzeigen: mittig. Jetzt wählt
man selbst, zieht und vergrössert, und hochgeladen wird genau dieser
Ausschnitt: 600×900 beim Plakat, 1920×1080 beim Hintergrund. Gerechnet
wird im Browser; das spart die Leitung und dem Server eine Bildbibliothek.

**Der Schriftzug nicht.** Er ist ein PNG mit durchsichtigem Grund. Durch
dieselbe Mühle geschickt würde aus der Durchsicht ein weisses Rechteck —
ein Aufkleber über dem Bild. Er geht unverändert hinaus, Byte für Byte.

**Die Kachel merkte nichts.** Nach dem Hochladen fehlte der Anstoss nach
aussen; die Oberfläche zeigte weiter das alte Bild, bis man sie neu lud.

**Und zurück ging es gar nicht.** „Von Hand gesetzt" heisst auch: beim
Auffrischen bleibt es stehen. Ohne Rückweg wäre ein Fehlgriff ein
Dauerzustand — jetzt steht „Wieder automatisch" daneben, sobald etwas
festgesetzt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 19:41:04 +02:00
ScarriffleandClaude Opus 5 327e2e9195 Einstellungen hat jeder, Verwaltung nicht
Die Seite hiess „Verwaltung" und war für alle anderen gar nicht
erreichbar — „Meine Sprache" lag dadurch hinter einer Tür, die den meisten
verschlossen ist. Jetzt heisst sie **Einstellungen**, steht jedem offen und
hat zwei beschriftete Gruppen: oben, was einem selbst gehört (Mein Konto,
Geschaut), darunter — nur für Administratoren — **Verwaltung**. Die
Überschrift ist hier die halbe Aussage: sie sagt ohne weiteren Satz, wem
der untere Teil gehört.

In der Hauptleiste steht der Eintrag jetzt unten, ohne Gruppenüberschrift.
Eine Überschrift über einem einzigen Eintrag ist eine Überschrift über
nichts, und am Fuss sucht man Einstellungen ohnehin. `#/profil` und
`#/verwaltung/…` leiten weiter; ein Lesezeichen merkt davon nichts.

**Ein kurzes Menü am Bild.** Erst meldete der Klick ab — ohne Rückfrage.
Dann führte er auf die eigene Seite, und zum Abmelden musste man erst
dorthin. Beides sind zwei Punkte; ein Menü mit zwei Punkten ist ehrlicher
als eine Tür, hinter der eine zweite liegt.

**Und das Profilbild lässt sich zuschneiden.** Ein Profilbild ist rund,
was hochgeladen wird selten quadratisch — den Ausschnitt dem Browser zu
überlassen hiesse: mittig, und das trifft bei einem Porträt den Hals. Der
Dialog zeigt den runden Ausschnitt, man zieht und vergrössert, und
hochgeladen wird genau das: ein Quadrat von 512 Pixeln. Gemessen, es kommt
auch so an.

**Dazu die Spielerleiste.** Beide Knopfgruppen durften schrumpfen, ihre
Knöpfe aber nicht — wird es eng, steht der äusserste halb hinter dem
Fensterrand. Rechts sind es nur Knöpfe, da gibt es nichts nachzugeben;
links schon. Dazu seitlich `env(safe-area-inset-*)`, damit auf einem Gerät
mit runden Ecken nichts darunter liegt. Gemessen: vorher riss es ab 420
Pixeln, jetzt bis 320 nicht mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 18:24:44 +02:00
ScarriffleandClaude Opus 5 9c2e2b5f47 „Kenne ich" heisst nicht „kann ich diese Datei"
Der Spieler schickte sein Profil aus `canPlayType` — und das ist eine
Auskunft des `<video>`-Elements. Ein Strom läuft aber nicht durch das
Element, sondern durch den MSE-Puffer, und die beiden sind sich nicht
einig.

Gemessen in Chromium:

| | canPlayType | MediaSource.isTypeSupported |
|---|---|---|
| `audio/flac` | probably | **false** |
| `audio/mp4; codecs="mp3"` | probably | **false** |

Der Server glaubte dem Profil, packte die Datei pflichtschuldig um und
liess die Spur unberührt — und der Browser stand vor etwas, das er nach
eigener Auskunft können müsste. Heraus kam „Dieser Browser kann die
Spuren dieser Datei nicht öffnen".

Jetzt zählt ein Codec nur, wenn **beide** Auskünfte ihn bestätigen; welcher
der beiden Wege es wird, entscheidet der Server erst danach. Und geprüft
wird jeder Typ in der Form, in der er wirklich ankommt — `audio/mp4` und
nicht `audio/flac`: genau dieser Unterschied entschied bei FLAC zwischen
„geht" und „geht nicht". Gemessen: MP3 fällt dadurch aus dem Profil, der
Server rechnet es um, statt es durchzureichen.

**Und wenn doch etwas durchrutscht, wird gerechnet statt gemeldet.**
Scheitert die Wiedergabe mit einem Codec- oder Dekodierfehler, baut der
Spieler den Strom genau einmal neu auf — mit `transcode: true`, an
derselben Stelle. Der Satz „über das Zahnrad lässt sich eine Fassung
wählen, die er versteht" ist weg: wer das liest, weiss weder welche, noch
warum der Server sie nicht gleich gegeben hat.

Lief schon ein gerechneter Strom, unterbleibt der zweite Versuch — dann
liegt es nicht am Codec, und dreissig Sekunden Warten auf dasselbe sind
keine Hilfe.

**Nicht geprüft:** dass der Rückfall im Betrieb auslöst. Jede Datei im
Prüfbestand spielt; ein echtes Scheitern liess sich hier nicht
herbeiführen. Gemessen ist der Serverteil (`remux` wird zu `transcode`)
und das neue Profil.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 17:44:11 +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 579a49b73b Der Server wusste, was du zuletzt gesehen hast — und sonst nichts
Für „wie viel habe ich letzte Woche geschaut" gab es keine Daten.
`UserItemState` ist eine Zeile je Titel: der letzte Stand. Wer einen Film
zweimal sieht, überschreibt den ersten Durchgang; wann etwas lief, wusste
der Server nur fürs jeweils letzte Mal. Nachträglich erfinden lässt sich
das nicht.

Jetzt gibt es ein Sehprotokoll: **eine Zeile je Nutzer, Tag und Titel.**
Keine Sitzungen — die brauchen ein Ende, und das kommt oft nie an, wenn
der Browser zugemacht oder das Telefon zugeklappt wird. Tagesbündel haben
diesen Lebenslauf nicht, es wird nur addiert.

**Gezählt wird, was wirklich lief.** Der Zuwachs zwischen zwei Meldungen,
gedeckelt auf die Zeit, die dazwischen verstrichen ist (mal zwei als
Toleranz für doppelte Geschwindigkeit und ausgefallene Meldungen). Sonst
ergäbe ein Sprung von Minute 5 auf Minute 90 anderthalb Stunden Sehzeit in
zehn Sekunden. Gemessen an fünf Meldungen im Abstand von elf Sekunden:
88 Sekunden statt 190.

Dabei fiel ein zweiter Fall auf: war **gar keine** Zeit verstrichen, griff
kein Deckel statt eines engen — eine frisch angelegte Zeile hätte ihre
Anfangsstelle voll mitgezählt. Wer einen Film öffnet und sofort auf Minute
fünf springt, hat keine fünf Minuten gesehen.

Im Profil stehen drei Zahlen, und jede sagt, woher sie kommt: die
aufgezeichnete Zeit fängt heute an, die Zahl gesehener Titel steht seit
jeher fest. Dazu eine Wochenkurve, die häufigsten Genres und was zuletzt
lief. **Jeder sieht nur sich selbst, auch ein Administrator** — es gibt
keinen Weg, ein fremdes Konto zu meinen.

Zwei Dinge auf dem Weg dorthin:

**Eine stellbare Uhr für die Tests.** Zwei Minuten Film lassen sich nicht
in fünf Millisekunden sehen, und das ist richtig so — prüfen liess es sich
nur, indem die Zeit vergeht, ohne sie zu verbringen.

**Ein Namenskonflikt im CSS.** Die leeren Wochenbalken trugen die Klasse
`leer`, und die gehört längst der Fläche „hier ist nichts" — mitsamt
24 Pixeln Polster. Jeder leere Balken war dadurch 48 Pixel hoch, egal
welche Höhe dastand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 17:20:01 +02:00
ScarriffleandClaude Opus 5 d8a1633cf4 Das eigene Bild führte aus dem Server heraus
Ein Klick auf den Buchstabenkreis meldete ab. Keine Rückfrage, kein Menü,
kein Weg zurück — hinter einer Geste, die überall sonst „mein Konto"
heisst. Jetzt führt er auf `#/profil`; Abmelden steht dort als Knopf, mit
Nachfrage.

Auf der Seite: Bild wählen, tauschen, entfernen. Dazu „Meine Sprache" —
**umgezogen, nicht kopiert.** Sie stand mitten in der Verwaltung, und die
zeigt der Client nur Administratoren; der Endpunkt erlaubte sie längst
jedem. Wer kein Admin war, konnte seine eigene Sprache nirgends ändern.
Der Warnhinweis „weicht ab" bleibt in der Verwaltung, denn dort entsteht
die Verwirrung — er verweist jetzt aufs Profil.

**Vorher musste ein Fehler weg, der seit Monaten dasteht.**
`ImageCache.StoreStreamAsync` baute den Schlüssel aus 32 Hexzeichen plus
Dateiendung; `PfadFür` verlangt genau 64 Hexzeichen und nichts sonst. Es
gab keinen Wert, den beide gelten liessen: **jeder Bildupload gab `null`
zurück und endete in einem 502.** Kein Test fragte je danach — jetzt vier.
Damit geht nebenbei auch „eigenes Bild hochladen" für Filme und Serien
wieder.

Weil der Schlüssel keine Endung mehr trägt, kommt der Medientyp beim
Ausliefern aus den ersten Bytes der Datei. Vorher ging alles als
`image/jpeg` hinaus — bei TMDB stimmte das, bei einem hochgeladenen PNG
nicht.

**Und die gemerkte Sitzung war eine Kopie, die nie nachzog.** Sie wurde
bei der Anmeldung geschrieben und danach nie wieder. Wer sein Bild am
Telefon setzt und den Browser am Schreibtisch öffnet, fand dort den alten
Buchstaben — bis zum nächsten Anmelden. Gemessen und behoben: beim
Hochfahren wird das Konto abgeglichen, und das Profil meldet Änderungen
an die Kopfleiste, damit sie ohne Neuladen nachzieht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 17:07:40 +02:00
ScarriffleandClaude Opus 5 9622da0284 Eine Sekunde ist kein Anfang
Drei Zahlen standen als Konstante im Quelltext und bestimmten, wie sich
der Server anfühlt. Jetzt stehen sie in der Verwaltung, unter
„Wiedergabe".

**Weiterschauen ab.** Gemessen: `BelongsInContinueWatching` verlangte nur
`Position.Ticks > 0` — ein Tick genügte. Wer im Aufmacher eine Sekunde
hineinschaute, fand den Titel danach in „Fortsetzen". Jetzt gibt es eine
Untergrenze, voreingestellt auf 1½ Minuten.

Was sich dabei **nicht** ändert: die Stelle wird weiter gemerkt. Das ist
der Unterschied zu Jellyfin, um dessentwillen es diesen Server gibt — die
Schwelle entscheidet, ob der Titel in der Reihe steht, nicht ob man ihn
wiederfindet. Nachgemessen: nach dem Umstellen ist „Heat" aus der Reihe
verschwunden und steht weiterhin bei Sekunde 1.

**Gesehen ab.** War 90 % als `const`, ist jetzt 85/90/95.

**Umkodieren.** `-preset veryfast -crf 21` und VAAPI `-qp 23` waren fest
verdrahtet, ohne Einstellung und ohne Umgebungsvariable. Drei Stufen:
Qualität (medium/19, qp 21), Ausgewogen (wie bisher), Tempo
(ultrafast/23, qp 26). Mit dem Warnsatz daneben, und der ist ernst
gemeint: was in Echtzeit laufen muss, darf nicht langsamer rechnen als es
abspielt.

**Und das Vorhandene wird neu bewertet.** `InProgress` ist eine
gespeicherte Spalte — die Bruchrechnung lässt sich nicht nach SQL
übersetzen. Eine neue Schwelle wirkte sonst nur auf künftige Meldungen.
Der Endpunkt läuft deshalb einmal in Stapeln durch und meldet zurück, wie
viele Zeilen das umgeworfen hat: „Übernommen — 2 Einträge wurden neu
bewertet." Ohne diesen Satz änderte sich die Startseite scheinbar von
selbst.

`ProgressRule` bleibt ein reiner Typ: die Regeln kommen als Wert herein.
Nebenbei zieht der doppelte `zeile()`-Bauer nach `teile.ts` — er stand
zweimal wortgleich da.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 16:52:53 +02:00
ScarriffleandClaude Opus 5 1e4a231d1b Elf Abschnitte in einer Spalte waren zehn zu viel
Die Verwaltung war eine Seite mit 1786 Zeilen. Wer das Protokoll sehen
wollte, scrollte an Mediatheken, Prüfliste, Benutzern und Speicher
vorbei — und jede neue Einstellung hätte sie länger gemacht.

Jetzt hat sie eine eigene Leiste mit sieben Bereichen, jeder mit seiner
Adresse: `#/verwaltung/protokolle` lässt sich ablegen, und ein Neuladen
bleibt dort stehen. Die Leiste sieht aus wie die Hauptleiste daneben,
damit sie als Navigation gelesen wird und nicht als Knopfreihe; am
Telefon wird aus der Spalte ein Streifen zum Schieben.

**Gebaut wird nur, was offen ist.** Das ist nicht nur sparsamer: das
Abspielfenster fragt im Fünf-Sekunden-Takt nach, die Mediathekenliste
beobachtet laufende Scans. Gemessen: nach dem Wechsel in einen anderen
Bereich kommt zwölf Sekunden lang keine Anfrage mehr.

`admin.ts` ist in acht Dateien zerlegt — eine je Bereich, dazu `teile.ts`
für die gemeinsame Überschrift und `index.ts` für Leiste und Weiche.

Zwei Dinge mussten dabei nachziehen:

**Die Markierung in der Hauptleiste.** Sie verglich stur zwei Segmente;
bei `#/verwaltung/metadaten` traf das auf keinen Eintrag, und
„Verwaltung" verlor seinen Balken. Jetzt gilt auch ein Segment.

**„5 Titel warten — ansehen".** Der Knopf sprang mit `scrollIntoView` zur
Prüfliste. Die steht nicht mehr im selben Dokument — er wechselt jetzt
den Bereich. Die Hervorhebung danach entfällt damit, und mit ihr ihr CSS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 16:39:50 +02:00
ScarriffleandClaude Opus 5 94c6604e8b Pfeile am Bildrand, und „Mehr" bleibt, wo es ist
**Pfeile.** Der Aufmacher hatte nur die Punkte; die Pfeile, die du gedrückt
hast, gehörten zur Reihe darunter. Jetzt stehen links und rechts über dem
Bild zwei grosse Knöpfe. Am Telefon gibt es sie nicht — dort nähme ein
Knopf über dem Bild ein Drittel der Breite —, dafür lässt sich wischen.
Nur waagerecht und erst ab 48 Pixeln, sonst käme man am Aufmacher nicht
mehr vorbeigescrollt.

**Und die Knöpfe springen nicht mehr.** „Abspielen" ist 128 Pixel breit,
„Fortsetzen ab 1:23:45" 207, der Terminhinweis 257 — alles in derselben
Flexreihe, also wanderte „Mehr" bei jedem Umblättern mit, und das
Karussell blättert von allein. Der vordere Platz hat jetzt eine feste
Breite. Gemessen: „Mehr" steht auf allen sieben Folien an derselben Stelle.

Dabei fielen zwei Dinge auf, die nicht am Umbau lagen:

**Der Textblock war so breit wie sein längster Satz.** Als blosses Flexkind
richtete er sich nach dem Inhalt — bei einer kurzen Inhaltsangabe schmaler
als bei einer langen. Auf dem Telefon rückte „Mehr" dadurch um fünfzig
Pixel, und mit dem festen Platz lief die Zeile sogar über den Bildschirm
hinaus; `overflow: hidden` schnitt die rechte Hälfte des Textes ab.

**Ein fehlender Schriftzug liess den Aufmacher namenlos.** Steht ein Logo
in den Metadaten, wurde der Titel daneben versteckt — auch wenn das Bild
gar nicht lud. Zu sehen war ein Platzhalter und sonst nichts. Jetzt tritt
der Name an seine Stelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 16:32:55 +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 f10fb4c1bc Abspielen spielt ab
Im Aufmacher der Startseite stand „Abspielen", und wer draufdrückte,
landete auf der Detailseite. Gemessen am laufenden Server: beide Knöpfe
riefen dieselbe Funktion — `oeffnen` —, der eine hiess nur anders.

Drei Dinge waren falsch, alle an derselben Stelle:

**Der Knopf log.** Jetzt startet er den Spieler, mit Fortsetzen ab der
Stelle, an der man war. Ohne Datei — eine ganze Serie — heisst er
„Ansehen" und führt dorthin, wo man wählen kann.

**Eine angefangene Folge war kein Film.** Sie lief durch `alsHero` mit
`art: 'filme'` und ihrer eigenen Kennung; der Klick ging auf
`#/filme/<Folgenkennung>` und endete in „Diesen Film gibt es nicht
mehr". Nachgemessen im Browser, dann `angefangenAlsHero` dafür gebaut:
die Detailseite gehört der Serie, der Fortschritt der Folge.

**Ein geplanter Titel sah aus wie jeder andere.** Jetzt steht statt des
Knopfes „Bald ist es soweit" mit dem Termin, wie auf der Detailseite.

Dafür nennt `/home/continue` und `/home/next` nun Datei, Länge und
Termin, und die Vorschläge bringen ihre Dateien mit — ohne die konnte
der Aufmacher gar nicht abspielen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 15:42:42 +02:00
ScarriffleandClaude Opus 5 343b0cdcfb Ein Knopf, der nichts tut, ist schlimmer als keiner
Der geplante Film zeigte weiter „Abspielen" und „Herunterladen", und wer
drückte, bekam „Dieser Titel ist ab dem 6.10. um 18 Uhr verfügbar". Die
Sperre im Server war richtig; die Oberfläche wusste nur nichts davon.

Gemessen: die Liste führte `PublishAt` seit jeher mit, die Einzelabfrage
`GET /api/v1/movies/{id}` nicht — und die Detailseite entscheidet genau
daran. `publishAt` kam also immer als `null` an, die Prüfung lief ins
Leere. Dasselbe fehlte bei `GET /api/v1/series/{id}` und bei den Folgen.

Jetzt steht dort, wo sonst der Knopf stünde: „Bald ist es soweit", mit
dem Termin darunter. Kein Versprechen, das beim Klicken zurückgenommen
wird.

Der Test prüfte bisher nur die Liste — darum fiel es nicht auf. Er fragt
jetzt die Detailseite und schlägt ohne die Korrektur fehl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 15:10:30 +02:00
ScarriffleandClaude Opus 5 255eb7c2f0 Spider-Man ist nicht 720p, er ist breit
Die Auflösung wurde allein aus der Höhe gebildet. Ein Kinoformat von 2.39:1
liegt als 1920×800 vor — nach der Höhe gerechnet kam „720p" heraus, und das
stand so in der Oberfläche. Jetzt zählt die grössere von Breite und Höhe;
Hochformat kippt damit nicht, und 4K-Breitbild heisst nicht mehr 1440p.

Die Detailansicht hat zwei Leserschaften, und bisher bekamen beide dasselbe.
Wer schauen will, sieht jetzt Laufzeit, Auflösung und die Sprachen — jede
einmal, statt „Englisch · Englisch · Englisch · Englisch · Deutsch". Wer die
Mediathek betreut, sieht darunter Dateiformat, Grösse und den Ordner. Der
Ordner verrät eine falsche Zuordnung in einer Sekunde: steht über der
Beschreibung „Hanni & Nanni 3" und der Ordner heisst „Hanni und Nanni", ist
die Sache klar. Er kommt nur für Administratoren überhaupt vom Server —
versteckt im Client wäre er weiterhin auslesbar.

Ein geplanter Titel war unsichtbar. Das ist die Hälfte, die nichts bringt:
dass er kommt, ist der Sinn des Planens. Er steht jetzt mit „Verfügbar ab …"
in der Mediathek, und die Datei geht trotzdem nicht heraus — Download wie
Strom lehnen bis zum Termin ab, auch für Administratoren. Wer vorziehen will,
hat „Jetzt veröffentlichen"; eine Hintertür für eine Rolle wäre eine Regel,
die manchmal gilt.

Was ein Auffrischen geändert hat, steht jetzt unter „Zu prüfen" — in einer
eigenen Gruppe mit „Zur Kenntnis" statt „Veröffentlichen". Diese Titel sind
sichtbar; es gibt nichts freizugeben, nur etwas abzuhaken.

Und das Protokoll sagt endlich, wo es knallt. „InvalidOperationException:
Nullable object must have a value." ohne eine einzige Zeile Herkunft ist für
niemanden eine Diagnose — weder für den Betreiber noch für mich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 14:51:00 +02:00
ScarriffleandClaude Opus 5 f44779c899 Eine Zahl ohne das, worauf sie sich bezieht
„4 warten auf Freigabe" stand in der Mediathekenzeile, und die Liste darüber
war leer — sogar versteckt. Zwei Gründe, beide behoben.

Die Prüfliste wurde einmal beim Öffnen geholt und nie wieder. Findet ein
laufender Scan danach etwas Neues, zählt die Zeile es mit, die Liste aber
nicht. Sie hört jetzt auf dieselbe Meldung wie die Seitenleiste: Scan durch,
etwas freigegeben, Mediathek geändert — nachladen.

Und der Knopf gab blind frei. Auf die Frage „wo sehe ich die vier Filme?"
war das keine Antwort: er veröffentlichte sie, ohne sie zu zeigen. Jetzt
führt er dorthin, wo sie mit Plakat, Jahr und Dateiname stehen und sich
einzeln anhaken lassen — und hebt die Stelle kurz hervor, damit man nicht
weitersucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 13:56:12 +02:00
ScarriffleandClaude Opus 5 58e06d0539 Hanni und Nanni ist nicht Hanni und Nanni 3
Deine drei Fälle haben eines gemeinsam, und es ist nicht das, was ich zuerst
vermutet hatte: keiner der Dateinamen nennt ein Jahr. Der Riegel von gestern
greift deshalb nicht.

Gemessen, woran es wirklich liegt. Bei TMDB heisst der Film „Hanni & Nanni",
auf der Platte „Hanni und Nanni". Das Kaufmanns-Und fällt im Vergleichs-
schlüssel als Sonderzeichen heraus, das Wort „und" bleibt stehen — die
Namensgleichheit griff also nie. Gewählt wurde stattdessen der erstbeste
Treffer, und das war „Hanni & Nanni 3". Der richtige stand auf Platz zwei,
und alle vier Treffer hatten dieselbe Beliebtheit: die Reihenfolge war
Zufall.

Bindewörter zwischen zwei Namen zählen jetzt nicht mehr mit — gestrichen,
nicht übersetzt, sonst bliebe „Batman and Robin" aussen vor. Am Wortanfang
bleiben sie stehen: „Und dann kam Polly" ist kein „dann kam Polly". Und die
Suche vergleicht über den Schlüssel statt Zeichen für Zeichen.

Das verhindert aber nur Neues. Was schon zusammengelegt ist, bleibt es —
deshalb gibt es jetzt „Eigener Film": eine Datei löst sich aus ihrem Titel
und wird ein eigener Eintrag, mit dem Titel aus dem Dateinamen und ohne
Kennung. Eine zu raten hiesse, denselben Fehler zu wiederholen; so steht sie
unter „Ohne Zuordnung" und ist in zwei Klicks richtig.

Der Super-Mario-Fall liegt anders: beide Titel treffen heute richtig. Der
steckt noch aus einem früheren Lauf fest und braucht das Trennen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 13:40:21 +02:00
ScarriffleandClaude Opus 5 c3124ba27c Zwei Teile, ein Treffer — und einer verschwand
Du hast drei Filme vermisst, und die Zahlen sagten schon, wo sie sind: 448
Filme plus 13 zweite Fassungen sind genau die 461 Dateien. Es fehlte keine
Datei; drei Filme steckten als Fassung in einem anderen.

Gemessen, warum. Der Dateinamenleser ist unschuldig — „Teil 2" bleibt im
Titel. TMDB aber liefert für „Heiligtümer des Todes Teil 1" wie für „Teil 2"
denselben Treffer zuerst, sobald ohne Jahr gesucht werden muss. Und weil
zwei Einträge dieselbe Kennung nicht teilen dürfen, wurde der zweite
geschluckt.

Zwei Dateien, deren Namen verschiedene Jahre nennen, sind jetzt verschiedene
Filme. Der erste Anlauf hatte ein Jahr Abstand als Toleranz erlaubt — für
Dezember-Erstaufführungen — und verschluckte damit genau den Fall, um den es
geht: Teil 1 ist von 2010, Teil 2 von 2011.

Dazu sieht man endlich, was nicht zugeordnet werden konnte. „Nicht
zugeordnet" listete bisher nur Dateien ganz ohne Titel, und die gibt es bei
Filmen nie — deshalb standen dort nur Serien. Daneben eine Liste der Titel
mit mehreren Dateien: zwei Fassungen sind die Regel, zwei verschiedene Filme
der Fehler, und an den Dateinamen sieht man es sofort.

Der Bearbeiten-Dialog hat jetzt Reiter statt Knöpfe, die weitere Fenster
öffnen — ein Fenster über einem Fenster, und man wusste nicht mehr, was man
zumacht. „Titel ändern" und „Bilder bearbeiten" sind aus dem Kontextmenü
raus; über „Bearbeiten" kommt man an beides.

Beim Prüfen gefunden: ein Dialog blieb nach einem Seitenwechsel über der
neuen Seite stehen. Er hängt an body, nicht am Inhaltsbereich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 13:21:42 +02:00
ScarriffleandClaude Opus 5 fcf40e4394 Der Orden des Phönix stand vor dem Stein der Weisen
Alphabetisch stimmt das, nur sieht man sie andersherum. Es gibt jetzt einen
Sortiertitel: „Harry Potter 1", „Harry Potter 2" — und die Reihe steht in
der Ordnung, in der man sie schaut. Leer gelassen bleibt alles wie bisher,
beim Titel ohne Artikel davor.

Er hat einen eigenen Merker, nicht den des Titels. Wer die Reihenfolge
festlegt, soll den Titel trotzdem weiter automatisch bekommen; beides an
einem Schalter hiesse, für das eine auf das andere zu verzichten. Eine
Korrektur der Zuordnung lässt ihn ebenfalls stehen.

Und WALL·E bleibt nicht mehr unter E. Die Kachel wurde nach einer Korrektur
zwar neu beschriftet, blieb aber an ihrem Platz — jetzt rutscht sie dorthin,
wo sie hingehört. Liegt der Platz im noch nicht geladenen Teil der Liste,
geht sie weg und sagt wohin, statt falsch stehenzubleiben.

Dazu ein Dialog, in dem alles zu einem Titel beieinander steht: Titel,
Sortiertitel, Bilder, Zuordnung. Die vier Menüpunkte gab es schon, jeder für
sich richtig — zusammen aber kein Ort. Wer einen Film geraderückt, macht
selten nur eines davon. Die Abkürzungen bleiben.

Beim Bauen gefunden: das Zurücksetzen des Sortiertitels endete in einer 500.
SortTitle.For ist eine C#-Methode, und EF Core übersetzt sie nicht nach SQL.
Es geht um eine Zeile — die wird jetzt geladen und zurückgeschrieben, statt
die Regel ein zweites Mal als SQL zu schreiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:25:31 +02:00
ScarriffleandClaude Opus 5 e70c33f7e2 Not Found war weder deutsch noch wahr
Beim Korrigieren einer Zuordnung geht der bearbeitete Eintrag im vorhandenen
auf — er wird gelöscht. Die Antwort nannte aber nur die Daten des
überlebenden, nicht seine Kennung, und so zeigte die Kachel weiter auf etwas,
das es nicht mehr gab. Wer sie dann bearbeiten wollte, bekam „Not Found".

Zwei Fehler in einer Meldung. Der Server sagt jetzt, welcher Eintrag
überlebt hat und ob zusammengelegt wurde; die Kachel nimmt sich weg statt
auf ein Loch zu zeigen, und sagt wohin.

Und das Englische: in `detail` steht der Satz, den der Server selbst
geschrieben hat — deutsch und mit einem Hinweis, was zu tun ist. `title` ist
dagegen der Statusname aus dem Rahmenwerk, und genau der wurde angezeigt, wo
`detail` fehlte. Er wird nicht mehr gelesen; wo der Server nichts zu sagen
hat, schreibt der Client selbst einen deutschen Satz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:05:36 +02:00
ScarriffleandClaude Opus 5 c46063637f Die Kachel zeigte auf die Folge und wollte zur Serie
„Nächste Folge" führte ins Leere, und es lag weder am Scan noch am
Auffrischen: die Kachel trug die Folgenkennung und öffnete damit eine
Serienseite — die es unter dieser Kennung nie gibt. Heraus kam ausgerechnet
dort „Diese Serie gibt es nicht mehr", wo man gerade weiterschauen wollte.
Der Server kannte die Serie an beiden Stellen und schickte sie nur nicht mit.

Wohin die Kachel führt und was sie misst, ist jetzt getrennt. Beides auf
dieselbe Kennung zu legen wäre der nächste Fehler gewesen: „Als gesehen
markieren" hätte die ganze Serie abgehakt statt der einen Folge, die man zur
Hälfte gesehen hat.

Dazu Streichen wie im Dateimanager: gedrückt halten und darüberfahren wählt
aus, rückwärts über Gewähltes nimmt wieder weg. Ein kurzer Klick bleibt, was
er war. Abgetastet wird die Strecke zwischen zwei Zeigerereignissen, nicht
nur deren Endpunkte — gemessen: ein Zug über vier Kacheln erwischte sonst die
erste und die letzte. Plakate lassen sich nicht mehr durch die Gegend
schleppen.

„Abwählen" ist ein Kreuz geworden; als vierter Knopf neben drei Aktionen sah
es aus, als täte es etwas mit den Titeln.

In der kleinen Leiste lag das Vorschaubild über dem Fortschrittsstrich und
verdeckte ihn rechts. Und ein Klick genau darauf tat nichts, obwohl das die
Stelle ist, auf die man zeigt — jetzt holt auch das Bild den Film zurück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 21:48:00 +02:00
ScarriffleandClaude Opus 5 7df9fa6c01 Zwei Dinge in derselben Ecke, und ein Modus, den niemand bestellt hat
Der Gesehen-Haken und die drei Punkte sassen beide oben rechts — auf dem
Plakat sah das aus wie ein Fehler. Jetzt hat jede Ecke eine Aufgabe: oben
links die Auswahl, oben rechts der Stand, unten links Abspielen, unten rechts
das Menü.

Der Knopf „Auswählen" ist weg. Gemessen, warum er stören musste: der längere
Text „Auswahl beenden" brach die Filterzeile um und schob das Raster um
zwanzig Bildpunkte nach unten. Und grundsätzlicher ist ein Modus ein Zustand,
den man kennen muss, bevor man etwas tun kann. Jetzt trägt jede Kachel oben
links ein Kästchen, das beim Überfahren erscheint — wie die drei Punkte auch.
Der erste Haken fängt die Auswahl an, der letzte beendet sie.

Der Spieler lässt sich verkleinern. Er rutscht in eine Leiste am unteren Rand
und läuft weiter, während man die Mediathek aufräumt; ein Klick darauf holt
ihn zurück. Dasselbe Videoelement, nur anders eingerahmt — ein zweiter
Spieler unten hiesse, das Video auszuhängen, und dabei hört es auf zu
spielen.

Das Zahnrad mit acht Strahlen sah aus wie eine Sonne. Jetzt: Wellenform für
den Ton, CC-Kasten für die Untertitel, Schieberegler für die Qualität. Alle
drei Menüs stehen immer da — vorher verschwanden sie bei nur einer Tonspur
oder ohne Untertitel, und wer sie suchte, fand nichts und wusste nicht warum.

Dabei gefunden und behoben: die Wiedergabe blieb nach zwanzig Sekunden
stehen, obwohl längst der ganze Rest auf der Platte lag. Results.File setzte
Last-Modified, der Browser legte die Playlist mit zwei Einträgen ab, und
hls.js bekam bei jedem Nachfragen dieselben zwei zurück. Eine wachsende Liste
ist das Gegenteil von zwischenspeicherbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 20:31:12 +02:00
ScarriffleandClaude Opus 5 eda9e67802 Wo die Grafikeinheit eingetragen wird, und was passiert wenn sie fehlt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:53:00 +02:00
ScarriffleandClaude Opus 5 4853200d5c Er packte um, was gar nicht am Umpacken lag
Der Browser bekam stillschweigend Apples Geräteprofil — und mit ihm HEVC, AC3
und EAC3, von denen er keines öffnet. Der Server packte die Datei
pflichtschuldig nach MP4 um, liess Bild und Ton dabei unberührt, und heraus
kam eine tadellose Datei, mit der Chrome nichts anfangen konnte. Umpacken
fasst das Bild nun einmal nicht an.

Jetzt sagt das Gerät selbst, was es kann, und es gibt einen zweiten Weg neben
der fertigen Datei: HLS, das nach Sekunden anfängt statt nach Minuten.
Dasselbe Rohr erledigt beides — mkv nach fMP4 mit „-c:v copy", 4K nach 480p
mit libx264 — und eine MP4, die der Browser ohnehin öffnet, geht ganz ohne
ffmpeg hinaus.

Dazu eine eigene Steuerleiste: Tonspur und Untertitel während der Wiedergabe
umschaltbar, vier Auflösungsstufen, Tastenkürzel, und bei einem Fehler ein
deutscher Satz statt eines schwarzen Bildes.

Gemessen statt vermutet: Chromium antwortet auf canPlayType für HLS mit
"maybe" und kann es dann doch nicht — deshalb entscheidet Hls.isSupported().
hls.js wiegt 372 KB und wird erst beim Abspielen geholt; die Oberfläche
bleibt bei 109.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:52:27 +02:00
ScarriffleandClaude Opus 5 557708b769 Ein Klick, der nichts tat, und niemand sagte warum
Eine Serienkachel kannte der Server als Titel nicht: „als gesehen markieren"
lief in eine 404, und weil das Kontextmenü die Ablehnung verschluckte, geschah
sichtbar gar nichts. Dasselbe beim Favoriten. UserItemState bekommt eine
Serienkennung, das Häkchen zieht alle Folgen mit, und was wirft, erscheint als
Meldung statt in der Browserkonsole.

Dazu eine Mehrfachauswahl in der Mediathek — gesehen, ungesehen, Metadaten
neu holen für das Gewählte, in einer Anfrage statt in zweihundert. Ein neues
Plakat steht sofort auf der Kachel, und ein Titel, den es nicht mehr gibt,
endet nicht mehr in einer Sackgasse.

Gemessen: ein POST auf /refresh ohne Rumpf antwortete nach der Erweiterung mit
404 — also genau der Aufruf „alles auffrischen", den es länger gibt als die
Auswahl. Der Rumpf wird jetzt von Hand gelesen, und beide Formen stehen in
einer Prüfung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:28:33 +02:00
ScarriffleandClaude Opus 5 c854c4a09a 451 gegen 461, und ein Band für das, was noch wartet
Drei Zahlen, die alle stimmen und Verschiedenes meinen: Plex zählte 451
Filme, Flaro 449, und der Scan lief über 461 Dateien. Die
Mediathekszeile nennt sie jetzt getrennt — Titel, Folgen, Dateien,
zweite Fassungen und nicht Zugeordnetes. Damit beantwortet sich die
Frage selbst, statt offen zu bleiben.

Die Rechnung dahinter war beim ersten Versuch falsch: sie verglich
gegen alle Folgen und kam auf 1223 Titel bei 324 Dateien. Gezählt wird
jetzt, zu wie vielen Titeln überhaupt noch eine Datei daliegt — dann
geht Dateien = Titel + Fassungen + ohne auf, auch wenn etwas vermisst
wird. Nachgemessen mit einer echten Plex-Kopie: 3 Titel, 4 Dateien, 1
zweite Fassung.

Dazu trägt eine Kachel, die noch auf Freigabe wartet, jetzt ein Band —
„Wartet" oder „Geplant". Für einen Administrator standen wartende Titel
bisher mitten zwischen den freigegebenen, ohne dass etwas sie
unterschied; wer einen suchte, fand ihn nur über die Prüfliste. Dafür
reicht `PublishAt` jetzt bis in die Kachel durch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 22:04:17 +02:00
ScarriffleandClaude Opus 5 5a5cf3d006 Dieselbe Kennung zweimal zu vergeben war kein Fehler, sah aber so aus
Im Zuordnungsdialog stand „An error occurred while processing your
request." — englisch, ohne Grund, ohne nächsten Schritt. Dahinter steckte
der Eindeutigkeitsindex: der gewählte Film war in derselben Mediathek
schon vergeben, und die Kennung wurde trotzdem blind gesetzt.

Es ist aber gar kein Fehler. Zwei Dateien, die denselben Film meinen,
gehören zusammengelegt — genau das tut der Scan am Ende jedes Laufs, und
der Sehfortschritt wird dabei gefaltet. Die Korrektur von Hand tut es
jetzt auch, für Filme wie für Serien.

Dazu die zweite Hälfte: auch das Letzte, was durchrutscht, spricht
Deutsch. Der Fehlerbehandler am Ende der Kette gab bisher die Vorgabe von
ASP.NET aus. Jetzt bekommt jede Ausnahme einen deutschen Satz, und die
Einzelheiten gehen ins Tagesprotokoll — dorthin, wo man sie sucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 21:53:20 +02:00
ScarriffleandClaude Opus 5 0c07d4efc7 Ein falscher Treffer verhinderte seine eigene Korrektur
„Doctor Strange" war fälschlich auf ein Making-of von 2022 gelaufen. Beim
Korrigieren hängte die Vorschlagssuche `year=2022` an — aus genau der
Zuordnung, die weg sollte — und lieferte nur noch Titel von 2022. Der
richtige Film von 2016 konnte gar nicht erscheinen, egal wie oft man
suchte.

An TMDB nachgemessen:

  query='Doctor Strange' ohne Jahr     → 2016 Doctor Strange, 2022 Multiverse …
  query='Doctor Strange' mit year=2022 → genau die drei Treffer aus der Meldung

Gesucht wird jetzt mit dem Jahr aus dem Dateinamen, nie mit dem aus dem
Treffer. Die Oberfläche trägt ebenfalls keines mehr vor: das angezeigte
stammt aus derselben falschen Zuordnung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 21:14:55 +02:00
ScarriffleandClaude Opus 5 74c625b829 Das Dreipunktemenü in der Prüfliste ging gar nicht auf
Der öffnende Klick blubbert nach dem eigenen Zuhörer weiter zum Dokument,
und der dortige schliesst alles, was nicht im Menü liegt — also auch
dieses, im selben Augenblick. In der Mediathek stand deshalb ein
`stopPropagation()` am Griff; in der Prüfliste fehlte es.

Eine Regel, an die sich jeder Aufrufer erinnern muss, ist keine Regel.
Deshalb nicht die Kopie nachgezogen, sondern das Menü merkt sich jetzt
selbst, woran es hängt, und der globale Zuhörer lässt den Griff in Ruhe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 21:07:36 +02:00
ScarriffleandClaude Opus 5 d2fbaad871 Jeder Titel war seine eigene Dublette, und das seit Monaten
„Metadaten auffrischen" hat an allem, was schon zugeordnet war, schlicht
nichts getan. Die Dublettenprüfung schlägt jeden Titel nach, der eine
TMDB-Kennung hat — beim Auffrischen also auch den gerade bearbeiteten.
Ohne Vergleich auf Gleichheit fand sich jeder als "Dublette seiner
selbst", wurde übersprungen und nie fortgeschrieben. Titel, Beschreibung
und Bilder blieben für immer so, wie sie beim allerersten Lauf
hereinkamen.

In der Oberfläche sah das aus, als wirke die Spracheinstellung nicht —
und genau so wurde es gemeldet. Gemessen, Ende zu Ende:

  Serversprache en-US:  101 Dalmatians  ·  The Lion King
  Serversprache de-DE:  101 Dalmatiner  ·  Der König der Löwen

Damit das nie wieder unbemerkt bleibt, weiss der Server jetzt, in welcher
Sprache er die Titel hat: eine Spalte MetadataLanguage, gesetzt beim
Anreichern, und in der Verwaltung eine Zeile mit der Verteilung. Weicht
etwas ab, frischt ein Knopf genau diese Titel auf — nicht den ganzen
Bestand, und ohne das Dateisystem noch einmal abzulaufen.

Dazu zwei Dinge, die ohne das nicht gingen: ein Titel lässt sich von Hand
setzen und wird dabei festgeschrieben wie ein Plakat, mit einem Weg
zurück. Und ein Titel, der auf Freigabe wartet, trägt jetzt dasselbe
Dreipunktemenü wie eine Kachel — Bilder, Zuordnung, Titel. Ausgerechnet
dort fehlte es, obwohl das der Eintrag ist, den man vor der Freigabe
geradeziehen will.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 20:49:06 +02:00
ScarriffleandClaude Opus 5 ce38da1376 Die Auswahlliste klappte hinter dem Dialog auf
Ein `<dialog>` mit `showModal()` liegt in der Top-Layer des Browsers,
über allem — unabhängig von `z-index`. Die Liste hing am `body` und
verschwand deshalb hinter genau dem Dialog, in dem ihr Knopf steht.
Jetzt wird sie in den Dialog gehängt, wenn es einen gibt. Mit
`elementFromPoint` nachgeprüft: an ihrer Stelle liegt wirklich ein
Listeneintrag obenauf.

Dazu zwei Stellen, die zu der Vermutung geführt haben, die
Spracheinstellung wirke nicht:

„Übersetzungen holen" stand stur auf en-US. Neben einem Server, der auf
Deutsch steht, liest sich das wie eine Aussage über den Server.
Vorgewählt wird jetzt die eigene Sprache, sonst die erste, die nicht
schon die des Servers ist — und der Text nennt die Serversprache.

Und „Meine Sprache" sagt jetzt deutlich, wenn sie abweicht. Sie ändert
nicht, was der Server holt, nur was man selbst sieht: steht sie auf
Englisch, während der Server auf Deutsch holt, sieht die ganze
Mediathek englisch aus und jedes Auffrischen scheint wirkungslos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 20:03:01 +02:00
ScarriffleandClaude Opus 5 e87d50e470 Wer die Genreliste durchblättern wollte, schloss sie dabei
Der Zuhörer hängt mit `capture` am Fenster, damit auch ein Bildlauf in
einem inneren Behälter die Liste schliesst — sie steht `position: fixed`
und bliebe sonst neben einem Knopf stehen, der längst weggescrollt ist.
Nur scrollt eine Liste mit zwanzig Genres selbst, und dieses Ereignis
kam dort ebenfalls an. Jetzt wird geprüft, woher es stammt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 19:22:42 +02:00
ScarriffleandClaude Opus 5 aeaf6eb578 „highschool musical 3" fand TMDB nicht, „high school musical 3" schon
Drei gemeldete Fälle, alle an TMDB nachgemessen statt vermutet:

  highschool musical 3              0 Treffer
  high school musical 3             2 Treffer, der richtige zuerst
  Harry Potter im Orden des Phoenix 0 Treffer
  Harry Potter                     20 Treffer, der richtige darunter
  dr dolittle                      13 Treffer, der richtige zuerst

Der fehlende Punkt bei Dr. Dolittle war nie das Problem — diese Vermutung
ist in Suchvarianten.cs festgehalten, damit sie nicht wiederkommt.

Die Suche versucht jetzt der Reihe nach: den Titel wie er dasteht, dann
das längste Wort an jeder Stelle getrennt (von der Mitte nach aussen,
ohne Wörterbuch — „highschool" trifft nach 2 Versuchen, „jurassicpark"
nach 5), dann von hinten gekürzt, bis zwei Worte übrig sind. Höchstens
zehn Anfragen, und nur dann, wenn die erste gar nichts brachte.

Und die Treffer werden nach Ähnlichkeit zum gesuchten Titel sortiert,
nicht nach Beliebtheit. Über gemeinsame Worte ohne Akzente: so trennt
„Harry Potter" den „Orden des Phönix" von „Harry Potter: Fireplace", und
„Phoenix" findet „Phönix".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 19:13:24 +02:00
ScarriffleandClaude Opus 5 4b0323f955 Drei Auswahlfelder liessen sich gleichzeitig aufklappen
Jedes merkte sich nur seine eigene Liste. Nebeneinander geöffnet lagen
sie übereinander, und keine wusste von den anderen. Ein Auswahlfeld ist
ein Menü, und von denen ist höchstens eines offen — jetzt modulweit
vermerkt, genau wie beim Kontextmenü. Escape schliesst zusätzlich von
überall, nicht nur mit dem Fokus in der Liste.

Dazu stand unter jeder Mediathek „0 Titel freigeben". Der Knopf wurde
über `hidden` versteckt — das Attribut wirkt über `display: none` mit der
Spezifität von Null, und `.btn` setzt `display: inline-flex`. Die Klasse
`.hidden` trägt ein `!important` und setzt sich durch.

Und das Favicon kommt jetzt aus `icon.png`, dem gerenderten App-Icon
selbst. Meine nachgezeichnete Fassung war falsch: die Grafik füllt die
Fläche, statt klein in der Mitte zu sitzen. Ein Icon, das dem echten nur
ähnelt, driftet beim nächsten Entwurf wieder auseinander.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 19:01:58 +02:00
ScarriffleandClaude Opus 5 8809b9f46f Die Mediathek hörte bei I auf, und jede Folge stand doppelt
Zwei Befunde aus dem laufenden Betrieb.

Die Liste holte genau eine Seite. Der Server gibt höchstens 200 Einträge
je Zug — das ist richtig so —, nur fragte die Oberfläche nie nach der
zweiten. Bei 455 Filmen endete die Mediathek mitten im Alphabet, ohne
Hinweis, dass da noch etwas kommt. Jetzt wird beim Scrollen nachgeladen,
und die Zahl sagt „200 von 455" statt einer Behauptung, die man beim
Scrollen widerlegt.

Dabei kam heraus, dass ein IntersectionObserver eine Seite braucht, die
auch gezeichnet wird: im Fenster im Hintergrund löst er nicht aus, und
beim Prüfen blieb die Liste genau deshalb bei 200 stehen. Daneben steht
jetzt eine Prüfung am Scrollereignis, die überall greift.

Der zweite Befund: jede Folge zeigte zwei gleich aussehende Knopfreihen.
Es sind zwei Dateien — Plex' „Optimized for Mobile"-Kopien. Flaro kannte
„optimized", aber nicht „Optimized for Mobile", und sah ohnehin nur den
unmittelbaren Elternordner; Plex legt die Fassung aber als
`Plex Versions/Optimized for Mobile/<Serie>/<Staffel>/…` ab. Jetzt wird
drei Ebenen hoch gesucht, die Schublade `Plex Versions` benennt keine
Serie mehr, und an der Folge steht „Mobil" statt einer zweiten,
unerklärten Reihe.

Nicht um Plex zu bedienen — die Dateien liegen nun einmal so auf der
Platte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 18:45:07 +02:00
ScarriffleandClaude Opus 5 960cca3dae Der Abschnitt verschwand alle drei Sekunden und kam wieder
`laden()` setzte `innerHTML = ''` und baute die Mediathekenliste neu —
während eines Scans alle drei Sekunden. Der ganze Abschnitt flackerte
sichtbar, und wer gerade auf einen Knopf zielte, klickte ins Leere.
Schlimmer: jeder Neuaufbau startete über `verfolgen(false)` einen
weiteren Takt. Nach einer Minute liefen zwanzig davon nebeneinander.

Jetzt wird fortgeschrieben statt neu gezeichnet: eine Zeile je
Mediathek, die bleibt, und nur Kopfzeile, Prüfungsknopf und
Freigabeknopf ändern sich. Belegt im Browser — nach einem Auffrischen
sind es dieselben Knoten, nur die Beschriftung ist eine andere.

Dieselbe Behandlung für die Seitenleiste. Und weil Seitenleiste und
Verwaltung getrennt nachfragten, zeigten sie zwei Momente — 95 hier, 84
dort, während im Hintergrund Doppelte zusammengelegt wurden. Die
Verwaltung reicht ihren Stand jetzt weiter: ein Abruf, zwei Anzeigen.

Dazu zwei Dinge, die beim Prüfen auffielen. Zurücksetzen und Entfernen
prallen jetzt serverseitig mit 409 ab, solange ein Lauf läuft — die
Oberfläche sperrt die Knöpfe, aber darauf darf sich der Server nicht
verlassen; ein Aufruf von Hand während eines Scans liess den Lauf an
einer DbUpdateConcurrencyException sterben. Und in der
Fortschrittszeile steht beim Holen der Folgenmetadaten jetzt der
Serienname: „Folge 2×13" allein sagt bei 3815 Folgen nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 18:21:43 +02:00
ScarriffleandClaude Opus 5 a6e9a6e425 Ein Klick auf einen Vorschlag sah aus wie gar kein Klick
Hinter der Auswahl steckt ein Abruf bei TMDB, der Titel, Besetzung und
Bilder nachlädt — mehrere Sekunden, in denen die Oberfläche aussah wie
vorher. Wer nichts sieht, klickt noch einmal, und dann laufen zwei.
Jetzt tritt die Liste zurück, die gewählte Zeile bekommt einen
Akzentrahmen und daneben steht „Wird zugeordnet…" mit laufendem Kreis.

Dazu das Plakat je Vorschlag. „Dark" von 2017 und „Dark Shadows" von
1966 sind an einer Textzeile kaum zu unterscheiden, am Bild in einer
Zehntelsekunde — und der Server lieferte die Adresse ohnehin schon mit,
sie wurde nur weggeworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:58:06 +02:00
ScarriffleandClaude Opus 5 193a23a904 Ein Unterstrich ist ein Wortzeichen, und das kostete 3445 Dateien
In `The Good Doctor_S02E18_Trampolin.mkv` steht vor dem S ein
Unterstrich. Das Muster verlangte dort `\b` — eine Wortgrenze sitzt aber
zwischen Wortzeichen und Nicht-Wortzeichen, und der Unterstrich ist
selbst eines. Zwischen `r` und `S` gab es damit keine Grenze, und der
gesamte Bestand aus einem Mediathek-Download fiel durch die Erkennung.
Die Rückschau verlangt jetzt ausdrücklich: kein Buchstabe, keine Ziffer
davor — ein Unterstrich schon.

Zwei weitere Muster kamen dabei ans Licht. Mediatheken nummerieren nach
Jahr (`Die Anstalt_S2024E04`); drei Ziffern für die Staffel waren zu
wenig. Und `# Folge 12 #` ist eine Folgennummer, nur auf Deutsch — die
Staffel kommt dann vom Ordner darüber, und nur wenn auch der keine
nennt, gilt die Übereinkunft Staffel 1.

Gemessen an den 3559 Pfaden aus dem Protokoll: 3554 werden jetzt
erkannt. Die fünf übrigen tragen wirklich keine Nummer — ein Titel in
einem Staffelordner, sonst nichts. Die Zahl der Serien steigt damit von
58 auf 95, und 95 ist die Zahl, die von Anfang an richtig war.

Die neuen Tests arbeiten mit genau diesen Namen. Die alten hatten den
Fehler nicht gefunden, weil in jedem ausgedachten Namen ein Leerzeichen
vor der Folgennummer stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:50:29 +02:00
ScarriffleandClaude Opus 5 cfb1e71762 3559 von 5884 Dateien verschwanden lautlos
Der Scan erfindet keine Serie mehr, wenn weder Ordner noch Dateiname
eine nennen — richtig so. Nur wurde die Datei dann übersprungen, ohne
Zeile, ohne Spur. In der Mediathek standen 58 Serien und niemand konnte
sagen, wo die anderen geblieben sind.

Jetzt bekommt jede solche Datei eine Zeile ohne Zuordnung. Damit ist sie
zählbar, auflistbar und nach einer besseren Regel nachträglich
zuzuordnen. Untersucht wird sie nicht: ffprobe auf 3559 Dateien kostet
eine halbe Stunde für Angaben, die niemand zu sehen bekommt, und sobald
die Zuordnung steht, holt der nächste Lauf es nach.

Dazu ein Abschnitt "Nicht zugeordnet" in der Verwaltung, nach Grund
gebündelt und mit den Pfaden zum Aufklappen. Zwei Gründe gibt es, und
sie verlangen Verschiedenes: fehlt die Folgennummer, muss umbenannt
werden; fehlt der Serienname, genügt ein Ordner darum herum. Die Liste
lässt sich als Datei herunterladen — eine, die man markieren und
kopieren muss, schickt niemand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:23:36 +02:00
ScarriffleandClaude Opus 5 a8bb98c44c Umsortiert wird gezogen, nicht geklickt
Die Pfeile sind weg; eine Mediathek wird in der Seitenleiste an ihren
Platz gezogen. Mit Pointer Events statt HTML5-Drag: letzteres kennt kein
Telefon, zeichnet ein Geisterbild, das sich nicht gestalten lässt, und
liefert während des Ziehens keine brauchbaren Koordinaten.

Mit Schwelle von sechs Bildpunkten — sonst verschiebt jeder leicht
wackelige Klick die Reihenfolge, und ein Klick ohne Bewegung führt
weiterhin zur Mediathek, auch im Bearbeitungsmodus. Die weichenden
Zeilen gleiten, die gezogene folgt dem Zeiger ohne Verzögerung; bei
„Bewegung reduzieren" gleitet nichts.

Die Pfeiltasten bleiben auf der fokussierten Zeile, nur ohne sichtbare
Knöpfe. Ziehen ist mit der Tastatur nicht bedienbar, und eine Funktion,
die nur mit der Maus erreichbar ist, ist für den, der keine benutzt, gar
keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:09:28 +02:00
ScarriffleandClaude Opus 5 a22fa9b58e Drei Zähler für dieselbe Sache, und keiner davon offensichtlich falsch
Während eines Scans stand in der Mediathek 20, in der Verwaltung 14 und
in der Seitenleiste 0. Die Verwaltung frischte alle fünf Runden auf, die
Seitenleiste erst am Ende des Laufs. Jetzt laufen beide auf demselben
Takt, alle drei Runden.

Dazu: während ein Lauf läuft, sind Scannen, Metadaten auffrischen,
Zurücksetzen und Entfernen gesperrt — die ersten beiden prallen am
Server ab und sind nur verwirrend, die anderen beiden löschen Zeilen,
während der Lauf welche schreibt. Umbenennen und Prüfung bleiben
bedienbar. Die Sperre richtet sich nach dem Zustand, den der Server
meldet, nicht nach dem Klick: wer die Seite neu lädt, während ein Lauf
läuft, findet sie ebenfalls gesperrt vor.

Die Reihenfolge der Mediatheken wird jetzt in der Seitenleiste
umsortiert, über „Seitenleiste bearbeiten" neben der Überschrift. Sie
wirkt dort, also gehört der Griff dorthin — vorher schob man in den
Einstellungen und musste danach nachsehen, ob es das war, was man
wollte.

Der Zuordnungsdialog zeigt die Herkunft: Ordner, Zahl der Dateien und
ein paar Namen. Liegt eine Zuordnung völlig daneben, sagt der angezeigte
Titel nichts mehr darüber, welche Datei gemeint ist.

Und SQLite läuft jetzt im WAL-Modus. In der Vorgabe sperrt ein Schreiber
die ganze Datei: solange der Scan schrieb, kam keine Leseanfrage durch.
Genau so sah es aus — Bilddialog und Protokoll liefen während eines
Laufs sporadisch ins Leere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 16:31:50 +02:00
ScarriffleandClaude Opus 5 e1b3625809 Das Würfeln der Build-Kennung brach den ganzen Lauf ab
`tr -dc … < /dev/urandom | head -c 6`: head schliesst die Pipe nach sechs
Zeichen, tr bekommt SIGPIPE, und mit `set -o pipefail` gilt das als
Fehlschlag. Das Skript endete mit 141, bevor es irgendetwas gebaut hatte
— dieselbe Falle, vor der dreissig Zeilen weiter oben eine Anmerkung
wegen `grep -q` steht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 15:58:18 +02:00
ScarriffleandClaude Opus 5 ac04901ccc Neun gemeldete Fehler, und die Frage, welcher Build gerade läuft
Ein grosser Commit, weil die Punkte einander bedingen: ohne lesbare
Fehlermeldung war die Serienerkennung nicht zu diagnostizieren, und ohne
Build-Kennung nicht einmal festzustellen, ob überhaupt der gebaute Stand
läuft. Zwei der neun Meldungen betrafen Funktionen, die seit Stunden im
Quelltext stehen.

Version und Build. Die Version steht fest; daneben eine Kennung aus
sechs Zeichen, die sich bei jedem Docker-Bau ändert. Sie steht in der
Verwaltung, unter /health und im Protokoll bei jedem Start. Damit ist
"reden wir vom selben Build?" in zwei Sekunden beantwortet statt in
einem halben Tag.

Serien. Lag eine Datei direkt im Serienordner und trug vor der
Folgennummer nichts, wurde der ganze Dateiname zum Serientitel: aus 95
Serien wurden 737, jede mit einer Folge, bei jedem Lauf neu. Dazu lief
der Scan in Verweise hinein — derselbe Ordner unter zwei Namen ergab
zwei Serien. Und weder neue noch umgezogene Dateien standen in der
Pfadliste des Laufs, weshalb ein doppelt gesehener Pfad eine zweite
Zeile mit gleichem (LibraryId, Path) anlegte und der
Eindeutigkeitsindex den ganzen Speichervorgang scheitern liess. Jede
Serienanlage wird jetzt im Tagesprotokoll begründet.

Fehlermeldungen. "An error occurred while saving the entity changes"
stand englisch in einer deutschen Oberfläche und nannte weder Ursache
noch nächsten Schritt. Jetzt geht der vollständige Text ins Protokoll
und ein deutscher Satz ohne Fehlercode in die Anzeige — auch für
"Failed to fetch", das aus dem Browser kam und nie übersetzt wurde.

Protokolle. Die Ansicht holte die ganze Datei und zeigte 800 Zeilen
davon; bei zwei Megabyte brach die Verbindung ab. Gelesen wird jetzt
serverseitig rückwärts, mit einem Knopf für mehr.

Auswahlfelder. Die drei <select> zeichnete das Betriebssystem: ein
helles Systemmenü mitten in der dunklen Oberfläche. Ersetzt durch ein
eigenes Listenfeld mit Tastaturbedienung.

Bildwahl. Drei Raster untereinander in einem scrollenden Dialog hiess
zwei Scrollebenen übereinander. Jetzt ein Reiter je Bildtyp.

Sprache. Die Serversprache stand in der Serverliste zwischen Name,
Metadaten und Fassung — gesucht und nicht gefunden. Sie hat jetzt einen
eigenen Abschnitt ganz oben.

Dazu: ein Knopf, der den erfassten Inhalt einer Mediathek verwirft, ohne
Name und Pfad mitzunehmen; ein Abschnitt Speicher mit den drei Pfaden,
ihrer Belegung und dem Leeren des Bildspeichers; und die Korrektur einer
Zuordnung frischt nur noch die Kachel auf statt die ganze Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 15:55:56 +02:00
ScarriffleandClaude Opus 5 ab8a66d898 Die Veröffentlichung meldete Erfolg, obwohl nichts ankam
docker buildx build --push beendete mit Rückgabewert 0, nachdem es
"ERROR: failed to build" gedruckt hatte — ein TLS-Zeitlimit mitten im
Hochladen. Das Skript meldete "Fertig", in der Registry lag das alte Image,
und auf dem NAS wäre eine Fassung angekommen, die niemand gebaut hat.

Das ist die schlimmste Sorte Fehler: einer, der wie Erfolg aussieht.

Jetzt wird der Digest vor und nach dem Bau abgefragt. Antwortet die
Registry nicht, oder liegt dort noch derselbe Stand wie vorher, bricht das
Skript mit Rückgabewert 1 ab und sagt es.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:41:39 +02:00
ScarriffleandClaude Opus 5 fe44d45323 Verschobene Dateien hinterliessen eine Leiche
Seit verschwundene Dateien markiert statt gelöscht werden, blieb nach jeder
Umbenennung die alte Zeile als "vermisst" stehen. Damit galt die alte Folge
nicht als verwaist, die alte Serie nicht als leer — und nach dem Umbenennen
eines Serienordners stand alles doppelt da.

Jetzt erkennt der Scan eine Verschiebung an Grösse und Änderungszeit und
hängt den vorhandenen Eintrag um, statt einen zweiten anzulegen.

Der Zeitstempel musste dafür auf Sekunden gerundet werden: er geht durch
die Datenbank und kommt nicht auf den Tick genau zurück. Mit Vergleich auf
Gleichheit schlug die Erkennung ausnahmslos fehl — der Test zeigte es,
nachdem er zweimal dasselbe falsche Ergebnis lieferte.

Grösse und Zeit müssen beide stimmen, und das Merkmal muss eindeutig sein.
Grösse allein reichte nicht: zwei gleich grosse Dateien gibt es, und die zu
vertauschen wäre schlimmer als eine Leiche.

Dazu: die Zahl der Titel läuft während eines Scans mit, statt erst am Ende
zu springen. Alle fünf Runden, nicht jede — ein Abruf je Sekunde für eine
Zahl wäre Verschwendung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:34:49 +02:00
ScarriffleandClaude Opus 5 b64497acd5 Nicht jede erfolgreiche Antwort hat einen Körper
Bei jedem Scanstart stand in der Verwaltung "JSON.parse: unexpected end of
data" — eine Fehlermeldung für etwas, das tadellos funktioniert hatte.

Gemessen: der Scan-Endpunkt antwortet mit 202 Accepted, Content-Length 0
und ohne Content-Type. Er läuft im Hintergrund weiter, da gibt es noch
nichts zu melden. Der Abruf-Helfer behandelte aber nur 204 als leer und
liess JSON.parse auf eine leere Zeichenkette los.

Jetzt entscheidet der Inhalt, nicht der Status. Eine Liste von
Status-Codes wäre wieder eine, die beim nächsten Endpunkt unvollständig
ist.

Dazu: die Zeile "Metadaten" nannte die Sprache ein zweites Mal, obwohl sie
seit kurzem eine eigene Zeile hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:22:47 +02:00
ScarriffleandClaude Opus 5 98565e14b5 Der Ordner bestimmt die Serie, nicht der Dateiname
Aus einer Serie wurden siebenhundert. Dateien wie "S01E001_Aang.mkv"
tragen vor der Folgennummer nichts, also wurde der ganze Dateiname zum
Serientitel — jede Folge eine eigene Serie. Und umgekehrt ergab
"Doctor Who (2005) - S12E04.mkv" im Ordner "Doctor Who" zwei Serien, weil
die Jahresangabe nur im Dateinamen stand.

Jetzt gilt: was unter Serien/Pingu/ liegt, gehört zu Pingu. Egal wie tief,
egal wie die Datei heisst. Der Ordner ist die verlässlichere Angabe, weil
ihn ein Mensch angelegt hat; so macht es Plex seit je und aus demselben
Grund. Staffel und Folge liest weiterhin der Dateiname.

Ohne bekannte Wurzel bleibt es beim Dateinamen — dort könnte über dem
Staffelordner irgendetwas stehen. Diesen Weg gehen nur die Tests.

Dazu vier Dinge aus der Benutzung:

Die Mediatheken standen scheinbar in zufälliger Reihenfolge. SQLite
vergleicht Text zeichengenau, und dort stehen alle Grossbuchstaben vor
allen kleinen: "Serien" landete über "film". Jetzt ohne Rücksicht auf
Gross- und Kleinschreibung — und umsortierbar über zwei Pfeile. Nicht über
Ziehen: das ist schöner, aber mit der Tastatur unbedienbar und auf dem
Telefon ein Glücksspiel.

Die Sprache des Servers steckte im Dialog "Metadaten", zusammen mit dem
TMDB-Schlüssel — also dort, wo niemand nach einer Sprache sucht. Wer die
Mediathek auf Englisch vorfand, sah nur "Meine Sprache" und hielt das für
die einzige Möglichkeit. Jetzt eine eigene Zeile.

Das Kontextmenü gab es nur per Rechtsklick, und den findet nur, wer ihn
vermutet — auf dem Telefon gibt es ihn gar nicht. Jetzt drei Punkte auf
jeder Kachel, beim Überfahren und bei Tastaturfokus, auf Geräten ohne
Zeiger dauerhaft. Und "Zuordnung korrigieren" steht endlich darin: wer
zwanzig falsch erkannte Titel vor sich hat, soll sie dort geradeziehen
können statt über vierzig Seitenwechsel.

Folgen ohne eigenes Bild nehmen das der Serie. TMDB liefert Folgenbilder
nur für einen Teil der Folgen und bei kleineren Serien für gar keine; eine
graue Fläche sagt nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:20:00 +02:00
ScarriffleandClaude Opus 5 fa18c0d7fa Zwei Namen für denselben Titel brachen den ganzen Lauf ab
Der Eindeutigkeitsindex aus Punkt 3 stellte die Regel auf, dichtete aber
den einen Weg nicht ab, auf dem sie verletzt wird: der Enricher setzt die
TMDB-Kennung, und sobald zwei Einträge derselben Mediathek dieselbe
bekamen, brach EF die gesamte Speicherung ab — mit "An error occurred
while saving the entity changes", einer Meldung, die den Grund nicht nennt
und den ganzen Scan mitnimmt.

Jetzt wird die Kennung nicht blind gesetzt, sondern vorgemerkt und nach
der Schleife zusammengelegt. Zwei Tests halten den Fall fest: der Anbieter
antwortet auf jede Anfrage mit demselben Film beziehungsweise derselben
Serie, also genau die Lage, die vorher den Lauf zerriss.

Dazu der wahrscheinliche Grund für die siebenhundert Serien: der
Serientitel kam ausschliesslich aus dem Dateinamen. Heisst eine Datei nur
"S01E01.mkv", steht vor der Folgennummer nichts, und der ganze Dateiname
wurde zum Serientitel — zwanzig Folgen ergaben zwanzig Serien namens
"S01E01", "S01E02", und so weiter. Jetzt sagt der Ordner den Namen, wenn
die Datei ihn nicht nennt; der Staffelordner wird dabei übersprungen, sonst
hiesse die Serie "Staffel 1". Plex und Jellyfin lesen ihn seit je so.

Dazu die Prüfliste: Folgen werden nach Serie gebündelt, weil zwanzig neue
Folgen eine Entscheidung sind und nicht zwanzig. Mit Mehrfachauswahl —
anhaken und gemeinsam freigeben oder auf einen Zeitpunkt legen. Und der
Zeitpunkt ist ein datetime-local statt eines Textfelds mit Formvorschrift,
bei dem jeder Tippfehler erst nach dem Absenden auffiel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 13:58:33 +02:00
ScarriffleandClaude Opus 5 228c318ad2 Die Medientabelle spricht durchgehend dieselbe Sprache
Die Tonzeile nahm bereits die Formulierung vom Server, die Videozeile baute
ihren Text weiter selbst — "240p (H264)" neben "AAC · Mono", zwei
Schreibweisen in derselben Tabelle untereinander. Die gemessene Auflösung
wandert in den Tooltip, wo sie hingehört: 1040 ist keine Stufe, sondern ein
beschnittenes Bild.

Dazu: ein Spurtitel, der nur die Sprache wiederholt, fällt weg. "Deutsch ·
AAC · Mono · ‚Deutsch'" sagt dasselbe zweimal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 13:19:21 +02:00
ScarriffleandClaude Opus 5 632d3efa35 Neue Funde erst prüfen, dann veröffentlichen
Je Mediathek einstellbar, Vorgabe aus: wer eine Mediathek anlegt, will sie
sehen. Angeschaltet warten neue Funde unter "Zu prüfen", bis jemand sie
freigibt — oder ihnen einen Termin gibt.

Ein Zeitpunkt, kein Zustandsfeld. PublishAt null heisst wartet, in der
Vergangenheit heisst veröffentlicht, in der Zukunft heisst geplant. Die
Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst
umgeschaltet; damit stimmt sie nach einem Neustart, nach einer
Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt gar
nicht lief. Ein Ticker hätte in jedem dieser Fälle das Falsche getan.

Das eigentliche Risiko waren die zwanzig Abfragestellen in vier Dateien.
Schriebe jede ihren eigenen Filter, reichte eine vergessene — und
auffallen würde es nicht beim Programmieren, sondern wenn jemand im
Haushalt einen Titel sieht, den er nicht sehen sollte. Deshalb ein
globaler Abfragefilter in FlaroContext, der sich nicht vergessen lässt,
und eine Middleware, die je Anfrage setzt, wer Unveröffentlichtes sehen
darf. Vorgabe am Kontext ist "alles sehen", damit Scanner und Metadaten
ohne Benutzer arbeiten können; für jede HTTP-Anfrage entscheidet die
Middleware.

Der Test zählt die Inhaltsrouten auf und prüft jede einzeln auf die
Abwesenheit eines wartenden Titels. Eine neue Route, die den Filter
umgeht, fällt dort durch.

Die Migration trägt für alle vorhandenen Titel das Hinzufügedatum als
Veröffentlichung nach. Ohne diese zwei Zeilen stünde jeder auf null — und
der gesamte Bestand wäre für alle ausser Administratoren verschwunden,
sofort beim Start im Container und ohne Meldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 13:03:24 +02:00
ScarriffleandClaude Opus 5 eb63271e92 Ein Kontextmenü auf jeder Kachel — mitsamt Bildauswahl
Gesehen, Favorit und Weglegen gab es ausschliesslich auf der Detailseite.
In einer Übersicht mit zwanzig Titeln hiess "die hab ich alle gesehen"
vierzig Seitenwechsel. Im ganzen Quelltext stand kein einziger
contextmenu-Zuhörer.

Drei Wege hinein, weil es drei Arten gibt zu bedienen: Rechtsklick für die
Maus, Shift+F10 und die Kontextmenü-Taste für die Tastatur, langer Druck
für den Finger. Nur den Rechtsklick anzubieten hiesse, das Menü auf dem
Telefon zu verstecken. Der lange Druck bricht bei jeder Bewegung ab, sonst
löst Scrollen ihn aus.

Die Aktionen wirken auf der Kachel selbst. Die Liste neu zu laden verlöre
Bildlauf und Filter — für einen Haken.

Dazu die Bildauswahl. Es gab genau einen Bild-Endpunkt, und der lieferte
nur aus: wählte TMDB ein schlechtes Plakat, blieb es. Jetzt stehen Plakat,
Hintergrund und Schriftzug zur Wahl, dazu eigene Bilder bis 10 MB. Die
Kandidaten kosten nichts — TMDB liefert sie beim Einzelabruf ohnehin mit,
und bisher wurde alles ausser dem Schriftzug weggeworfen.

Drei Merker je Titel, nicht einer: man kann ein eigenes Plakat wollen und
trotzdem automatische Hintergründe. Ohne sie überschriebe das nächste
Auffrischen jede Handauswahl, und wer einmal ein besseres Plakat gesucht
hat, suchte es jedes Mal neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:43:52 +02:00
ScarriffleandClaude Opus 5 e1399c808f Die Detailseite trägt wieder
Drei Dinge, eins davon war mein Fehlbefund: Regie und Drehbuch werden sehr
wohl angezeigt — nur standen sie irgendwo zwischen dreissig Gesichtern.
Jetzt kommt der Stab zuerst, weil man beim Suchen eher die Regie im Kopf
hat als den zwölften Darsteller.

Bei Zeichentrick hängt TMDB "(voice)" an den Rollennamen, und so stand es
auch da — englisch, mitten im deutschen Text. Daraus wird "Stimme: Gru".

Ähnliche Titel als eigene Reihe, nachgeladen, damit die Seite sofort
steht. Aus dem eigenen Bestand gerechnet, nicht aus TMDBs
Empfehlungsliste: ein Vorschlag, den man nicht abspielen kann, ist
Werbung. Gemeinsame Menschen wiegen dreifach gegenüber gemeinsamen
Genres — dass zwei Filme "Drama" sind, sagt wenig; dieselbe Regie sagt
viel. Kostet keine einzige fremde Anfrage, der Katalog steht ohnehin in
der Datenbank. Findet sich nichts, erscheint die Reihe gar nicht.

Und der Hintergrund: er begann bei left: 240px, also hinter der
Seitenleiste, und war mit opacity .28 und blur(2px) weder Bild noch
Fläche. Jetzt über die volle Breite, kräftiger unscharf — er taucht die
Seite in die Farbe des Films, so wie bei Plex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:37:55 +02:00
ScarriffleandClaude Opus 5 11a44f6a90 Ein Aufmacher über die volle Breite
Die Startseite begann bei einer Reihe aus 150 px breiten Plakaten — das
grösste Element über dem Falz war 225 px hoch. Plex und Netflix öffnen mit
einem Bild über die ganze Breite, und das ist der grösste Einzelunterschied
im Eindruck. Ein Layout-, kein Farbproblem.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Drei Ebenen, weil eine nicht trägt.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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