Der Fortschritt sagt, woran er gerade ist — und schreibt es mit

Vorher stand in der Verwaltung minutenlang "Wird durchsucht…", ohne
Hinweis, ob überhaupt etwas passiert. Bei vierzigtausend Dateien ist das
nicht zu unterscheiden von einem hängenden Lauf.

Scanschritt trägt Phase, Gegenstand und Zählstand und bildet daraus den
Satz selbst — im Kern und nicht in der Oberfläche, weil die Apps denselben
brauchen und zweimal formuliert er auseinanderdriftet. Sechs Phasen von
"Suche Medien in /media/Filme" über "Suche ‚Ich einfach unverbesserlich'
bei der Metadatenquelle — 44 von 1204" bis "Räume auf und lege Doppeltes
zusammen". Pfade werden auf die letzten zwei Bestandteile gekürzt.

ScanAsync und EnrichAsync bekommen je einen optionalen IProgress; optional
heisst, dass kein vorhandener Aufrufer bricht. Der Koordinator hält nur
den letzten Schritt — eine Liste über vierzigtausend Titel wäre ein Leck
im Arbeitsspeicher.

Dazu das Tagebuch: eine Datei je Tag unter config/logs, nach lokaler Zeit
und nicht nach UTC, weil ein Scan um 23 Uhr am heutigen Datum gesucht
wird. Dreissig Tage Aufbewahrung, einstellbar über FLARO_LOG_TAGE — ohne
Grenze läuft die config-Partition eines NAS lautlos voll. Es wirft nie:
ein volles Dateisystem darf keinen Scan abbrechen.

