Serie für Serie, Film für Film — statt einer Zeile mit einer GUID

Die Markensuche lief stundenlang über 95 Serien, 4213 Folgen und 453
Filme und schrieb dabei fünf Zeilen, in denen Kennungen standen statt
Namen. „Markenerkennung für 4f3a…: 12 Marken." beantwortet keine der
Fragen, die in den letzten Tagen tatsächlich aufkamen — welche Staffel
nichts bekommen hat, warum, und ob die Serie überhaupt dran war.

Jetzt nennt jede Zeile den Titel, und es gibt eine je Serie, je Staffel
und je Folge:

    „Biohackers": 2 Staffel(n), 18 Folge(n).
    „Biohackers", Staffel 2: Vorspann in 8 von 8 Folge(n), Abspann in 1 von 8.
    Biohackers S02E01 · Neuanfang: Vorspann 0:31–1:24, Abspann ab 41:12 (über das Bild).
    Biohackers S02E07 · Letzte Fahrt: Vorspann 0:31–1:24, kein Abspann.
    „Biohackers" fertig: 14 Marke(n).

Die Folgenzeilen werden **aus der Datenbank gelesen, nicht mitgezählt**:
so steht dort, was danach wirklich an der Folge hängt, und nicht, was
dieser eine Lauf geschrieben hat. Nur das beantwortet „warum kommt bei
dieser Folge kein Knopf?".

**Drei Gründe statt eines `false`.** `FilmAsync` gab für „keine Datei",
„schon durchsucht" und „nichts gefunden" dasselbe zurück, und von aussen
waren sie nicht zu unterscheiden. Jetzt sagt jeder seinen Namen.

**Und eine Live-Zeile bei der Mediathek**, wie der Scan sie hat. Sie
benutzt denselben Mechanismus — `Scanschritt` mit einer neuen Phase,
derselbe Endpunkt, ein eigenes Feld. Ein eigenes, weil der Scan sich auf
„fertig" setzt, sobald er die Markensuche angestossen hat; in sein Feld
geschrieben sähe er wieder laufend aus.

**Zwei Fehler sind dabei herausgefallen.** In `NachScanAsync` stand
`if (serien.Count == 0) return;` vor dem Filmteil — eine reine
Filmmediathek bekam nach einem Scan also nie eine Abspannsuche. Und die
fpcalc-Prüfung stand ganz oben und galt für beides, obwohl der Bildweg
fpcalc gar nicht braucht: ohne fpcalc fand der Server keinen einzigen
Filmabspann.

