Neun gemeldete Fehler, und die Frage, welcher Build gerade läuft

Ein grosser Commit, weil die Punkte einander bedingen: ohne lesbare
Fehlermeldung war die Serienerkennung nicht zu diagnostizieren, und ohne
Build-Kennung nicht einmal festzustellen, ob überhaupt der gebaute Stand
läuft. Zwei der neun Meldungen betrafen Funktionen, die seit Stunden im
Quelltext stehen.

Version und Build. Die Version steht fest; daneben eine Kennung aus
sechs Zeichen, die sich bei jedem Docker-Bau ändert. Sie steht in der
Verwaltung, unter /health und im Protokoll bei jedem Start. Damit ist
"reden wir vom selben Build?" in zwei Sekunden beantwortet statt in
einem halben Tag.

Serien. Lag eine Datei direkt im Serienordner und trug vor der
Folgennummer nichts, wurde der ganze Dateiname zum Serientitel: aus 95
Serien wurden 737, jede mit einer Folge, bei jedem Lauf neu. Dazu lief
der Scan in Verweise hinein — derselbe Ordner unter zwei Namen ergab
zwei Serien. Und weder neue noch umgezogene Dateien standen in der
Pfadliste des Laufs, weshalb ein doppelt gesehener Pfad eine zweite
Zeile mit gleichem (LibraryId, Path) anlegte und der
Eindeutigkeitsindex den ganzen Speichervorgang scheitern liess. Jede
Serienanlage wird jetzt im Tagesprotokoll begründet.

Fehlermeldungen. "An error occurred while saving the entity changes"
stand englisch in einer deutschen Oberfläche und nannte weder Ursache
noch nächsten Schritt. Jetzt geht der vollständige Text ins Protokoll
und ein deutscher Satz ohne Fehlercode in die Anzeige — auch für
"Failed to fetch", das aus dem Browser kam und nie übersetzt wurde.

Protokolle. Die Ansicht holte die ganze Datei und zeigte 800 Zeilen
davon; bei zwei Megabyte brach die Verbindung ab. Gelesen wird jetzt
serverseitig rückwärts, mit einem Knopf für mehr.

Auswahlfelder. Die drei <select> zeichnete das Betriebssystem: ein
helles Systemmenü mitten in der dunklen Oberfläche. Ersetzt durch ein
eigenes Listenfeld mit Tastaturbedienung.

Bildwahl. Drei Raster untereinander in einem scrollenden Dialog hiess
zwei Scrollebenen übereinander. Jetzt ein Reiter je Bildtyp.

Sprache. Die Serversprache stand in der Serverliste zwischen Name,
Metadaten und Fassung — gesucht und nicht gefunden. Sie hat jetzt einen
eigenen Abschnitt ganz oben.

