10a5070e58e73d4d4b39e372770fdf493401bc1d
46
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ede4069cd4 |
Der Server weiss jetzt mehr als Titel und Plakat
Bisher standen zu einem Film vier Angaben in der Datenbank: Titel, Jahr, Beschreibung, zwei Bilder. Damit lässt sich eine Kachel zeichnen und sonst nichts — keine Detailseite, die mehr sagt als der Dateiname, keine Vorschläge, kein Filter nach Genre. Alles drei hing daran. Jetzt kommen dazu: **Laufzeit, Bewertung, Werbezeile, Altersfreigabe, Genres, Besetzung, Stab — und der Schriftzug des Titels.** Letzterer ist das, was eine Startseite teuer aussehen lässt: „Blade Runner 2049" in seiner eigenen Schrift über dem Hintergrundbild statt in der Systemschrift. **Alles in einer Anfrage.** `append_to_response` hängt Besetzung, Fremdkennungen, Freigaben und Bilder an den Einzelabruf. Vier getrennte Anfragen je Titel wären bei vierhundert Filmen sechzehnhundert, und TMDB begrenzt, wie oft man fragen darf. Der eigene Abruf der IMDb-Kennung ist dabei entfallen — sie hängt jetzt mit dran. Freigaben sind national: die FSK gilt in Deutschland, die MPAA in den USA, und TMDB führt beide nebeneinander. Genommen wird die des eingestellten Landes. Beim Schriftzug entscheidet zuerst die Sprache und erst danach die Bildbewertung — ein deutscher Titel in deutscher Schrift schlägt den besser bewerteten englischen. Vom Stab nur Regie und Drehbuch. TMDB führt dort auch Beleuchter und Caterer; eine Detailseite, die alle nennt, nennt nichts. **Genres und Personen als eigene Tabellen**, nicht als Text im Film. Sonst: filtern über `LIKE`, Tippfehler als eigenes Genre, und bei einer Umbenennung vierhundert Zeilen anfassen. Personen über die Kennung der Quelle geführt, nicht über den Namen — es gibt zwei Schauspieler namens „Chris Evans". Darauf sitzen zwei neue Endpunkte: ein Filter nach Genre (nur die, die in der Mediathek auch vorkommen — ein Knopf zu einer leeren Liste ist schlimmer als kein Knopf) und **Vorschläge** über die Genres des zuletzt Gesehenen. Keine Empfehlungsmaschine: ein Haushaltsserver hat keine Millionen Sehverläufe, aus denen sich etwas lernen liesse. Eine schlichte Regel ist nachvollziehbar, was eine undurchschaubare Empfehlung nie ist. --- **Zwei Fehler, beide fand erst der echte Lauf.** `UNIQUE constraint failed: Genres.Name`, und damit scheiterte der ganze Scan — in der Mediathek erschien kein einziger Titel. Vier Bibliotheken scannen gleichzeitig, jede mit eigenem Datenbankkontext; zwei stossen auf „Drama", keine sieht die ungespeicherte Zeile der anderen. Statt die Läufe zu serialisieren — das kostete bei zehn Bibliotheken das Zehnfache an Zeit — wird der Fehlschlag hingenommen und die Zeile des anderen benutzt. Sie steht ja genau dafür da. Und: **eine geänderte Sprache erreichte vorhandene Titel nie.** Ein zweiter Scan fragt nichts erneut, das ist Absicht. Wer aber von Englisch auf Deutsch stellt, bekam deutsche Titel erst beim nächsten neuen Film. Dafür gibt es jetzt „Metadaten auffrischen" je Bibliothek — geholt über die bekannte Kennung, nicht über eine neue Suche, damit eine Zuordnung von Hand stehenbleibt. 368 Tests grün, zehn davon neu. Am Container nachgewiesen: „Der Untergang", 156 Minuten, FSK 12, Bewertung 7,9, Genres Drama/Historie/Kriegsfilm, 22 Mitwirkende mit Rollennamen, Schriftzug und Hintergrund da — und nach dem Umstellen auf de-DE heisst er nicht mehr „Downfall". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
f98b995050 |
Beiwerk im Medienordner, und eine Zuordnung lässt sich korrigieren
Plakate, Hintergründe und NFO-Dateien landen jetzt neben dem Film — und eine
falsche Zuordnung lässt sich richtigstellen, ohne dass der nächste Scan sie
wieder überschreibt.
Der Untergang (2004)/
├── Der Untergang (2004).mkv
├── poster.jpg
├── fanart.jpg
└── movie.nfo
Bei Serien liegt `tvshow.nfo` eine Ebene über dem Staffelordner, je Folge
kommen NFO und Standbild daneben. Liegen mehrere Filme in einem Ordner, heisst
das Beiwerk `Titel (Jahr)-poster.jpg` — sonst überschrieben sie sich
gegenseitig.
**Die Erlaubnisliste ist der ganze Schutz.** Damit das geht, muss die
Medieneinhängung ohne `:ro` auskommen, und damit könnte der Server dort alles
anstellen. Dass er es nicht tut, hängt an `SidecarNames`: sechs Arten von
Datei, nichts sonst. Ein Name, der nicht daraufsteht, wirft — laut, nicht
still, denn das wäre ein Fehler im Programm. Mediendateien werden nie
geändert, umbenannt oder gelöscht; es gibt keine Stelle, die das könnte.
Geprüft an echten Dateien, einschliesslich der Versuche mit `../`, mit
absoluten Pfaden und mit einer Mediendatei als Ziel.
Standardmässig **aus**. In fremde Medienordner zu schreiben ist nichts, was
ein Server ungefragt tun sollte.
**Gelesen wird auch ohne Schreiberlaubnis.** Eine vorhandene NFO ist eine
Zuordnung, die jemand getroffen hat — Halo nimmt sie und sucht gar nicht
erst. Dasselbe gilt für Ordnernamen der Form `[tmdbid-613]`. Halo liest sie
und benennt **nichts** um: Umbenennen ist der einzige Eingriff, der eine
Bibliothek wirklich zerlegen kann.
Nebenbei: eine NFO aus dem Medienordner hat Halo nicht geschrieben. DTDs sind
beim Lesen abgeschaltet — eine untergeschobene Datei mit externer Entität
könnte sonst den Server dazu bringen, `/config/halo.db` einzulesen und als
Titel auszuliefern. Mit Prüfung.
---
**Der Fehler, den erst der echte Lauf zeigte** — und er ist die zweite
Auflage eines schon einmal behobenen:
Nach der Korrektur stand im Ordner „Dark (2017)", in der Datenbank aber 2004.
Der nächste Scan suchte nach *(Dark, 2017)*, fand nichts, legte den Film neu
an und löschte den korrigierten als verwaist. Überlebt hat die Zuordnung nur,
weil sie in der NFO stand — das System hat sich selbst gerettet, aber der
Eintrag war ein anderer und das Jahr wieder falsch.
Dieselbe Klasse wie bei `ParsedTitle`: zugeordnet wurde über einen Wert, den
die Metadatenquelle verändert. Jetzt gibt es `ParsedYear`, und das ändert sich
nie. Mit Prüfung, die genau diesen Ablauf festhält.
---
Dazu von TMDB: die IMDb-Kennung wird nachgeschlagen, damit sie in der NFO
steht — wer später auf ein anderes Werkzeug wechselt, findet denselben Titel
wieder, auch wenn das TMDB gar nicht kennt.
308 Tests grün. Im Container nachgewiesen: Zuordnung falsch, korrigiert,
zweiter Scan — derselbe Eintrag, dieselbe Kennung, Jahr bleibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
79b6001f86 |
Metadaten ohne Einrichtung — mit Lese-Token statt API-Schlüssel
Halo bringt einen TMDB-Zugang mit. Wer den Server startet, bekommt Titel, Beschreibungen und Plakate, ohne irgendwo ein Konto anzulegen. Am echten Dienst nachgewiesen: „Der Untergang" (2004) mit deutscher Beschreibung, „Blade Runner 2049" (2017), Plakate als JPEG aus Halos eigener Ablage. **Warum überhaupt die Frage stand.** Jellyfin liefert ebenfalls einen Schlüssel mit — fest im Quellcode, offen im Repository, Konstante `ApiKey` in `TmdbUtils.cs`. Ein Abkommen mit TMDB dazu liess sich nicht belegen: deren Bedingungen erwähnen das Teilen von Schlüsseln mit keinem Wort, und die einzige Äusserung eines Moderators zu genau dieser Frage im TMDB-Forum ging nicht darauf ein, sondern wies nur darauf hin, dass Missbrauch zur Sperrung führt. Geduldet also, nicht gesegnet. Deshalb **mit Ausweg**: wer einen eigenen Zugang hinterlegt, benutzt den. Würde der mitgelieferte je gesperrt, steht damit nicht jede Installation still. **Ein Lese-Token, kein API-Schlüssel.** TMDBs eigene Dokumentation bestätigt, dass er auch an den v3-Endpunkten gilt. Zwei Gründe, und beide zählen bei einem Zugang, der öffentlich im Repo steht: er kann nur lesen, und er geht als `Authorization`-Kopfzeile hinaus statt als `?api_key=` in der Adresse — und eine Adresse steht in jedem Zugriffsprotokoll und jedem Reverse Proxy. Der ältere Schlüssel wird weiterhin angenommen; er kann nur in die Adresse. **Der Hinweis auf TMDB ist Bedingung.** Logo und ein fester Satz, so verlangt es deren Lizenz. `system/info` liefert beides ohne Anmeldung, damit ein Client es auch auf dem Anmeldebildschirm zeigen kann. Das fehlte bisher. --- **Zwei Funde, beide nur durch einen echten Lauf sichtbar:** *„Dark" wurde zu „Dark Shadows" von 1966.* TMDB sortiert nach Beliebtheit, und ohne Jahresangabe führt das in die Irre. Jetzt sticht ein Treffer mit genau diesem Namen den beliebteren — danach: „Dark" (2017), deutsche Beschreibung, Folgentitel „Geheimnisse" und „Lügen". Nur bei Gleichheit, nicht bei Ähnlichkeit: eine eigene Bewertung wäre TMDBs Rangfolge nachgebaut, mit weniger Daten. *Die Tests griffen plötzlich übers Netz zu.* Sobald ein Schlüssel mitgeliefert wird, fragt jeder Scan wirklich bei TMDB nach — und prompt hiess „Der Untergang" im Test „Downfall", weil die Voreinstellung Englisch ist. Genau das, was ein Testwerk nicht tun darf: am WLAN hängen und an fremden Daten. Der Anbieter ist jetzt für alle Tests abgeschaltet, ausser ein Test schiebt ausdrücklich einen eigenen nach. 238 Tests grün, keiner davon geht ins Netz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e4a90fce4e |
Stufe 3: Download mit allen Ton- und Textspuren
**Der Grund, warum es diesen Server gibt.** Eine MKV mit zwei Tonspuren geht
hinein, eine MP4 mit zwei Tonspuren kommt heraus — mitsamt Sprachkennung und
Spurnamen. Im Container nachgewiesen, nicht nur im Test:
0 video h264 und
1 audio aac deu Deutsch
2 audio aac deu Audiodeskription
3 subtitle mov_text deu
Jellyfins Transcode-Endpunkt bildet je Sitzung **eine** Tonspur ab und
verliert beim Umpacken zusätzlich die Sprachkennungen. Der Plan hier hat gar
kein Feld „welche Tonspur" — es gibt nichts auszuwählen, weil alle mitgehen.
**Vorher fragen können.** `GET /files/{id}/delivery` sagt, was geschehen wird:
umgepackt oder nicht, welche Tonspuren neu gerechnet werden müssen, welche
Untertitel nicht mitkönnen. Drei Gigabyte zu laden und erst danach zu merken,
dass die falsche Tonspur drin ist, war der Anlass für das Ganze.
**Umgepackt wird auf die Platte, nicht in die Antwort.** Der bequeme Weg wäre,
ffmpeg durchzureichen. Dann kennt aber niemand die Länge im Voraus: kein
Fortschrittsbalken, kein Fortsetzen, kein Springen. Jetzt kommt beim ersten
Abruf ein 202 mit Fortschritt, danach die fertige Datei mit `Content-Length`
und `Accept-Ranges`.
**Bildbasierte Untertitel fallen weg, und das steht im Plan.** PGS passt in
keine MP4 — betrifft fast jeden Blu-ray-Rip. Stillschweigend wegzulassen
hiesse, jemanden nach Untertiteln suchen zu lassen, die der Server bewusst
entfernt hat.
---
**Zwei Funde aus echten Messungen:**
*Die Spurnamen überlebten das Umpacken nicht* — „Deutsch" kam als `null` an,
und damit wären zwei deutsche Tonspuren hinterher nicht mehr zu unterscheiden
gewesen. Ein Versuch an einer echten Datei zeigte den Grund: **MP4 nennt das
Feld `name`, Matroska nennt es `title`.** ffmpeg schreibt das eine ins andere
um; mein Leser kannte nur `title`. Keine Grenze des Formats, ein Lesefehler.
`handler_name` steht ausdrücklich nicht in der Suche — dort schreibt ffmpeg
„SoundHandler" hinein.
*Der Image-Bau brach mit Code 100 ab.* `intel-media-va-driver` gibt es für
arm64 nicht, und die Meldung verrät nicht, welches von sechs Paketen fehlt.
Die Intel-Treiber kommen jetzt nur auf amd64 dazu; `mesa-va-drivers` bedient
AMD auf beiden.
---
**Die ffmpeg-Zeile ist eine reine Funktion und wird ohne Ausführen geprüft.**
Hier liegt die Erfahrung dieser Woche: eine falsche Parameterkombination
kostete Tage, weil sie nur gegen einen echten Server zu entdecken war. Dazu
gehört die Verwechslung, vor der jetzt ein Test steht: `-c:a:1` meint die
zweite Tonspur der **Ausgabe**, nicht Spur 1 der Datei.
ffmpeg läuft als eigener Prozess, nicht als eingebundene Bibliothek — das
hält die Lizenzen getrennt und lässt einen Absturz dort den Server nicht
mitnehmen.
223 Tests grün (98 Kern, 31 Metadaten, 26 Medien, 68 Schnittstelle).
Image baut für amd64 und arm64, mit ffmpeg 6.1.1 und VAAPI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
acfe2668a8 |
Stufe 2b: Metadaten von TMDB — und ein Testlauf, der 19-mal schneller ist
Titel, Beschreibungen und Bilder kommen jetzt von TMDB. Dazu zwei Fehler, die erst das Zusammenspiel zeigte. **Der Anbieter steckt hinter einer Schnittstelle, und das ist kein Vorrat für einen zweiten.** Es geht darum, dass alles, was darauf aufbaut, *ohne Netz und ohne Schlüssel* prüfbar bleibt. Ein Projekt, dessen Tests einen fremden Dienst brauchen, hat am Ende keine Tests: sie scheitern, wenn das WLAN hakt, und irgendwann schaltet sie jemand ab. **Ein Fehlschlag bei TMDB ist kein Abbruch.** Kein Netz, Kontingent erschöpft, ein Anmeldeportal im Hotel-WLAN, das HTML mit Status 200 schickt — in allen Fällen bleibt der Titel beim Dateinamen und der Scan läuft durch. Die eine Ausnahme ist ein zurückgewiesener Schlüssel: „nichts gefunden" und „dein Schlüssel ist falsch" sehen in der Mediathek gleich aus, und ohne diese Unterscheidung sucht jemand stundenlang. Der Schlüssel steht im Scanbericht. **Der Schlüssel kommt nie wieder heraus.** Abgefragt wird nur, ob einer da ist — sonst gäbe die Oberfläche jedem Angemeldeten das TMDB-Kontingent des Betreibers in die Hand. Geprüft, dass er nicht einmal versehentlich im Antwortkörper steht. **Bilder holt die App bei Halo, nicht bei TMDB.** Sonst bräuchte sie Internet für ein Plakat, das zehn Zentimeter weiter auf der Platte liegt — und TMDB erführe, wer wann was ansieht. Der Schlüssel ist der Hash der Quelladresse, also lädt dasselbe Plakat nie zweimal. Dass daraus kein Weg ins Dateisystem wird, ist eigens geprüft: ohne die Formprüfung wäre `/api/v1/images/../../config/halo.db` ein Weg, die Datenbank samt aller Token-Fingerabdrücke herunterzuladen. --- **Der erste Fehler: nach dem Eintragen der Metadaten legte der nächste Scan den Film erneut an.** Zugeordnet wurde über den Titel — und den hatte TMDB gerade von „The Matrix" auf „Matrix" geändert. Beim übernächsten Lauf dasselbe noch einmal. Das ist die Sorte Fehler, die man erst nach Wochen bemerkt, wenn jeder Film fünfmal dasteht. Jetzt gibt es `ParsedTitle`: den Titel aus dem Dateinamen, der sich nie ändert, und nur der entscheidet beim Zuordnen. **Der zweite Fehler kostete 15 Minuten je Testlauf.** Zwei Tests löschen die Pfadvariablen, um den Rückfall auf die Voreinstellung zu prüfen — und liessen sie gelöscht zurück. Jeder danach gestartete Testserver fiel dann auf `<Projektverzeichnis>/data` zurück, und Dutzende Server teilten sich **eine** SQLite-Datei, auf die sie der Reihe nach warteten. Dazu wartete jeder Host beim Herunterfahren die vollen fünf Sekunden. Gefunden durch Halbieren, nicht durch Raten: einzeln war jede Testklasse schnell, zusammen nicht. Der Beweis lag am Ende als leeres Verzeichnis `src/Halo.Api/data` auf der Platte. **15 Minuten 32 → 49 Sekunden.** 180 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78b7e3bcb8 |
Stufe 2a: Der Bestand wird gelesen, nicht mehr nur verwaltet
Dateinamen erkennen, Verzeichnisse durchlaufen, abgleichen, blättern. Noch ohne TMDB — die Titel kommen vorerst aus den Dateinamen, und genau daran entscheidet sich später, ob die Metadatenquelle etwas findet. **Der Dateiname ist die Stelle, an der Medienserver in der Praxis scheitern.** Was auf der Platte liegt, heisst nicht „Der Fall", sondern `Notruf.Hafenkante.S01E01.German.1080p.WEB-DL.x264-GRP.mkv`. Das Verfahren schneidet ab dem ersten Technik-Merkmal ab, statt einzelne Tags herauszulösen: Letzteres klingt gründlicher, lässt aber bei jedem unbekannten Tag Bruchstücke stehen — und unbekannte Tags gibt es immer. Drei Fälle, an denen sich die Regeln entscheiden, alle geprüft: - **„Blade Runner 2049" behält seine Zahl.** Eine Jahreszahl muss plausibel sein; was weit in der Zukunft liegt, gehört zum Titel. Ohne diese Grenze hiesse der Film „Blade Runner", erschienen 2049, und keine Metadatenquelle fände ihn so. - **„S.W.A.T." behält seine Punkte.** Trennpunkte werden nur dort zu Leerzeichen, wo sonst keine sind — stehen welche im Namen, hat jemand von Hand benannt, und seine Punkte sind gewollt. (Der Schlusspunkt fiel erst einem Test auf.) - **Ohne Folgennummer wird nichts geraten.** Eine erfundene Staffel 1 landete unter einer Serie, zu der die Datei nicht gehört. **`@eaDir` ist der Grund, warum der Walker von Hand absteigt.** Synologys Vorschauablage enthält eine Kopie *jeder* Datei; mit `SearchOption.AllDirectories` — der bequemen Fassung — stünde jeder Film zweimal in der Mediathek, zur Hälfte als unbrauchbare Kopie. Äste beschneiden kann nur, wer selbst absteigt. Ein gesperrter Unterordner bricht den Lauf nicht ab, sondern wird vermerkt. **Die Datei zeigt auf den Titel, nicht umgekehrt.** Sieht herumgedreht aus und ist Absicht: ein Film liegt oft als 4K *und* als 1080p vor. Trüge der Film den Verweis auf seine eine Datei, wäre die zweite Fassung ein Umbau am Datenmodell — so ist sie eine Zeile mehr. Geprüft. **Der Scan läuft im Hintergrund**, nicht in der Anfrage: ein Bestand mit 40 000 Titeln braucht Minuten, und eine so lange offene Anfrage läuft hinter jedem Reverse Proxy ins Zeitlimit. Einer je Bibliothek, sonst löschen sich zwei Läufe gegenseitig die Einträge. Aufgeräumt wird über den Zeitstempel des Laufs — billiger, als sich alle gesehenen Pfade zu merken. Zweimal scannen ergibt dasselbe wie einmal. Das ist der Fehler, den man sonst erst nach Wochen bemerkt, wenn jeder Film fünfmal dasteht. 131 Tests grün (88 Kern, 43 Schnittstelle). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
156c740330 |
Halo-Server, Stufe 1: Benutzer, Anmeldung, Einrichtung
Der Anfang eines eigenen Medienservers — C#, .NET 10, SQLite, im Container für amd64 und arm64. Stufe 1 von fünf: er spielt noch keinen Film ab, aber ein Fremder kann ihn in Betrieb nehmen, und genau daran scheitern solche Projekte sonst als Erstes. **Was läuft, gegen den laufenden Container gemessen** — nicht gegen einen Prüfstand: Gesundheitsfrage, Einrichtungsassistent, zweiter Einrichtungsversuch abgelehnt (409), Anmeldung, falsches Passwort abgelehnt (401), und der ganze Weg der Code-Anmeldung vom Bildschirm bis zum Token. **Drei Entscheidungen, die beim Lesen Fragen aufwerfen:** *Zeitpunkte liegen als Unix-Millisekunden in der Datenbank.* SQLite hat keinen Zeittyp; EF legt einen `DateTimeOffset` sonst als Text mitsamt Zonenversatz ab, und dann steht `…T12:00+02:00` neben `…T11:00Z` — derselbe Augenblick, als Text falsch herum sortiert. EF weigert sich deshalb, `ExpiresAt > jetzt` überhaupt zu übersetzen; das war der erste echte Fehlschlag und ist jetzt eine Typumwandlung für das ganze Modell statt je Eigenschaft. *Token sind keine JWT.* Der Server schlägt sie nach, statt sie zu prüfen — nur so lässt sich ein verlorenes Telefon sofort aussperren. In der Datenbank steht allein der Fingerabdruck. *Der Code am Fernseher holt nie selbst ein Token ab.* Dafür gibt es den Abholschein, den nur das anfragende Gerät kennt. Ohne ihn wären sechsstellige Codes ein Türschloss mit einer Million Schlüsseln — in Minuten durchprobiert. Dass ein erratener Schein nichts bekommt, ist die wichtigste der 55 Prüfungen. **Zwei Fehler, die erst der echte Container zeigte:** `EnsureExists` legte nur an und prüfte nicht, ob sich schreiben lässt — bei einem vorhandenen, aber gesperrten Verzeichnis meldete es Erfolg, und der Server starb zehn Zeilen später an „unable to open database file". Diese Meldung nennt weder das Verzeichnis noch verrät sie, dass es um Dateirechte geht. Jetzt wird tatsächlich geschrieben, und die Meldung nennt Variable, Pfad, die eigene Kennung und den nächsten Schritt. Und: Docker legt den Einhängepunkt eines frischen benannten Volumes als `root` an, wenn das Verzeichnis im Image fehlt. Das traf jeden, der ohne Bind Mount startet. Die drei Verzeichnisse werden jetzt im Image angelegt und übereignet. **Die Lizenz ist eine Bauvorschrift, keine Fussnote.** GPL-2.0-only verträgt sich nicht mit Apache-2.0 — dessen Patentklausel gilt unter Abschnitt 6 als „weitere Einschränkung". Darum steht in `LICENSES.md`, dass vor jedem `PackageReference` die Lizenz zu prüfen ist, und darum ist das Testwerk NUnit (MIT) und nicht xUnit (Apache-2.0). Alle vier Pakete sind geprüft MIT. Die Apps stehen unter MPL-2.0 statt GPL, weil GPL und App Store sich nicht vertragen — Apple entfernte 2011 VLC aus dem Store, nachdem ein Rechteinhaber sich auf Abschnitt 6 berufen hatte. 55 Tests grün. Image baut für linux/amd64 und linux/arm64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |