Files
Flaro/web/app/src/zuordnung.ts
T
ScarriffleandClaude Opus 5 c3124ba27c Zwei Teile, ein Treffer — und einer verschwand
Du hast drei Filme vermisst, und die Zahlen sagten schon, wo sie sind: 448
Filme plus 13 zweite Fassungen sind genau die 461 Dateien. Es fehlte keine
Datei; drei Filme steckten als Fassung in einem anderen.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 13:21:42 +02:00

214 lines
7.9 KiB
TypeScript

import { vorschlaege, zuordnen, herkunft, type Zuordnungsstand } from './api';
import { esc, knopf, feld, fehlerzeile, leer, meldung } from './util';
import { icons } from './icons';
import { beimSeitenwechselSchliessen } from './dialog';
/** Einen Titel einem anderen Eintrag der Metadatenquelle zuordnen.
*
* **Als eigener Dialog und nicht nur auf der Detailseite.** Wer in der
* Mediathek zwanzig falsch erkannte Titel vor sich hat, soll sie dort
* geradeziehen können — vierzig Seitenwechsel sind keine Bedienung.
*
* `fertig` wird gerufen, wenn etwas zugeordnet wurde; der Aufrufer frischt
* damit auf, was er zeigt.
*/
export function zuordnungsdialog(
art: 'movies' | 'series',
id: string,
titel: string,
jahr: number | null,
/** Wird nach einer übernommenen Zuordnung gerufen — mit dem neuen Stand.
*
* **Der Stand sagt auch, ob zusammengelegt wurde.** Dann gibt es den
* korrigierten Eintrag nicht mehr, und wer weiter auf ihn zeigt, bekommt
* beim nächsten Handgriff eine 404. */
fertig?: (stand: Zuordnungsstand) => void,
): void {
const d = document.createElement('dialog');
d.className = 'dlg dlg-breit';
const kopf = document.createElement('div');
kopf.className = 'dlg-kopf';
const h = document.createElement('h2');
h.textContent = 'Zuordnung korrigieren';
const zu = document.createElement('button');
zu.className = 'dlg-zu';
zu.innerHTML = icons.schliessen;
zu.setAttribute('aria-label', 'Schliessen');
zu.onclick = () => d.close();
kopf.append(h, zu);
const koerper = document.createElement('div');
koerper.className = 'dlg-koerper';
d.append(kopf, koerper);
d.addEventListener('close', () => d.remove());
d.addEventListener('click', e => { if (e.target === d) d.close(); });
document.body.appendChild(d);
d.showModal();
beimSeitenwechselSchliessen(d);
zuordnungsflaeche(koerper, art, id, titel, jahr, stand => {
d.close();
fertig?.(stand);
});
}
/** Dieselbe Zuordnungssuche, aber in eine beliebige Fläche gezeichnet.
*
* **Damit sie als Reiter in „Bearbeiten" stehen kann** — statt als zweiter
* Dialog über dem ersten. Der eigene Dialog bleibt für alle, die nur die
* Zuordnung wollen; er ist jetzt nur noch ein Rahmen um diese Fläche.
*
* `fertig` wird nach einer übernommenen Zuordnung gerufen. Ob daraufhin etwas
* geschlossen wird, entscheidet der Aufrufer — in einem Reiter wäre das
* falsch, in einem eigenen Dialog richtig.
*/
export function zuordnungsflaeche(
koerper: HTMLElement,
art: 'movies' | 'series',
id: string,
titel: string,
jahr: number | null,
fertig?: (stand: Zuordnungsstand) => void,
): void {
koerper.innerHTML = '';
// **Woher der Titel stammt, über allem anderen.**
//
// Liegt die Zuordnung völlig daneben, sagt der angezeigte Titel nichts mehr
// darüber, welche Datei hier eigentlich gemeint ist — „Dark Shadows" von
// 1966 kann alles sein. Der Ordner sagt es in einer Zeile.
const quelle = document.createElement('div');
quelle.className = 'quelle';
quelle.innerHTML = '<span class="winzig">Herkunft wird gelesen…</span>';
void herkunft(art, id).then(h => {
quelle.innerHTML = '';
const ordner = document.createElement('div');
ordner.className = 'quelle-pfad';
ordner.textContent = h.ordner ?? 'Kein gemeinsamer Ordner';
ordner.title = h.ordner ?? '';
quelle.appendChild(ordner);
const dazu = document.createElement('span');
dazu.className = 'winzig';
const namen = h.beispiele.join(' · ');
dazu.textContent = h.dateien === 0
? 'Keine Datei mehr zugeordnet.'
: `${h.dateien} ${h.dateien === 1 ? 'Datei' : 'Dateien'}`
+ (namen ? ` — ${namen}${h.dateien > h.beispiele.length ? ' …' : ''}` : '');
quelle.appendChild(dazu);
}).catch(() => {
quelle.innerHTML = '<span class="winzig">Herkunft nicht lesbar.</span>';
});
const hinweis = document.createElement('p');
hinweis.className = 'hinweis';
hinweis.textContent = 'Der gewählte Eintrag ersetzt Titel, Beschreibung, Bilder '
+ 'und Besetzung. Von Hand gewählte Bilder bleiben.';
const suche = feld('Anderer Suchbegriff');
suche.value = titel;
// Leer heisst: der Server nimmt das Jahr aus dem Dateinamen. Hier etwas
// vorzutragen, das aus der zu korrigierenden Zuordnung stammt, hiesse die
// Suche auf genau den falschen Jahrgang einzugrenzen.
const jahrfeld = feld('Jahr (aus dem Dateinamen)', 'number');
if (jahr) jahrfeld.value = String(jahr);
const suchen = knopf('Suchen', 'btn');
const reihe = document.createElement('div');
reihe.className = 'knopfreihe';
reihe.append(suche, jahrfeld, suchen);
const treffer = document.createElement('div');
treffer.className = 'einstellungen';
koerper.append(quelle, hinweis, reihe, treffer);
// Enter im Suchfeld sucht, statt den Dialog zu schliessen.
suche.onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); void fragen(); } };
suchen.onclick = () => void fragen();
void fragen();
async function fragen(): Promise<void> {
treffer.innerHTML = '<div class="spinner"></div>';
try {
const liste = await vorschlaege(art, id,
suche.value.trim(), jahrfeld.value ? Number(jahrfeld.value) : undefined);
treffer.innerHTML = '';
if (liste.length === 0) { treffer.appendChild(leer('Nichts gefunden.')); return; }
const zeilen: HTMLButtonElement[] = [];
liste.forEach(v => {
const zeile = document.createElement('button');
zeile.className = 'vorschlag';
// **Mit Plakat.** „Dark" von 2017 und „Dark Shadows" von 1966 sind an
// einer Textzeile kaum zu unterscheiden, am Plakat in einer
// Zehntelsekunde. Der Server liefert die Adresse längst mit; sie wurde
// bisher weggeworfen.
const bild = document.createElement('div');
bild.className = 'vorschlag-bild';
if (v.posterUrl) {
const img = document.createElement('img');
img.src = v.posterUrl;
img.alt = '';
img.loading = 'lazy';
img.onload = () => bild.classList.add('geladen');
bild.appendChild(img);
}
const text = document.createElement('div');
text.className = 'vorschlag-text';
text.innerHTML =
`<div class="zeile"><strong>${esc(v.title)}</strong>`
+ `<span class="winzig">${esc(v.year ?? '')} · TMDB ${v.tmdbId}</span></div>`
+ `<div class="hinweis">${esc((v.overview ?? '').slice(0, 160))}</div>`;
zeile.append(bild, text);
zeile.onclick = async () => {
// **Sichtbar machen, dass etwas passiert.**
//
// Ein Klick löste einen Abruf bei TMDB aus, der Bilder nachlädt und
// mehrere Sekunden braucht — und die Oberfläche sah aus wie vorher.
// Wer nichts sieht, klickt noch einmal, und dann laufen zwei.
zeilen.forEach(z => { z.disabled = true; });
zeile.classList.add('laeuft');
const lauf = document.createElement('span');
lauf.className = 'vorschlag-lauf';
lauf.innerHTML = '<span class="spinner klein"></span><span>Wird zugeordnet…</span>';
zeile.appendChild(lauf);
try {
const stand = await zuordnen(art, id, v.tmdbId);
// Erst melden, dann schliessen: sonst blitzt nur der Dialog weg,
// und es bleibt offen, ob es geklappt hat.
lauf.innerHTML = stand.merged
? '<span>Übernommen und mit dem vorhandenen Eintrag zusammengelegt.</span>'
: '<span>Übernommen.</span>';
fertig?.(stand);
} catch (fehler) {
lauf.remove();
zeile.classList.remove('laeuft');
zeilen.forEach(z => { z.disabled = false; });
treffer.appendChild(fehlerzeile(meldung(fehler)));
}
};
zeilen.push(zeile);
treffer.appendChild(zeile);
});
} catch (fehler) {
treffer.innerHTML = '';
treffer.appendChild(fehlerzeile(meldung(fehler)));
}
}
}