Commit Graph
21 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 f2b5ee633b Dialoge statt Blöcke, bessere Filter, ein Dashboard — und eine NAS-Anleitung
**Was etwas anlegt oder ändert, steht jetzt in einem Dialog.** Das Formular
zum Anlegen einer Mediathek nahm als Block in der Seite Platz für etwas weg,
das man einmal im Monat braucht — und sah aus wie eine Eingabe, die noch
aussteht. Dasselbe für Benutzer, Umbenennen, den Servernamen und die
Metadaten-Einstellungen.

Gebaut mit dem `<dialog>` des Browsers, nicht mit einem nachgebauten: Escape
schliesst, der Hintergrund wird unbedienbar, der Tastaturfokus bleibt
gefangen, Vorlesesoftware kündigt ihn als Dialog an. Jede Zeile, die das
selbst baut, ist eine Zeile, die es schlechter kann. Scheitert das
Speichern, bleibt der Dialog offen — ihn zu schliessen hiesse, die Eingabe
wegzuwerfen und die Meldung dorthin zu schreiben, wo gerade niemand hinsieht.

**Die Filter sehen nicht mehr nach Eingabe aus.** Bei Plex stehen dort
Wörter mit einem kleinen Pfeil; ein umrandetes Auswahlfeld sagt „hier fehlt
noch etwas", und das stimmt nicht. Gesetzte Filter werden farbig, daneben
steht die Anzahl, und „Filter zurücksetzen" erscheint nur, wenn es etwas
zurückzusetzen gibt. Dazu neu: gesehen / noch nicht gesehen.

Das siebt vorerst in der Oberfläche, nicht am Server — er kann es noch
nicht. Der Preis steht in der Zahl daneben: sie zeigt dann „3 von 7".

---

**„Wird abgespielt" — wer gerade was schaut.**

Abgeleitet aus den Fortschrittsmeldungen, nicht aus einer eigenen Anmeldung
am Spieler: jeder Client meldet beim Abspielen ohnehin alle zehn Sekunden,
wo er steht. Wer das als Lebenszeichen liest, braucht kein zweites
Verfahren, das ein Client vergessen könnte — bleibt die Meldung aus, läuft
der Eintrag nach einer Minute von selbst ab.

Im Arbeitsspeicher, nicht in der Datenbank: „wer schaut gerade was" ist eine
Frage über den Augenblick. Nach einem Neustart lautet die Antwort „niemand",
und das zu speichern hiesse, Wiedergaben anzuzeigen, die es nicht gibt.

Zwei Geräte desselben Menschen sind zwei Zeilen — derselbe Mensch kann auf
dem Fernseher und auf dem Telefon gleichzeitig etwas laufen lassen.
Geschlüsselt wird deshalb über das Gerätetoken, nicht über den Benutzer und
nicht über den Gerätenamen: zwei iPhones heissen beide „iPhone".

**Nur für Administratoren.** Wer wann welchen Film anschaut, ist eine
Auskunft über Menschen, nicht über den Server. In einem Haushalt mag das
harmlos sein; die Vorgabe eines offenen Projekts darf es trotzdem nicht sein.

Der Spieler meldet beim Schliessen ausdrücklich „beendet". Ohne das stünde
ein geschlossener Film noch eine Minute als „läuft" — und wer gerade den
Spieler zugemacht hat, sähe sich selbst beim Schauen zu.

---

**`docs/NAS-EINRICHTEN.md`** beantwortet die Frage, die offen war: **es ist
ein einziges Image.** Die Weboberfläche wird beim Bauen hineingelegt und vom
Server selbst ausgeliefert — ein Container, drei Volumes, kein Proxy, keine
Datenbank daneben.

Drei Wege aufs NAS: über die Container-Registry deines Gitea (die läuft —
nachgeprüft), als Datei über `docker save`, oder dort bauen. Dazu der
Schritt, den man am leichtesten überspringt und am teuersten bezahlt: die
Architektur. Ein Mac baut ohne Angabe arm64, ein Intel-NAS braucht amd64,
und `exec format error` sagt das nicht deutlich.

Eine Zahl darin war geraten und ist korrigiert: `docker save | gzip` ergibt
nicht 90 MB, sondern 230 — die Ebenen eines Images sind bereits gepackt.

