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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a8bb98c44c
commit
cfb1e71762
@@ -643,6 +643,13 @@ export const protokolltage = () => anfrage<Protokolltag[]>('/system/logs');
|
||||
export const protokollAdresse = (tag: string, zeilen?: number) =>
|
||||
`/api/v1/system/logs/${tag}` + (zeilen ? `?tail=${zeilen}` : '');
|
||||
|
||||
export type UnzugeordneteDatei = { id: string; pfad: string; grund: string };
|
||||
export type Unzugeordnet = { gesamt: number; dateien: UnzugeordneteDatei[] };
|
||||
|
||||
/** Dateien, die zu keinem Titel gehören — mit Begründung je Datei. */
|
||||
export const unzugeordnet = (bibliothek: string, anzahl = 300) =>
|
||||
anfrage<Unzugeordnet>(`/libraries/${bibliothek}/unassigned?take=${anzahl}`);
|
||||
|
||||
export type VermissteDatei = {
|
||||
fileId: string; path: string; versionLabel: string | null;
|
||||
missingSince: string; libraryName: string;
|
||||
|
||||
@@ -1001,6 +1001,16 @@ h2 { font-size: var(--t-abschnitt); font-weight: 700; letter-spacing: -.015em; m
|
||||
font-size: var(--t-klein); color: var(--text); word-break: break-all;
|
||||
}
|
||||
|
||||
/* Ein aufklappbarer Block je Grund — zugeklappt sieht man die Verteilung,
|
||||
* aufgeklappt die Pfade. Dreitausend Zeilen am Stück liest niemand. */
|
||||
.grundblock { margin-top: var(--s-snug); }
|
||||
.grundblock summary {
|
||||
cursor: pointer; padding: 6px 0; color: var(--text-dim); font-size: var(--t-normal);
|
||||
}
|
||||
.grundblock summary:hover { color: var(--text); }
|
||||
.grundblock summary strong { color: var(--accent); margin-right: 6px; }
|
||||
.grundblock .protokoll { max-height: 220px; margin-top: var(--s-snug); }
|
||||
|
||||
.bildblock { display: flex; flex-direction: column; gap: var(--s-snug); }
|
||||
.bildblock h3 { margin: 0; font-size: var(--t-gross); font-weight: 600; flex: 1; }
|
||||
|
||||
|
||||
+113
-2
@@ -9,7 +9,7 @@ import {
|
||||
benutzer, benutzerAnlegen, metadatenEinstellung, metadatenSetzen,
|
||||
servernameSetzen, systemauskunft,
|
||||
laufendeWiedergaben, serverzustand, bildAdresse,
|
||||
speicherorte, bildspeicherLeeren,
|
||||
speicherorte, bildspeicherLeeren, unzugeordnet,
|
||||
type Bibliothek, type Wiedergabe, ApiFehler,
|
||||
} from '../api';
|
||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../util';
|
||||
@@ -32,7 +32,8 @@ export function verwaltung(
|
||||
const wurzel = document.createElement('div');
|
||||
wurzel.append(dashboard(signal), sprachbereich(), pruefbereich(geaendert),
|
||||
vermisstenbereich(geaendert), bibliotheksbereich(geaendert, signal), benutzerbereich(),
|
||||
serverbereich(geaendert), speicherbereich(), protokollbereich());
|
||||
serverbereich(geaendert), speicherbereich(), unzugeordnetbereich(),
|
||||
protokollbereich());
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
@@ -363,6 +364,116 @@ function pruefbereich(geaendert: () => void): HTMLElement {
|
||||
* Das Containerprotokoll ist nach einem Neustart weg und nur über SSH
|
||||
* erreichbar — die Frage „warum wurde der Film gestern Nacht nicht erkannt?"
|
||||
* lässt sich damit nicht beantworten. */
|
||||
/** Dateien, die zu keinem Titel gehören.
|
||||
*
|
||||
* **Warum das eine eigene Ansicht verdient.** Der Scan erfindet keine Serie
|
||||
* mehr, wenn weder Ordner noch Dateiname eine nennen — richtig so, aber die
|
||||
* Dateien verschwanden damit lautlos. Bei 3559 von 5884 war das die halbe
|
||||
* Mediathek, und in der Übersicht stand eine Zahl, die niemand erklären
|
||||
* konnte.
|
||||
*
|
||||
* **Mit Grund je Datei**, weil die beiden Gründe Verschiedenes verlangen:
|
||||
* fehlt die Folgennummer, muss umbenannt werden; fehlt der Serienname, genügt
|
||||
* ein Ordner darum herum.
|
||||
*/
|
||||
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<void> {
|
||||
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 = `<strong>${esc(b.name)}</strong>`
|
||||
+ `<span class="winzig">${offen.gesamt} ${offen.gesamt === 1 ? 'Datei' : 'Dateien'} `
|
||||
+ 'ohne Zuordnung</span>';
|
||||
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<string, string[]>();
|
||||
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 = `<strong>${pfade.length}×</strong> ${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;
|
||||
}
|
||||
|
||||
function protokollbereich(): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.appendChild(abschnitt('Protokolle'));
|
||||
|
||||
Reference in New Issue
Block a user