Dazu: ein Knopf, der den erfassten Inhalt einer Mediathek verwirft, ohne
Name und Pfad mitzunehmen; ein Abschnitt Speicher mit den drei Pfaden,
ihrer Belegung und dem Leeren des Bildspeichers; und die Korrektur einer
Zuordnung frischt nur noch die Kachel auf statt die ganze Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 15:55:56 +02:00
co-authored by Claude Opus 5
parent ab8a66d898
commit ac04901ccc
34 changed files with 1583 additions and 255 deletions
+27 -3
View File
@@ -5,7 +5,13 @@ namespace Flaro.Core.Library;
public sealed record MovieName(string Title, int? Year);
public sealed record EpisodeName(string SeriesTitle, int Season, int Episode, string? EpisodeTitle);
/// <param name="AusOrdner">
/// <b>Ob der Serientitel vom Ordner kommt oder aus dem Dateinamen.</b> Steht
/// nur im Protokoll — aber genau diese Angabe fehlte, als aus 95 Serien 737
/// wurden: niemand konnte sagen, welcher der beiden Wege sie angelegt hatte.
/// </param>
public sealed record EpisodeName(
string SeriesTitle, int Season, int Episode, string? EpisodeTitle, bool AusOrdner = false);
/// <summary>Macht aus einem Dateinamen wieder Titel, Jahr, Staffel und Folge.</summary>
///
@@ -236,11 +242,29 @@ public static partial class FileNames
? SerieAusOrdner(dateiname, wurzel)
: serie.Length > 0 ? null : SerieAusOrdner(dateiname, null);
// **Der Rohname ist keine Serie.**
//
// Hier stand als letzter Rückfall `Rohname(dateiname)`. Das war der
// teuerste Fehler des Projekts: liegt eine Datei direkt in der Wurzel
// und trägt vor der Folgennummer nichts Brauchbares, wurde der
// komplette Dateiname — samt Folgentitel — zum Serientitel. Aus 95
// Serien wurden 737, jede mit genau einer Folge, und jede frisch bei
// jedem Lauf.
//
// Jetzt gilt: entweder der Ordner weiss es, oder der Teil vor der
// Folgennummer weiss es — oder niemand weiss es, und dann wird nichts
// erfunden. Was hier `null` ergibt, zählt im Scan als unerkannt und
// erscheint unter „Nicht zugeordnet"; eine Geisterserie mit einer
// Folge ist schlechter als ein ehrliches „weiss ich nicht".
var serientitel = ausOrdner ?? (serie.Length > 0 ? serie : null);
if (serientitel is null) return null;
return new EpisodeName(
SeriesTitle: ausOrdner ?? (serie.Length > 0 ? serie : Rohname(dateiname)),
SeriesTitle: serientitel,
Season: staffel,
Episode: folge,
EpisodeTitle: folgenTitel.Length > 0 ? folgenTitel : null);
EpisodeTitle: folgenTitel.Length > 0 ? folgenTitel : null,
AusOrdner: ausOrdner is not null);
}
/// <summary>Der Serienname aus dem Pfad.</summary>
+44 -3
View File
@@ -46,11 +46,44 @@ public static class LibraryWalker
return new WalkResult(gefunden, probleme);
}
Absteigen(wurzel, 0, gefunden, probleme);
// **Jedes echte Verzeichnis genau einmal.** Siehe <see cref="Echtpfad"/>.
var besucht = new HashSet<string>(StringComparer.Ordinal) { Echtpfad(wurzel) };
Absteigen(wurzel, 0, gefunden, probleme, besucht);
return new WalkResult(gefunden, probleme);
}
private static void Absteigen(string ordner, int tiefe, List<ScannedFile> gefunden, List<WalkProblem> probleme)
/// <summary>Wohin ein Verzeichnis wirklich zeigt.</summary>
///
/// <remarks>
/// <b>Ohne das wird aus einem Ordner zwei.</b> Ein Verweis — ein Symlink,
/// ein zweiter Bind Mount derselben Freigabe — macht denselben Inhalt unter
/// einem zweiten Namen erreichbar. Der Scan lief beide Wege ab und legte
/// „Pingu" und „Pingu-nochmal" als zwei Serien an; dieselbe Datei bekam
/// zwei Pfade und damit zwei Zeilen, was am Eindeutigkeitsindex den ganzen
/// Speichervorgang zerriss.
///
/// Lässt sich das Ziel nicht auflösen, gilt der Pfad selbst. Lieber einmal
/// zu viel absteigen als wegen eines Rechtefehlers einen echten Ordner
/// auslassen.
/// </remarks>
private static string Echtpfad(string ordner)
{
try
{
var ziel = Directory.ResolveLinkTarget(ordner, returnFinalTarget: true);
return Path.GetFullPath(ziel?.FullName ?? ordner).TrimEnd(Path.DirectorySeparatorChar);
}
catch (Exception fehler) when (fehler is IOException or UnauthorizedAccessException
or ArgumentException or NotSupportedException)
{
return Path.GetFullPath(ordner).TrimEnd(Path.DirectorySeparatorChar);
}
}
private static void Absteigen(
string ordner, int tiefe, List<ScannedFile> gefunden, List<WalkProblem> probleme,
HashSet<string> besucht)
{
if (tiefe > MaximaleTiefe)
{
@@ -87,7 +120,15 @@ public static class LibraryWalker
foreach (var unter in unterordner)
{
if (MediaExtensions.IsSkippedDirectory(Path.GetFileName(unter))) continue;
Absteigen(unter, tiefe + 1, gefunden, probleme);
if (!besucht.Add(Echtpfad(unter)))
{
probleme.Add(new WalkProblem(
unter, "Zeigt auf einen Ordner, der in diesem Lauf schon erfasst wurde."));
continue;
}
Absteigen(unter, tiefe + 1, gefunden, probleme, besucht);
}
}
}
@@ -20,6 +20,16 @@ public enum Scanphase
/// <summary>Verwaistes wird entfernt, Doppeltes zusammengelegt.</summary>
Aufraeumen,
/// <summary>Eine Notiz fürs Protokoll — nicht für die Fortschrittszeile.</summary>
///
/// <remarks>
/// <b>Warum getrennt.</b> „Serie ‚Pingu' neu angelegt, aus dem Ordner" ist
/// beim Suchen eines Fehlers Gold wert und in einer Zeile, die im
/// Sekundentakt wechselt, nur Flackern. Die Oberfläche überspringt diese
/// Schritte, die Tagesdatei nimmt sie auf.
/// </remarks>
Vermerk,
}
/// <summary>Ein Zwischenstand: was gerade passiert, und wie weit.</summary>
@@ -60,6 +70,7 @@ public readonly record struct Scanschritt(Scanphase Phase, string Was, int Erled
Scanphase.Holen => $"Hole Metadaten zu {Was}{zaehler}",
Scanphase.Bilder => $"Lade Bilder zu {Was}{zaehler}",
Scanphase.Aufraeumen => "Räume auf und lege Doppeltes zusammen",
Scanphase.Vermerk => Was,
_ => Was,
};
}