378 Tests grün, sechs davon neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:03:07 +02:00
ScarriffleandClaude Opus 5 a7ec3a73ba Angemeldet bleiben — und eine 401 reisst nicht mehr die Sitzung ab
Erst die ehrliche Hälfte: dass es heute **andauernd** abgemeldet hat, geht
auf mein Konto. Ich habe die Datenbank dreimal geleert, um mit frischen
Metadaten zu bauen; jedes Mal verfallen alle Token. Kein Schalter der Welt
hätte das verhindert.

Die andere Hälfte ist ein echter Fehler, und er macht aus einem Aussetzer
eine Abmeldung:

**Eine einzelne 401 warf die Anmeldung weg** — und rief dabei
`/auth/logout`, der Server zog das Token also *aktiv zurück*. War die
Ablehnung nur ein Aussetzer, war eine intakte Anmeldung damit absichtlich
zerstört, und zwar unwiderruflich. Jetzt wird einmal nachgefragt: gilt das
Token wirklich nicht mehr, ist die Sitzung vorbei; antwortet der Server, war
es diese eine Anfrage. Mehrere gleichzeitige Anfragen laufen in dieselbe
Prüfung — sonst stellte eine Startseite mit acht Abfragen achtmal dieselbe
Frage. Antwortet der Server **gar nicht**, ist das kein Beleg gegen das
Token: jemanden abzumelden, weil das WLAN hakt, ist der falsche Schluss.

**Abmelden scheitert nie mehr.** Vorher verlangte der Weg eine gültige
Anmeldung — und genau dann, wenn man ihn am dringendsten braucht, gibt es
keine. Nach dem Leeren des Datenbestands zeigte das Cookie im Browser auf
ein Token, das es nicht mehr gab; es ging bei jedem Bild erneut hinaus, und
der Server lehnte es jedes Mal ab. Achtzig Ablehnungen in einer Stunde, und
nichts davon liess sich von aussen beenden. Jetzt wird das Cookie immer
gelöscht; widerrufen wird, was sich widerrufen lässt.

---

**Und der Schalter, um den es ging.** „Angemeldet bleiben" steht jetzt im
Anmeldefeld, mit Haken als Vorgabe — am eigenen Rechner will sich niemand
täglich neu anmelden.

Umgesetzt über die beiden Speicher, die der Browser ohnehin richtig
aufräumt: mit Haken `localStorage` und ein Cookie für ein Jahr, ohne Haken
`sessionStorage` und ein Sitzungscookie, das mit dem Fenster verschwindet.
Ein eigenes Verfallsdatum zu verwalten wäre die schlechtere Variante — eine
Uhr, die man stellen kann, gegen zwei Mechanismen, die es schon gibt.

Das Token selbst läuft weiterhin **nie** ab; es wird widerrufen, nicht
vergessen. Was hier entschieden wird, ist allein, wie lange der Browser sich
erinnert.

372 Tests grün, vier davon neu. Am Container nachgemessen: mit Haken
`max-age=31536000`, ohne Haken kein `max-age`, und ein Abmelden mit
ungültigem Cookie antwortet 204 statt 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 07:40:19 +02:00
ScarriffleandClaude Opus 5 230beb9664 Mehrere Mediatheken, jede für sich
Bisher gab es in der Seitenleiste „Filme" und „Serien" — zwei feste Zeilen,
hinter denen alles zusammenfiel, was der Server kannte. Wer „Filme" und
„Dokumentationen" getrennt angelegt hatte, sah sie wieder in einem Topf.
Das nahm die Entscheidung zurück, sie zu trennen.

Jetzt führt die Seitenleiste **jede Mediathek einzeln**, mit Namen und Zahl,
wie bei Plex und Jellyfin. Die Startseite bekommt je Mediathek eine Reihe
mit dem zuletzt Hinzugefügten; steht mehr drin, als die Reihe zeigt, führt
„Alle zeigen" dorthin. Die Mediathekenseite trägt den Namen im Titel, sucht
innerhalb der Mediathek und lässt sich sortieren.

**Am Server dafür:**

`?sort=` auf `/movies` und `/series` — `title`, `added`, `year`. Der Titel
immer als letzter Vergleich, sonst hätten zwei Filme aus demselben Jahr keine
feste Reihenfolge und Seite 2 zeigte denselben Film noch einmal oder gar
nicht.

