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>
This commit is contained in:
Scarriffle
2026-10-06 13:21:42 +02:00
co-authored by Claude Opus 5
parent fcf40e4394
commit c3124ba27c
12 changed files with 626 additions and 78 deletions
@@ -23,6 +23,28 @@ public sealed record Speicherort(
/// <summary>Dateien, die zu keinem Titel gehören — mit Begründung.</summary>
public sealed record Unzugeordnet(int Gesamt, IReadOnlyList<UnzugeordneteDatei> Dateien);
/// <summary>Ein Titel, zu dem die Metadatenquelle nichts hergab.</summary>
///
/// <param name="Art">„movie" oder „series" — damit der Client weiss, wohin.</param>
/// <param name="Dateien">Wie viele Dateien daran hängen.</param>
/// <param name="Beispiel">Ein Dateiname, damit man den Titel wiedererkennt.</param>
///
/// <remarks>
/// <b>Weil die Zahl allein nichts erklärt.</b> In der Übersicht standen 448
/// Filme, in Plex 451 — und nirgends stand, welche drei fehlen oder warum.
/// Eine Mediathek, die etwas nicht zuordnen konnte, muss sagen was.
/// </remarks>
public sealed record OhneZuordnung(
Guid Id, string Art, string Titel, int? Jahr, int Dateien, string? Beispiel);
/// <param name="Titel">Titel mit mehr als einer Datei — dort versteckt sich Falsches.</param>
public sealed record Mehrfachbericht(
int Gesamt, IReadOnlyList<Mehrfachtitel> Titel);
/// <param name="Dateien">Die Namen der Dateien, die an diesem einen Titel hängen.</param>
public sealed record Mehrfachtitel(
Guid Id, string Art, string Titel, int? Jahr, IReadOnlyList<string> Dateien);
public sealed record UnzugeordneteDatei(Guid Id, string Pfad, string Grund);
/// <summary>Wie viele Titel in welcher Sprache vorliegen.</summary>
@@ -227,6 +227,92 @@ public static class ManagementEndpoints
f.Id, f.Path, FileNames.WarumNicht(f.Path, bibliothek.Path))).ToList()));
}).RequireAuthorization(FlaroPolicies.Administrator);
// **Was die Metadatenquelle nicht hergab.**
//
// `/unassigned` listet Dateien ganz ohne Titel — bei einer
// Filmbibliothek ist das immer leer, weil der Scan für jede Datei
// einen Eintrag anlegt. Sichtbar war damit nur die Hälfte: dass ein
// Film zwar dasteht, aber unter seinem Dateinamen und ohne Plakat,
// erfuhr niemand. In der Übersicht standen 448 Filme, in Plex 451 —
// und nirgends, welche drei fehlen.
bibliotheken.MapGet("/{id:guid}/unmatched", async (
Guid id, FlaroContext db, int take = 300) =>
{
if (!await db.Libraries.AsNoTracking().AnyAsync(l => l.Id == id)) return Results.NotFound();
var grenze = Math.Clamp(take, 1, 2000);
var filme = await db.Movies.AsNoTracking()
.Where(m => m.LibraryId == id && m.TmdbId == null)
.OrderBy(m => m.SortTitleValue)
.Take(grenze)
.Select(m => new
{
m.Id, m.Title, Jahr = m.ParsedYear ?? m.Year,
Dateien = db.MediaFiles.Count(f => f.MovieId == m.Id),
Beispiel = db.MediaFiles.Where(f => f.MovieId == m.Id)
.Select(f => f.Path).FirstOrDefault(),
})
.ToListAsync();
var serien = await db.Series.AsNoTracking()
.Where(x => x.LibraryId == id && x.TmdbId == null)
.OrderBy(x => x.SortTitleValue)
.Take(grenze)
.Select(x => new
{
x.Id, x.Title, Jahr = x.ParsedYear ?? x.Year,
Dateien = db.Episodes.Count(f => f.SeriesId == x.Id),
Beispiel = db.Episodes.Where(f => f.SeriesId == x.Id)
.Join(db.MediaFiles, f => f.Id, d => d.EpisodeId, (_, d) => d.Path)
.FirstOrDefault(),
})
.ToListAsync();
var liste = filme
.Select(m => new OhneZuordnung(
m.Id, "movie", m.Title, m.Jahr, m.Dateien, Dateiname(m.Beispiel)))
.Concat(serien.Select(x => new OhneZuordnung(
x.Id, "series", x.Title, x.Jahr, x.Dateien, Dateiname(x.Beispiel))))
.OrderBy(x => x.Titel, StringComparer.CurrentCultureIgnoreCase)
.ToList();
return Results.Ok(liste);
}).RequireAuthorization(FlaroPolicies.Administrator);
// **Titel mit mehr als einer Datei — dort versteckt sich Falsches.**
//
// Zwei Dateien an einem Film sind meist zwei Fassungen und völlig in
// Ordnung. Manchmal sind es aber zwei verschiedene Filme, die bei der
// Metadatenquelle auf denselben Treffer liefen — und dann ist einer
// davon lautlos verschwunden. Wer die Liste durchsieht, erkennt das
// an den Dateinamen in zwei Sekunden.
bibliotheken.MapGet("/{id:guid}/versions", async (
Guid id, FlaroContext db, int take = 300) =>
{
if (!await db.Libraries.AsNoTracking().AnyAsync(l => l.Id == id)) return Results.NotFound();
var mehrfach = await db.Movies.AsNoTracking()
.Where(m => m.LibraryId == id && db.MediaFiles.Count(f => f.MovieId == m.Id) > 1)
.OrderBy(m => m.SortTitleValue)
.Take(Math.Clamp(take, 1, 2000))
.Select(m => new
{
m.Id, m.Title, Jahr = m.ParsedYear ?? m.Year,
Pfade = db.MediaFiles.Where(f => f.MovieId == m.Id)
.Select(f => f.Path).ToList(),
})
.ToListAsync();
var liste = mehrfach
.Select(m => new Mehrfachtitel(
m.Id, "movie", m.Title, m.Jahr,
m.Pfade.Select(p => Dateiname(p) ?? p).ToList()))
.ToList();
return Results.Ok(new Mehrfachbericht(liste.Count, liste));
}).RequireAuthorization(FlaroPolicies.Administrator);
bibliotheken.MapGet("/missing", async (FlaroContext db) =>
{
var liste = await db.MediaFiles.AsNoTracking()
@@ -395,6 +481,10 @@ public static class ManagementEndpoints
return Results.NoContent();
}).RequireAuthorization(FlaroPolicies.Administrator);
}
/// <summary>Nur der Dateiname — der volle Pfad geht niemanden etwas an.</summary>
private static string? Dateiname(string? pfad) =>
pfad is null ? null : Path.GetFileName(pfad);
}
public static class FlaroPolicies