Files
Flaro/web/app/src/zuordnung.ts
T
ScarriffleandClaude Opus 5 18db89399d Ein leeres Jahrfeld ist eine Angabe, kein Versehen
Wer bei der Zuordnung kein Jahr einträgt, sagt damit, dass er keines
weiss. Der Server reichte trotzdem eines nach — das aus dem Dateinamen —,
und das leere Feld hatte überhaupt keine Wirkung. Gerade bei einem falsch
benannten Film ist der Dateiname aber die unzuverlässigste Quelle, die es
gibt: „Plan B (2010)" heisst bei TMDB „DivertiMento – Plan B" und ist von
2011. Mit dem nachgereichten 2010 kam nie ein Treffer, und es gab keinen
Weg daran vorbei.

Ohne Angabe wird jetzt ohne Jahr gesucht. Der Platzhalter sagt es auch.

Auf der Detailseite stand im Feld bisher das Jahr der bestehenden
Zuordnung — also genau der, die gerade korrigiert werden soll. „Doctor
Strange" war auf ein Making-of von 2022 gelaufen; mit dessen Jahr konnte
der richtige Film von 2016 gar nicht erscheinen. Das Feld bleibt jetzt
leer, und leer heisst überall dasselbe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 16:02:29 +02:00

218 lines
8.2 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.
// **Leer heisst: unbekannt.** Das Jahr aus dem Dateinamen steht schon
// drin; wer es herausnimmt, sucht ohne Jahr. Der Server fiel hier
// früher auf eben dieses Jahr zurück — das leere Feld hatte damit
// keine Wirkung.
const jahrfeld = feld('Jahr — leer: unbekannt', '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)));
}
}
}