import { bibliotheken, unzugeordnet, ohneZuordnung, mehrfachfassungen, fassungenAbhaken, dateiHerausloesen, type Mehrfachtitel, } from '../../api'; import { esc, knopf, meldung, hinweisZeigen } from '../../util'; import { bearbeiten } from '../../bearbeiten'; import { abschnitt } from './teile'; /** Was sich nicht zuordnen liess. */ /** Titel, zu denen die Metadatenquelle nichts hergab. * * **Weil die Zahl allein nichts erklärt.** In der Übersicht standen 448 * Filme, in Plex 451 — und nirgends, welche drei fehlen oder warum. Ein Film * ohne Zuordnung steht zwar in der Mediathek, aber unter seinem Dateinamen * und ohne Plakat; wer ihn sucht, findet ihn nicht, weil er anders heisst. * * Daneben: Titel mit mehr als einer Datei. Zwei Dateien an einem Film sind * meist zwei Fassungen und in Ordnung — manchmal aber zwei verschiedene * Filme, die bei der Quelle auf denselben Treffer liefen. An den Dateinamen * sieht man das in zwei Sekunden. */ export function ohneZuordnungBereich(): HTMLElement { const bereich = document.createElement('section'); bereich.classList.add('hidden'); /** Ob die abgehakten Mehrfachfassungen gezeigt werden statt der offenen. * * **Ohne diesen Schalter wäre das Abhaken unumkehrbar.** Man klickt eine * Gruppe weg und weiss hinterher nicht mehr, welche es war — dieselbe * Überlegung wie bei den erledigten Meldungen. */ let zeigeIgnorierte = false; const umschalten = knopf('Ignorierte zeigen', 'btn'); umschalten.onclick = () => { zeigeIgnorierte = !zeigeIgnorierte; umschalten.textContent = zeigeIgnorierte ? 'Offene zeigen' : 'Ignorierte zeigen'; void laden(); }; bereich.appendChild(abschnitt('Ohne Zuordnung', [umschalten])); const liste = document.createElement('div'); liste.className = 'einstellungen'; bereich.appendChild(liste); void laden(); async function laden(): Promise { let alle; try { alle = await bibliotheken(); } catch { return; } const ergebnisse = await Promise.all(alle.map(async b => { try { const [offen, mehrfach] = await Promise.all([ ohneZuordnung(b.id), mehrfachfassungen(b.id, 300, zeigeIgnorierte), ]); return { b, offen, mehrfach }; } catch { return null; } })); const mitFunden = ergebnisse .filter(e => e !== null) .filter(e => e!.offen.length > 0 || e!.mehrfach.gesamt > 0); // **Solange jemand die Ignorierten ansieht, bleibt der Bereich stehen.** // Sonst verschwände er samt Umschalter in dem Augenblick, in dem die // letzte abgehakte Gruppe zurückgeholt ist — und niemand fände den Weg // zurück auf die offenen. if (mitFunden.length === 0 && !zeigeIgnorierte) { bereich.classList.add('hidden'); return; } bereich.classList.remove('hidden'); liste.innerHTML = ''; if (mitFunden.length === 0) { const leer = document.createElement('p'); leer.className = 'winzig'; leer.textContent = 'Hier ist nichts abgehakt.'; liste.appendChild(leer); return; } mitFunden.forEach(e => { const { b, offen, mehrfach } = e!; if (offen.length > 0) { liste.appendChild(gruppe( `${b.name} — ${anzahl(offen.length, 'Titel')} ohne Zuordnung`, 'Diese stehen unter ihrem Dateinamen in der Mediathek. ' + 'Über „Zuordnung korrigieren" lassen sie sich geradeziehen.', offen.map(t => ({ titel: t.titel + (t.jahr ? ` (${t.jahr})` : ''), dazu: t.beispiel ?? `${anzahl(t.dateien, 'Datei')}`, // **Hier bearbeiten, nicht woandershin springen.** Wer eine // Liste durchgeht, will nach dem Geradeziehen beim nächsten // weitermachen — nicht jedes Mal zurücknavigieren. tun: () => bearbeiten(t.art === 'series' ? 'series' : 'movies', t.id, () => void laden()), })))); } if (mehrfach.gesamt > 0) { liste.appendChild(fassungsgruppe( `${b.name} — ${anzahl(mehrfach.gesamt, 'Titel')} mit mehreren Dateien`, zeigeIgnorierte ? 'Diese Gruppen wurden als geprüft abgehakt. „Wieder zeigen" holt sie zurück.' : 'Meist zwei Fassungen desselben Films — dann ist alles in Ordnung und ' + '„Ignorieren" nimmt die Gruppe aus der Liste. Stehen hier zwei ' + 'verschiedene Filme, war einer davon nicht mehr zu sehen: ' + '„Eigener Film" löst ihn wieder heraus.', mehrfach.titel, b.id)); } }); } function anzahl(n: number, wort: string): string { return `${n} ${n === 1 ? wort : (wort === 'Datei' ? 'Dateien' : wort)}`; } /** Dieselbe Gruppe, aber jede Datei einzeln mit einem Weg heraus. */ function fassungsgruppe( ueberschrift: string, hinweis: string, titel: Mehrfachtitel[], bibliothek: string, ): HTMLElement { const d = document.createElement('div'); d.className = 'einstellung'; const kopf = document.createElement('div'); kopf.className = 'zeile'; kopf.innerHTML = `${esc(ueberschrift)}` + `${esc(hinweis)}`; d.appendChild(kopf); const innen = document.createElement('div'); innen.className = 'ohnezuordnung'; titel.forEach(t => { const zeile = document.createElement('div'); zeile.className = 'fassungstitel'; const kopfzeile = document.createElement('div'); kopfzeile.className = 'fassungsdatei'; const name = document.createElement('button'); name.className = 'ohnezuordnung-zeile'; name.innerHTML = `${esc(t.titel + (t.jahr ? ` (${t.jahr})` : ''))}`; name.onclick = () => bearbeiten( t.art === 'series' ? 'series' : 'movies', t.id, () => void laden()); // **Für die, die absichtlich zwei Fassungen halten.** Die Liste // rechnet jedes Mal frisch; ohne das Abhaken stand eine gewollte // Gruppe hier für immer, und darunter ging das Ungewollte unter. const abhaken = knopf(zeigeIgnorierte ? 'Wieder zeigen' : 'Ignorieren', 'btn klein'); abhaken.onclick = async () => { abhaken.disabled = true; try { await fassungenAbhaken(bibliothek, [t.id], !zeigeIgnorierte); hinweisZeigen(zeigeIgnorierte ? `„${t.titel}" steht wieder in der Liste.` : `„${t.titel}" gilt als geprüft. Kommt eine Fassung dazu, meldet er sich wieder.`); await laden(); } catch (fehler) { hinweisZeigen(meldung(fehler), 'fehler'); abhaken.disabled = false; } }; kopfzeile.append(name, abhaken); zeile.appendChild(kopfzeile); t.dateien.forEach(f => { const d2 = document.createElement('div'); d2.className = 'fassungsdatei'; const n = document.createElement('span'); n.className = 'oz-dazu'; n.textContent = f.name; // **Nur bei mehr als einer Datei sinnvoll** — und mehr als eine ist // hier die Bedingung, überhaupt in der Liste zu stehen. const raus = knopf('Eigener Film', 'btn klein'); raus.onclick = async () => { raus.disabled = true; try { const neu = await dateiHerausloesen(f.id); hinweisZeigen(`„${neu.title}" ist jetzt ein eigener Film — ohne Zuordnung.`); await laden(); } catch (fehler) { hinweisZeigen(meldung(fehler), 'fehler'); raus.disabled = false; } }; d2.append(n, raus); zeile.appendChild(d2); }); innen.appendChild(zeile); }); d.appendChild(innen); return d; } function gruppe( ueberschrift: string, hinweis: string, zeilen: { titel: string; dazu: string; tun: () => void }[], ): HTMLElement { const d = document.createElement('div'); d.className = 'einstellung'; const kopf = document.createElement('div'); kopf.className = 'zeile'; kopf.innerHTML = `${esc(ueberschrift)}` + `${esc(hinweis)}`; d.appendChild(kopf); const innen = document.createElement('div'); innen.className = 'ohnezuordnung'; zeilen.forEach(z => { const b = document.createElement('button'); b.className = 'ohnezuordnung-zeile'; b.innerHTML = `${esc(z.titel)}` + `${esc(z.dazu)}`; b.onclick = z.tun; innen.appendChild(b); }); d.appendChild(innen); return d; } return bereich; } export function unzugeordnetbereich(): HTMLElement { const bereich = document.createElement('section'); const kopf = abschnitt('Nicht zugeordnet'); bereich.appendChild(kopf); const liste = document.createElement('div'); liste.className = 'einstellungen'; bereich.appendChild(liste); void laden(); async function laden(): Promise { let alle; try { alle = await bibliotheken(); } catch { bereich.classList.add('hidden'); return; } const ergebnisse = await Promise.all(alle.map(async b => { try { return { b, offen: await unzugeordnet(b.id) }; } catch { return null; } })); const mitFunden = ergebnisse.filter(e => e !== null).filter(e => e!.offen.gesamt > 0); // Nichts offen heisst: der Abschnitt hat hier nichts verloren. if (mitFunden.length === 0) { bereich.classList.add('hidden'); return; } bereich.classList.remove('hidden'); liste.innerHTML = ''; mitFunden.forEach(eintrag => { const { b, offen } = eintrag!; const d = document.createElement('div'); d.className = 'einstellung'; const oben = document.createElement('div'); oben.className = 'knopfreihe'; const text = document.createElement('div'); text.className = 'zeile'; text.style.flex = '1'; text.innerHTML = `${esc(b.name)}` + `${offen.gesamt} ${offen.gesamt === 1 ? 'Datei' : 'Dateien'} ` + 'ohne Zuordnung'; oben.appendChild(text); // **Zum Verschicken.** Eine Liste, die man markieren und kopieren muss, // schickt niemand — und ohne die Pfade lässt sich nicht sagen, woran // die Erkennung scheitert. const holen = knopf('Liste herunterladen', 'btn'); holen.onclick = () => { const zeilen = offen.dateien.map(f => `${f.pfad}\t${f.grund}`).join('\n'); const blob = new Blob( [`# ${b.name} — ${offen.gesamt} ohne Zuordnung, davon ${offen.dateien.length} aufgeführt\n` + zeilen + '\n'], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `nicht-zugeordnet-${b.name}.txt`; a.click(); URL.revokeObjectURL(a.href); }; oben.appendChild(holen); d.appendChild(oben); // **Nach Grund gebündelt, nicht als Liste von dreitausend Zeilen.** So // sieht man in zwei Zeilen, ob umzubenennen oder einzuordnen ist. const gruende = new Map(); offen.dateien.forEach(f => { if (!gruende.has(f.grund)) gruende.set(f.grund, []); gruende.get(f.grund)!.push(f.pfad); }); [...gruende.entries()] .sort((a, c) => c[1].length - a[1].length) .forEach(([grund, pfade]) => { const g = document.createElement('details'); g.className = 'grundblock'; const z = document.createElement('summary'); z.innerHTML = `${pfade.length}× ${esc(grund)}`; g.appendChild(z); const pre = document.createElement('pre'); pre.className = 'protokoll'; pre.textContent = pfade.slice(0, 50).join('\n') + (pfade.length > 50 ? `\n… und ${pfade.length - 50} weitere` : ''); g.appendChild(pre); d.appendChild(g); }); liste.appendChild(d); }); } return bereich; }