In der Verwaltung lesbar, nach Fehlern filterbar und herunterladbar. Das
Containerprotokoll ist nach einem Neustart weg und nur über SSH
erreichbar — die Frage "warum wurde der Film gestern Nacht nicht erkannt?"
liess sich damit nicht beantworten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 12:15:18 +02:00
co-authored by Claude Opus 5
parent 637e95fbb1
commit f359807561
12 changed files with 411 additions and 16 deletions
+8 -1
View File
@@ -38,7 +38,8 @@ public sealed class BestandVerschwundenException(string nachricht) : Exception(n
public sealed class LibraryScanner(
FlaroContext db, IMediaProbe sonde, TimeProvider clock, ILogger<LibraryScanner> log)
{
public async Task<ScanReport> ScanAsync(Guid libraryId, CancellationToken ct = default)
public async Task<ScanReport> ScanAsync(
Guid libraryId, IProgress<Scanschritt>? fortschritt = null, CancellationToken ct = default)
{
var bibliothek = await db.Libraries.FindAsync([libraryId], ct)
?? throw new InvalidOperationException($"Keine Bibliothek mit der Kennung {libraryId}.");
@@ -60,6 +61,7 @@ public sealed class LibraryScanner(
"Es wurde nichts verändert.");
}
fortschritt?.Report(new Scanschritt(Scanphase.Suchen, bibliothek.Path));
var ergebnis = LibraryWalker.Walk(bibliothek.Path);
if (ergebnis.Files.Count == 0 && warVorherDa > 0)
@@ -93,9 +95,13 @@ public sealed class LibraryScanner(
int neu = 0, geändert = 0, unerkannt = 0, untersucht = 0;
var seitLetztemSchreiben = 0;
var durch = 0;
foreach (var datei in ergebnis.Files)
{
ct.ThrowIfCancellationRequested();
fortschritt?.Report(new Scanschritt(
Scanphase.Pruefen, datei.Path, ++durch, ergebnis.Files.Count));
Guid? filmId = null, folgeId = null;
@@ -196,6 +202,7 @@ public sealed class LibraryScanner(
.Where(f => f.LibraryId == libraryId && f.SeenAt < laufZeit && f.MissingSince == null)
.ExecuteUpdateAsync(s => s.SetProperty(f => f.MissingSince, laufZeit), ct);
fortschritt?.Report(new Scanschritt(Scanphase.Aufraeumen, ""));
await VerwaisteAufraeumen(libraryId, ct);
log.LogInformation(
+22 -7
View File
@@ -43,7 +43,8 @@ public sealed class MetadataEnricher(
/// Suche.</b> Eine Zuordnung von Hand bleibt damit stehen.
/// </param>
public async Task<EnrichReport> EnrichAsync(
Guid libraryId, bool erneut = false, CancellationToken ct = default)
Guid libraryId, bool erneut = false,
IProgress<Scanschritt>? fortschritt = null, CancellationToken ct = default)
{
if (!await anbieter.IsConfiguredAsync(ct))
{
@@ -54,9 +55,9 @@ public sealed class MetadataEnricher(
int filme = 0, serien = 0, folgen = 0;
try
{
filme = await FilmeZuordnen(libraryId, erneut, ct);
serien = await SerienZuordnen(libraryId, erneut, ct);
folgen = await FolgenZuordnen(libraryId, erneut, ct);
filme = await FilmeZuordnen(libraryId, erneut, fortschritt, ct);
serien = await SerienZuordnen(libraryId, erneut, fortschritt, ct);
folgen = await FolgenZuordnen(libraryId, erneut, fortschritt, ct);
}
catch (MetadataCredentialsException fehler)
{
@@ -71,16 +72,20 @@ public sealed class MetadataEnricher(
return new EnrichReport(filme, serien, folgen, Skipped: false, null);
}
private async Task<int> FilmeZuordnen(Guid libraryId, bool erneut, CancellationToken ct)
private async Task<int> FilmeZuordnen(
Guid libraryId, bool erneut, IProgress<Scanschritt>? fortschritt, CancellationToken ct)
{
var offen = await db.Movies
.Where(m => m.LibraryId == libraryId && (erneut || m.TmdbId == null))
.ToListAsync(ct);
var zugeordnet = 0;
var durch = 0;
foreach (var film in offen)
{
ct.ThrowIfCancellationRequested();
fortschritt?.Report(new Scanschritt(
Scanphase.Nachschlagen, film.Title, ++durch, offen.Count));
var datei = await db.MediaFiles.FirstOrDefaultAsync(f => f.MovieId == film.Id, ct);
var (vorgegeben, festgelegt) = await VorhandeneZuordnung(datei, SidecarNames.MovieNfo, ct);
@@ -142,16 +147,20 @@ public sealed class MetadataEnricher(
return (new ProviderIds(ausNfo.Tmdb ?? ausPfad.Tmdb, ausNfo.Imdb ?? ausPfad.Imdb), Nfo.ReadLocked(nfo));
}
private async Task<int> SerienZuordnen(Guid libraryId, bool erneut, CancellationToken ct)
private async Task<int> SerienZuordnen(
Guid libraryId, bool erneut, IProgress<Scanschritt>? fortschritt, CancellationToken ct)
{
var offen = await db.Series
.Where(s => s.LibraryId == libraryId && (erneut || s.TmdbId == null))
.ToListAsync(ct);
var zugeordnet = 0;
var durch = 0;
foreach (var serie in offen)
{
ct.ThrowIfCancellationRequested();
fortschritt?.Report(new Scanschritt(
Scanphase.Nachschlagen, serie.Title, ++durch, offen.Count));
// Bei einer Serie liegt die NFO neben der Folge, also im
// Staffelordner — gesucht wird deshalb ab der ersten Folgendatei.
@@ -230,7 +239,8 @@ public sealed class MetadataEnricher(
}
/// <summary>Folgen nachschlagen — aber nur bei Serien, die zugeordnet sind.</summary>
private async Task<int> FolgenZuordnen(Guid libraryId, bool erneut, CancellationToken ct)
private async Task<int> FolgenZuordnen(
Guid libraryId, bool erneut, IProgress<Scanschritt>? fortschritt, CancellationToken ct)
{
var serien = await db.Series
.Where(s => s.LibraryId == libraryId && s.TmdbId != null)
@@ -244,8 +254,13 @@ public sealed class MetadataEnricher(
.ToListAsync(ct);
var zugeordnet = 0;
var durch = 0;
foreach (var folge in offen)
{
fortschritt?.Report(new Scanschritt(
Scanphase.Holen,
$"Folge {folge.SeasonNumber}x{folge.EpisodeNumber:00}", ++durch, offen.Count));
ct.ThrowIfCancellationRequested();
var serie = serien.First(s => s.Id == folge.SeriesId);