Dazu ein Suchfeld im Protokoll und tausend Anfangszeilen statt
dreihundert. Ohne das wäre eine Zeile je Folge keine Auskunft, sondern
eine Mauer — genau der Fall, gegen den sich `ScanCoordinator` für den
Scan schon einmal entschieden hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-15 13:11:58 +02:00
co-authored by Claude Opus 5
parent 70ab4cc421
commit 767491ef03
8 changed files with 460 additions and 77 deletions
+5
View File
@@ -196,6 +196,11 @@ export type Scanstand = {
missing: number | null;
/** Woran der Lauf gerade ist, in einem Satz. */
step: string | null;
/** Woran die Markensuche gerade ist — `null`, wenn keine läuft.
*
* **Ein eigenes Feld, nicht `step`.** Sie hängt sich hinter den Scan und
* läuft weiter, wenn der längst „fertig" meldet. */
marken?: string | null;
problems: { path: string; reason: string }[] | null; error: string | null;
metadata: { moviesMatched: number; seriesMatched: number; episodesMatched: number;
skipped: boolean; error: string | null } | null;
+13 -2
View File
@@ -238,8 +238,10 @@ export function bibliotheksbereich(
+ 'werden noch einmal geprüft.',
bestaetigen: 'Suchen',
tun: async () => {
laufendSetzen(true);
await mediathekMarken(aktuell.id, true);
hinweisZeigen('Die Suche läuft. Das Protokoll von heute zeigt den Fortschritt.');
hinweisZeigen('Die Suche läuft. Der Stand steht hier und im Protokoll von heute.');
void verfolgen(false);
},
});
@@ -310,6 +312,13 @@ export function bibliotheksbereich(
laufendSetzen(false);
stand.textContent = s.error ?? 'Gescheitert.';
return;
} else if (s.marken) {
// **Die Markensuche hängt sich hinten an und läuft weiter.**
// Sie dauert Stunden, wo der Scan Minuten dauert; ohne diesen
// Zweig stünde hier längst das Ergebnis des Scans, während der
// Server noch jede Folge durchhört.
laufendSetzen(true);
stand.textContent = s.marken;
}
// **Alle Zähler auf demselben Takt.**
@@ -325,7 +334,9 @@ export function bibliotheksbereich(
// Mediathek, ohne je Sekunde zwei Abrufe zu kosten.
if (melden && i > 0 && i % 3 === 0) geaendert(await laden());
if (s.state === 'finished') {
// Ein fertiger Scan ist nicht das Ende, solange die Markensuche
// dahinter noch läuft — sonst fiele die Zeile mitten im Lauf weg.
if (s.state === 'finished' && !s.marken) {
const teile = [`${s.filesFound} Dateien`, `${s.added} neu`, `${s.removed} entfernt`];
// „ohne Folgennummer" stimmte nicht mehr: gezählt wird jetzt auch,
// was eine Nummer trägt, aber zu keiner Serie gehört.
+46 -4
View File
@@ -20,8 +20,14 @@ import { abschnitt } from './teile';
* ihm die Seite aus der Hand nehmen.
*/
/** Wie viele Zeilen anfangs geholt werden. */
const ANFANG = 300;
/** Wie viele Zeilen anfangs geholt werden.
*
* **Tausend statt dreihundert.** Seit die Markensuche je Folge eine Zeile
* schreibt, hat ein Tag, an dem sie lief, Tausende davon — mit
* dreihundert sah man vom übrigen Tag nichts mehr. Das Suchfeld unten
* macht die Menge dann wieder beherrschbar.
*/
const ANFANG = 1000;
/** Wie oft nachgesehen wird, solange die Seite offen ist. */
const TAKT = 4000;
@@ -30,6 +36,24 @@ export function protokollbereich(): HTMLElement {
const bereich = document.createElement('section');
bereich.appendChild(abschnitt('Protokoll'));
// **Ohne Suche ist ein langes Protokoll keine Auskunft.** An einem Tag,
// an dem die Markensuche lief, stehen hier Tausende Zeilen; die eine
// Serie darin von Hand zu finden hiesse scrollen, bis man aufgibt.
const suchreihe = document.createElement('div');
suchreihe.className = 'knopfreihe';
const suche = document.createElement('input');
suche.type = 'search';
suche.className = 'feld';
suche.placeholder = 'Im Protokoll suchen …';
suche.setAttribute('aria-label', 'Protokoll durchsuchen');
const treffer = document.createElement('span');
treffer.className = 'winzig';
suchreihe.append(suche, treffer);
bereich.appendChild(suchreihe);
const liste = document.createElement('div');
liste.className = 'protokollliste';
liste.innerHTML = '<div class="spinner"></div>';
@@ -97,18 +121,36 @@ export function protokollbereich(): HTMLElement {
}
}
// Beim Tippen neu zeichnen — die Zeilen liegen schon da, es wird nichts
// nachgeholt.
suche.oninput = () => zeichnen(zuletzt);
function zeichnen(roh: string): void {
const zeilenListe = roh.split('\n').filter(z => z.trim().length > 0);
const alle = roh.split('\n').filter(z => z.trim().length > 0);
const gesucht = suche.value.trim().toLowerCase();
const zeilenListe = gesucht
? alle.filter(z => z.toLowerCase().includes(gesucht))
: alle;
// **Die Trefferzahl gehört daneben.** Ein leeres Ergebnis ohne Zahl
// sieht aus wie ein kaputtes Protokoll statt wie „nichts gefunden".
treffer.textContent = gesucht
? `${zeilenListe.length} von ${alle.length} Zeilen`
: '';
// **Vor dem Neuzeichnen merken, ob man unten stand.** Danach ist die
// Höhe eine andere, und die Frage liesse sich nicht mehr beantworten.
const amEnde = liste.scrollHeight - liste.scrollTop - liste.clientHeight < 40;
liste.innerHTML = '';
if (zeilenListe.length === 0) {
if (alle.length === 0) {
liste.appendChild(leer('Heute noch nichts protokolliert.'));
return;
}
if (zeilenListe.length === 0) {
liste.appendChild(leer(`Keine Zeile enthält „${gesucht}".`));
return;
}
zeilenListe.forEach(z => liste.appendChild(protokollzeile(z)));
if (amEnde) liste.scrollTop = liste.scrollHeight;