Die Liste der Bibliotheken nennt je Zeile einen Zählwert. „Dokumentationen ·
0" sagt mehr als ein Eintrag, hinter dem sich nach dem Klick nichts findet —
da ist der Unterschied zwischen „noch nicht gescannt" und „falscher Pfad"
wenigstens sichtbar. `null` heisst „nicht gezählt", nicht „leer".

Umbenennen und Entfernen, beides nur für Administratoren. **Umbenennen, mehr
nicht:** die Art zu ändern hiesse, Filme in Serien zu verwandeln; den Pfad
zu ändern hiesse, einen Bestand gegen einen anderen zu tauschen, während die
Einträge stehenbleiben. Beides ist kein Bearbeiten, sondern eine neue
Bibliothek.

**Beim Entfernen bleiben die Mediendateien liegen.** Weg sind der Eintrag,
die Zuordnungen und der Sehfortschritt aller Benutzer dazu; die Oberfläche
sagt das vorher, und eine Prüfung hält fest, dass die Datei danach noch da
ist. Ein Server, der beim Aufräumen in der Oberfläche fremde Filme löscht,
ist einer, den man nicht noch einmal benutzt.

---

**Der Fehler, den der Browser zeigte, und er betrifft jeden Betrieb:** nach
einem Server-Update lief weiter die alte Oberfläche.

Die Dateien unter `assets/` tragen eine Prüfsumme im Namen — ändert sich der
Inhalt, ändert sich der Name. `index.html` trägt keine; sie nennt nur deren
Namen. Lag sie im Zwischenspeicher, bekam der Browser weiter die alte App:
nicht kaputt, sondern veraltet, und das fällt niemandem auf. Nachgemessen:
geladen war `index-C0K3V8eg.js`, ausgeliefert wurde längst
`index-DomHN4_b.js`.

Jetzt zwei Lebensdauern — `no-cache` für die Seite, ein Jahr und `immutable`
für die Bausteine. Mit Prüfung, und am Container nachgemessen.

Der Beispielbestand bekommt zwei weitere Mediatheken, „Dokumentationen" und
„Konzerte". Mit nur Filme und Serien liesse sich gar nicht prüfen, ob
mehrere wirklich getrennt bleiben — zwei Bestände **derselben Art** sind der
Fall, in dem es auffällt.

358 Tests grün. Am Container nachgewiesen: vier Mediatheken in der
Seitenleiste mit Zählwerten, je eine Reihe auf der Startseite, und
„Dokumentationen" zeigt zwei Titel statt sieben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 22:57:25 +02:00
ScarriffleandClaude Opus 5 67cf0fd1d9 HEAD auf den Download, und /api verschluckt nichts mehr
Zwei Fehler, derselbe Grund — und beide zeigte erst der Browser.

**Der Spieler bekam die Webseite statt des Films.** „MEDIA_ELEMENT_ERROR:
Format error", 531 Byte, `Content-Type: text/html`. Das ist genau die Grösse
von `index.html`.

`MapGet` nimmt HEAD nicht an. Die Oberfläche fragt aber mit HEAD nach, ob das
Umpacken fertig ist — einen halbfertigen Film herunterzuladen, nur um das zu
erfahren, wäre absurd. Die Anfrage fiel durch den Methodenabgleich hindurch
bis zum SPA-Rückfall, und der nimmt jeden Pfad ohne Dateiendung. Antwort:
200, mit der Startseite im Rumpf. Der Client hielt das Umpacken für fertig
und reichte HTML an `<video>` weiter.

Jetzt `MapMethods` mit GET und HEAD. Kestrel lässt den Rumpf bei HEAD selbst
weg.

**Und ein vertippter Aufruf sah aus wie ein Erfolg.** `GET /api/v1/gibtsnicht`
gab 200 und die Webseite zurück — derselbe Rückfall. Ein Fehler, der erst
dort auffällt, wo jemand HTML durch einen JSON-Parser schickt.

`app.Map("/api/{*rest}", 404)` davor. Buchstaben schlagen Platzhalter, also
gewinnt jeder echte Endpunkt; was übrig bleibt, ist nicht da und sagt das
auch.

350 Tests grün, die zwei neuen eingerechnet. Am Container nachgemessen: HEAD
auf den Download gibt 200 `video/mp4` mit 71 920 Byte, `/api/v1/gibtsnicht`
gibt 404, und ein Neuladen auf `/verwaltung` bekommt weiterhin die Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:33:56 +02:00
ScarriffleandClaude Opus 5 c4081ff251 Stufe 4: Fortschritt ab der ersten Sekunde, ohne Schwelle
Der Anlass für diesen Server, jetzt als Code. Fortschritt, Gesehen-Status,
Weiterschauen, Nächste Folge, Favoriten und Bewertungen — alles je Benutzer.

**Die Jellyfin-Werte sind jetzt belegt statt behauptet.** Das Entwurfsdokument
hatte das ausdrücklich offengelassen („aus dem Gedächtnis"); hier stehen sie
aus `MediaBrowser.Model/Configuration/ServerConfiguration.cs` und
`Emby.Server.Implementations/Library/UserDataManager.cs`:

| | Vorgabe | Was passiert |
|---|---|---|
| `MinResumePct` | 5 | darunter `positionTicks = 0` — Kommentar im Quelltext: „ignore progress during the beginning" |
| `MaxResumePct` | 90 | darüber gesehen, Position **auf 0** |
| `MinResumeDurationSeconds` | 300 | alles unter fünf Minuten gilt nach dem ersten Lebenszeichen als gesehen |

Bei einem Zweistünder sind die ersten sechs Minuten damit weg. Eine
vierminütige Folge gilt nach dreissig Sekunden als vollständig gesehen. Und
fehlt die Laufzeit, nimmt Jellyfin an, der Titel sei durch — wörtlich:
„we'll just have to assume it was fully played".

**Flaro legt jede Meldung ab und wirft nie eine Position weg.** Eine Grenze
fürs Häkchen gibt es trotzdem — 90 %, sonst stünde jeder zu Ende geschaute
Film für immer bei 98 % in Weiterschauen. Der Unterschied ist nicht, *dass*
es die Grenze gibt, sondern was sie anrichtet: hier wird ein Häkchen gesetzt,
dort zusätzlich die Stelle vernichtet. Wer einen gesehenen Film noch einmal
anfängt, hat beides — das Häkchen und den Punkt, an dem er steht.

Ohne bekannte Laufzeit wird nichts vermutet. Das ist der Fehler, den man
nicht zurücknehmen kann, ohne ihn zu bemerken.

---

**Eine Spalte statt einer zweiten Regel.** „Gehört in Weiterschauen" vergleicht
die Stelle mit einem Anteil der Laufzeit, und diese Rechnung lässt sich nicht
nach SQL übersetzen. Die Regel daneben noch einmal als Abfrage zu schreiben
wäre die Variante, bei der beide irgendwann auseinanderlaufen — unbemerkt,
weil keine von beiden dabei falsch aussieht. Stattdessen hält `InProgress`
das Ergebnis fest, geschrieben an genau einer Stelle. Eine Prüfung hält Reihe
und Regel über vier Stände gegeneinander.

**„Weiterschauen" und „Nächste Folge" überschneiden sich nicht.** Was
angefangen ist, steht links; was ansteht, rechts. Jellyfin zeigt beides
doppelt, und auf einer Startseite mit zwei Reihen nebeneinander sieht das
nach einem Fehler aus.

Dazu, weil die App sie braucht: Einzelabruf für Film und Serie, die nächste
Folge nach einer gegebenen, und der Zustand hängt an jeder Liste mit dran —
in einer Abfrage je Seite, nicht in einer je Titel.

Ein Titel wird über seine Kennung allein angesprochen, ohne dass der Aufrufer
sagen muss, ob es ein Film oder eine Folge ist. Die App hat diese Auskunft an
der Stelle nicht.

348 Tests grün. Im Container nachgewiesen: fünf Minuten „Der Untergang" von
2:36 bleiben stehen, bei Minute 150 kommt das Häkchen und die Stelle bleibt,
beim zweiten Anlauf steht der Film mit beidem wieder in der Reihe — und nach
Folge 1 von Dark zeigt „Nächste Folge" S01E02.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:25:55 +02:00
ScarriffleandClaude Opus 5 91eb934048 Die Weboberfläche — im Zuschnitt von Plex, vom Server selbst ausgeliefert
Einrichtung, Anmeldung, Mediathek, Detailseite mit Abspielen und
Herunterladen, Zuordnung korrigieren, Verwaltung. Die Oberfläche aus der
Jellyfin-Zeit ist dabei vollständig ersetzt: `types.ts`, `prefs.ts`,
`contrast.ts`, `views/cards.ts`, `views/player.ts` und `views/settings.ts`
sprachen Jellyfins Schnittstelle und hatten hier nichts mehr zu suchen.

Seitenleiste links, waagerechte Reihen auf der Startseite, Plakate im
Verhältnis 2:3, unscharfer Hintergrund hinter dem Kopf der Detailseite —
das ist der Zuschnitt von Plex, weil er gefällt. **Die Farbe bleibt Flaros
Orchidee**, nicht Plex' Orange; der Aufbau ist das Vorbild, nicht das
Aussehen.

Welche Bibliothek gezeigt wird, entscheidet die Seitenleiste. Eine zweite
Umschaltung innerhalb der Seite wäre dieselbe Entscheidung an zwei Orten.

---

**Der Zugang liegt jetzt auch in einem Cookie.** Ein `<img>`, ein `<video>`
und ein Download-Link senden keine eigenen Kopfzeilen — eine App kann ihr
Token in jede Anfrage legen, ein Browser nicht. Das Token stattdessen in
die Adresse zu schreiben wäre der naheliegende Ausweg und stünde danach in
jedem Zugriffsprotokoll, in jedem Verlauf und in jedem weitergegebenen
Link.

`HttpOnly`, damit kein eingeschlichenes Skript herankommt. `SameSite=Strict`
gegen Anfragen von fremden Seiten. `Secure` nur über https — sonst liesse
sich die Oberfläche im Heimnetz über http gar nicht benutzen. Die Apps
benutzen weiterhin die Kopfzeile: ein zweiter Weg zum selben Token, kein
zweites Verfahren.

---

**Der Fehler, den erst der echte Container zeigte:** eine weisse Seite,
ohne Hinweis worauf.

Die Rückfallregel verlangt für jede Anfrage ohne eigenen Endpunkt einen
angemeldeten Benutzer, und `/assets/index.js` hat keinen. Die statischen
Dateien standen unter `UseAuthorization` — ein neuer Besucher bekam 401 auf
genau das Skript, das ihm die Anmeldemaske zeichnen sollte. `/` kam durch,
weil der SPA-Rückfall `AllowAnonymous` trägt; alles mit Dateiendung nicht.
Jetzt liegen sie davor. Mit Prüfung.

Beim Bau desselben Images der zweite: seit der Kontext die Projektwurzel
ist, überschrieb `COPY server/` die `obj/`-Verzeichnisse aus dem Container
mit denen vom Arbeitsrechner — die zeigen auf osx-arm64, gebaut wird
linux-arm64, `dotnet publish` brach mit NETSDK1047 ab. Dafür gibt es jetzt
eine `.dockerignore` in der Wurzel; die unter `server/` sucht Docker seit
dem Umzug nicht mehr.

---

Vier Prüfungen schlugen danach fehl, und zwar zu Recht: sie hiessen „ohne
Anmeldung gibt es nichts", benutzten aber den Client, der nach dem Anmelden
ein Cookie hält. Sie prüften also den Angemeldeten. Jetzt sagen sie
ausdrücklich, dass sie ohne Zugang fragen.

313 Tests grün. Im Container nachgewiesen: Anmelden, Plakate, Abspielen,
Staffeln und Folgen, Verwaltung — und ein Neuladen auf `/verwaltung`, das
nicht in einem 404 endet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:07:29 +02:00
ScarriffleandClaude Opus 5 a7cf6c5def Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine
eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt
so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner
Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging
daneben.

**Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den
deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche
Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in
Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store
der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden
frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler.

Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen
ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim
letzten Mal stattfand.

---

2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der
Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`,
`Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen
`FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und
das Image.

**Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen
Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes
Ersetzen hätte den sonst zerlegt.

`docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte
Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das
Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden
in die Irre führt.

**Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag:
`com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat
jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe.

308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS
bauen, das Image baut, und ein frischer Container läuft durch Einrichtung
und Anmeldung.

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