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
@@ -20,6 +20,11 @@ public sealed record SystemInfo(
|
|||||||
public sealed record Speicherort(
|
public sealed record Speicherort(
|
||||||
string Name, string Variable, string Pfad, long Belegt, long? Frei, long? Bilder);
|
string Name, string Variable, string Pfad, long Belegt, long? Frei, long? Bilder);
|
||||||
|
|
||||||
|
/// <summary>Dateien, die zu keinem Titel gehören — mit Begründung.</summary>
|
||||||
|
public sealed record Unzugeordnet(int Gesamt, IReadOnlyList<UnzugeordneteDatei> Dateien);
|
||||||
|
|
||||||
|
public sealed record UnzugeordneteDatei(Guid Id, string Pfad, string Grund);
|
||||||
|
|
||||||
public sealed record ServerNameRequest(string Name);
|
public sealed record ServerNameRequest(string Name);
|
||||||
|
|
||||||
public sealed record SetupRequest(string UserName, string Password);
|
public sealed record SetupRequest(string UserName, string Password);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Flaro.Api.Auth;
|
using Flaro.Api.Auth;
|
||||||
using Flaro.Core.Libraries;
|
using Flaro.Core.Libraries;
|
||||||
|
using Flaro.Core.Library;
|
||||||
using Flaro.Core.Users;
|
using Flaro.Core.Users;
|
||||||
using Flaro.Data;
|
using Flaro.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -148,6 +149,35 @@ public static class ManagementEndpoints
|
|||||||
return betroffen > 0 ? Results.NoContent() : Results.NotFound();
|
return betroffen > 0 ? Results.NoContent() : Results.NotFound();
|
||||||
}).RequireAuthorization(FlaroPolicies.Administrator);
|
}).RequireAuthorization(FlaroPolicies.Administrator);
|
||||||
|
|
||||||
|
// **Was zu keinem Titel gehört.**
|
||||||
|
//
|
||||||
|
// Der Scan erfindet keine Serie mehr, wenn weder Ordner noch Dateiname
|
||||||
|
// eine nennen — nur verschwanden die Dateien damit lautlos. Bei 3559
|
||||||
|
// von 5884 war das die halbe Mediathek, und in der Übersicht stand
|
||||||
|
// eine Zahl, die niemand erklären konnte. Hier stehen sie, mit dem
|
||||||
|
// Grund dazu.
|
||||||
|
bibliotheken.MapGet("/{id:guid}/unassigned", async (
|
||||||
|
Guid id, FlaroContext db, int take = 300) =>
|
||||||
|
{
|
||||||
|
var bibliothek = await db.Libraries.AsNoTracking().FirstOrDefaultAsync(l => l.Id == id);
|
||||||
|
if (bibliothek is null) return Results.NotFound();
|
||||||
|
|
||||||
|
var offen = db.MediaFiles.AsNoTracking()
|
||||||
|
.Where(f => f.LibraryId == id && f.MovieId == null && f.EpisodeId == null
|
||||||
|
&& f.MissingSince == null);
|
||||||
|
|
||||||
|
var gesamt = await offen.CountAsync();
|
||||||
|
var pfade = await offen.OrderBy(f => f.Path).Take(Math.Clamp(take, 1, 2000)).ToListAsync();
|
||||||
|
|
||||||
|
// Der Grund wird hier gerechnet, nicht gespeichert: er hängt allein
|
||||||
|
// am Pfad und an der Benennungsregel. Eine Spalte dafür wäre beim
|
||||||
|
// nächsten Regelwechsel veraltet, ohne dass es jemand merkt.
|
||||||
|
return Results.Ok(new Unzugeordnet(
|
||||||
|
gesamt,
|
||||||
|
pfade.Select(f => new UnzugeordneteDatei(
|
||||||
|
f.Id, f.Path, FileNames.WarumNicht(f.Path, bibliothek.Path))).ToList()));
|
||||||
|
}).RequireAuthorization(FlaroPolicies.Administrator);
|
||||||
|
|
||||||
bibliotheken.MapGet("/missing", async (FlaroContext db) =>
|
bibliotheken.MapGet("/missing", async (FlaroContext db) =>
|
||||||
{
|
{
|
||||||
var liste = await db.MediaFiles.AsNoTracking()
|
var liste = await db.MediaFiles.AsNoTracking()
|
||||||
|
|||||||
@@ -267,6 +267,31 @@ public static partial class FileNames
|
|||||||
AusOrdner: ausOrdner is not null);
|
AusOrdner: ausOrdner is not null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Warum sich eine Datei keiner Serie zuordnen liess.</summary>
|
||||||
|
///
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Zwei Gründe, und sie verlangen Verschiedenes.</b> Fehlt die
|
||||||
|
/// Folgennummer, muss die Datei umbenannt werden. Fehlt der Serienname,
|
||||||
|
/// genügt ein Ordner darum herum. „Nicht zugeordnet" allein sagt beides
|
||||||
|
/// nicht — und bei 3559 Dateien ist der Unterschied die ganze Arbeit.
|
||||||
|
/// </remarks>
|
||||||
|
public static string WarumNicht(string dateiname, string? wurzel = null)
|
||||||
|
{
|
||||||
|
var roh = Rohname(dateiname);
|
||||||
|
var treffer = StaffelUndFolge().Match(roh);
|
||||||
|
if (!treffer.Success) treffer = KreuzSchreibweise().Match(roh);
|
||||||
|
|
||||||
|
if (!treffer.Success)
|
||||||
|
{
|
||||||
|
return "keine Folgennummer im Dateinamen (erwartet wird etwa S01E02 oder 1x02).";
|
||||||
|
}
|
||||||
|
|
||||||
|
return wurzel is { Length: > 0 } && SerieAusOrdner(dateiname, wurzel) is null
|
||||||
|
? "liegt ohne Ordner direkt in der Mediathek, und vor der Folgennummer "
|
||||||
|
+ "steht kein Serienname."
|
||||||
|
: "vor der Folgennummer steht kein Serienname.";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Der Serienname aus dem Pfad.</summary>
|
/// <summary>Der Serienname aus dem Pfad.</summary>
|
||||||
///
|
///
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|||||||
@@ -162,16 +162,25 @@ public sealed class LibraryScanner(
|
|||||||
var name = FileNames.ParseEpisode(datei.Path, bibliothek.Path);
|
var name = FileNames.ParseEpisode(datei.Path, bibliothek.Path);
|
||||||
if (name is null)
|
if (name is null)
|
||||||
{
|
{
|
||||||
// **Nicht raten.** Eine erfundene Staffel 1 landete unter
|
// **Nicht raten — aber auch nicht verschweigen.**
|
||||||
// einer Serie, zu der die Datei nicht gehört, und dort
|
//
|
||||||
// fände sie niemand je wieder.
|
// Eine erfundene Staffel 1 landete unter einer Serie, zu
|
||||||
|
// der die Datei nicht gehört, und dort fände sie niemand je
|
||||||
|
// wieder. Die Datei einfach zu überspringen war aber die
|
||||||
|
// andere Hälfte des Fehlers: 3559 von 5884 Dateien
|
||||||
|
// verschwanden lautlos, und in der Mediathek stand eine
|
||||||
|
// Zahl, die keiner erklären konnte.
|
||||||
|
//
|
||||||
|
// Jetzt bekommt sie eine Zeile ohne Zuordnung. Damit ist
|
||||||
|
// sie zählbar, auflistbar und beim nächsten Lauf — oder
|
||||||
|
// nach einer besseren Regel — nachträglich zuzuordnen.
|
||||||
unerkannt++;
|
unerkannt++;
|
||||||
log.LogInformation("Scan: {Pfad} liess sich keiner Serie zuordnen.", datei.Path);
|
log.LogInformation("Scan: {Pfad} liess sich keiner Serie zuordnen.", datei.Path);
|
||||||
fortschritt?.Report(new Scanschritt(Scanphase.Vermerk,
|
fortschritt?.Report(new Scanschritt(Scanphase.Vermerk,
|
||||||
$"Nicht zugeordnet: {datei.Path} — weder ein Ordner noch der "
|
$"Nicht zugeordnet: {datei.Path} — {FileNames.WarumNicht(datei.Path, bibliothek.Path)}"));
|
||||||
+ "Dateiname nennen eine Serie."));
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
if (name is not null)
|
||||||
|
{
|
||||||
var vorher = serien.Count;
|
var vorher = serien.Count;
|
||||||
var serie = SerieFinden(serien, libraryId, name.SeriesTitle);
|
var serie = SerieFinden(serien, libraryId, name.SeriesTitle);
|
||||||
|
|
||||||
@@ -189,6 +198,7 @@ public sealed class LibraryScanner(
|
|||||||
|
|
||||||
folgeId = FolgeFinden(folgen, serie.Id, name).Id;
|
folgeId = FolgeFinden(folgen, serie.Id, name).Id;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MediaFile eintrag;
|
MediaFile eintrag;
|
||||||
bool mussUntersucht;
|
bool mussUntersucht;
|
||||||
@@ -264,7 +274,13 @@ public sealed class LibraryScanner(
|
|||||||
mussUntersucht = true;
|
mussUntersucht = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mussUntersucht && await UntersuchenAsync(eintrag, laufZeit, ct)) untersucht++;
|
// **Was keiner Serie gehört, wird nicht untersucht.** Ein
|
||||||
|
// ffprobe-Durchgang kostet rund fünfzig Millisekunden; bei 3559
|
||||||
|
// unzugeordneten Dateien ist das eine halbe Stunde für Angaben,
|
||||||
|
// die niemand zu sehen bekommt. Sobald die Zuordnung steht, holt
|
||||||
|
// der nächste Lauf es nach — `ProbedAt` ist dann noch leer.
|
||||||
|
var gehoertDazu = filmId is not null || folgeId is not null;
|
||||||
|
if (gehoertDazu && mussUntersucht && await UntersuchenAsync(eintrag, laufZeit, ct)) untersucht++;
|
||||||
|
|
||||||
// **Zwischendurch schreiben, nicht erst am Ende.**
|
// **Zwischendurch schreiben, nicht erst am Ende.**
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -464,7 +464,15 @@ public class ScanTests
|
|||||||
Assert.That(nurStaffel2!.Items, Has.Count.EqualTo(1));
|
Assert.That(nurStaffel2!.Items, Has.Count.EqualTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Was keine Folgennummer trägt, wird nicht geraten.</summary>
|
/// <summary>Was keine Folgennummer trägt, wird nicht geraten — aber auch nicht verschwiegen.</summary>
|
||||||
|
///
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Beide Hälften gehören zusammen.</b> Eine erfundene Staffel 1 wäre
|
||||||
|
/// falsch; die Datei lautlos zu überspringen war aber genauso falsch. Bei
|
||||||
|
/// 3559 von 5884 Dateien verschwand so die halbe Mediathek, ohne dass
|
||||||
|
/// irgendwo eine Zahl davon erzählte. <c>Added</c> zählt Dateizeilen, und
|
||||||
|
/// eine bekommt sie jetzt — zugeordnet ist sie deshalb nicht.
|
||||||
|
/// </remarks>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Dateien_ohne_Folgennummer_werden_gezaehlt_aber_nicht_zugeordnet()
|
public async Task Dateien_ohne_Folgennummer_werden_gezaehlt_aber_nicht_zugeordnet()
|
||||||
{
|
{
|
||||||
@@ -477,7 +485,19 @@ public class ScanTests
|
|||||||
|
|
||||||
Assert.That(stand.FilesFound, Is.EqualTo(2));
|
Assert.That(stand.FilesFound, Is.EqualTo(2));
|
||||||
Assert.That(stand.Unrecognised, Is.EqualTo(1));
|
Assert.That(stand.Unrecognised, Is.EqualTo(1));
|
||||||
Assert.That(stand.Added, Is.EqualTo(1));
|
Assert.That(stand.Added, Is.EqualTo(2), "Beide Dateien bekommen eine Zeile.");
|
||||||
|
|
||||||
|
// Die Serie hat genau eine Folge — der Mitschnitt hängt an keiner.
|
||||||
|
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||||||
|
Assert.That(serien!.Total, Is.EqualTo(1));
|
||||||
|
Assert.That(serien.Items[0].EpisodeCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
// Und er ist auffindbar, statt lautlos zu verschwinden.
|
||||||
|
var offen = await client.GetFromJsonAsync<Unzugeordnet>(
|
||||||
|
$"/api/v1/libraries/{bibliothek}/unassigned");
|
||||||
|
Assert.That(offen!.Gesamt, Is.EqualTo(1));
|
||||||
|
Assert.That(offen.Dateien[0].Pfad, Does.EndWith("Irgendein Mitschnitt.mkv"));
|
||||||
|
Assert.That(offen.Dateien[0].Grund, Does.Contain("keine Folgennummer"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -643,6 +643,13 @@ export const protokolltage = () => anfrage<Protokolltag[]>('/system/logs');
|
|||||||
export const protokollAdresse = (tag: string, zeilen?: number) =>
|
export const protokollAdresse = (tag: string, zeilen?: number) =>
|
||||||
`/api/v1/system/logs/${tag}` + (zeilen ? `?tail=${zeilen}` : '');
|
`/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 = {
|
export type VermissteDatei = {
|
||||||
fileId: string; path: string; versionLabel: string | null;
|
fileId: string; path: string; versionLabel: string | null;
|
||||||
missingSince: string; libraryName: string;
|
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;
|
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 { display: flex; flex-direction: column; gap: var(--s-snug); }
|
||||||
.bildblock h3 { margin: 0; font-size: var(--t-gross); font-weight: 600; flex: 1; }
|
.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,
|
benutzer, benutzerAnlegen, metadatenEinstellung, metadatenSetzen,
|
||||||
servernameSetzen, systemauskunft,
|
servernameSetzen, systemauskunft,
|
||||||
laufendeWiedergaben, serverzustand, bildAdresse,
|
laufendeWiedergaben, serverzustand, bildAdresse,
|
||||||
speicherorte, bildspeicherLeeren,
|
speicherorte, bildspeicherLeeren, unzugeordnet,
|
||||||
type Bibliothek, type Wiedergabe, ApiFehler,
|
type Bibliothek, type Wiedergabe, ApiFehler,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../util';
|
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../util';
|
||||||
@@ -32,7 +32,8 @@ export function verwaltung(
|
|||||||
const wurzel = document.createElement('div');
|
const wurzel = document.createElement('div');
|
||||||
wurzel.append(dashboard(signal), sprachbereich(), pruefbereich(geaendert),
|
wurzel.append(dashboard(signal), sprachbereich(), pruefbereich(geaendert),
|
||||||
vermisstenbereich(geaendert), bibliotheksbereich(geaendert, signal), benutzerbereich(),
|
vermisstenbereich(geaendert), bibliotheksbereich(geaendert, signal), benutzerbereich(),
|
||||||
serverbereich(geaendert), speicherbereich(), protokollbereich());
|
serverbereich(geaendert), speicherbereich(), unzugeordnetbereich(),
|
||||||
|
protokollbereich());
|
||||||
return wurzel;
|
return wurzel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +364,116 @@ function pruefbereich(geaendert: () => void): HTMLElement {
|
|||||||
* Das Containerprotokoll ist nach einem Neustart weg und nur über SSH
|
* Das Containerprotokoll ist nach einem Neustart weg und nur über SSH
|
||||||
* erreichbar — die Frage „warum wurde der Film gestern Nacht nicht erkannt?"
|
* erreichbar — die Frage „warum wurde der Film gestern Nacht nicht erkannt?"
|
||||||
* lässt sich damit nicht beantworten. */
|
* 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 {
|
function protokollbereich(): HTMLElement {
|
||||||
const bereich = document.createElement('section');
|
const bereich = document.createElement('section');
|
||||||
bereich.appendChild(abschnitt('Protokolle'));
|
bereich.appendChild(abschnitt('Protokolle'));
|
||||||
|
|||||||
Reference in New Issue
Block a user