diff --git a/server/src/Flaro.Data/LibraryScanner.cs b/server/src/Flaro.Data/LibraryScanner.cs
index 142eb14..aeee126 100644
--- a/server/src/Flaro.Data/LibraryScanner.cs
+++ b/server/src/Flaro.Data/LibraryScanner.cs
@@ -62,6 +62,7 @@ public sealed class LibraryScanner(
: [];
int neu = 0, geändert = 0, unerkannt = 0, untersucht = 0;
+ var seitLetztemSchreiben = 0;
foreach (var datei in ergebnis.Files)
{
@@ -129,6 +130,27 @@ public sealed class LibraryScanner(
}
if (mussUntersucht && await UntersuchenAsync(eintrag, laufZeit, ct)) untersucht++;
+
+ // **Zwischendurch schreiben, nicht erst am Ende.**
+ //
+ // Gemessen an achtzig Dateien erscheinen die Titel ohnehin
+ // laufend — denn speichert je
+ // Datei. Nur gilt das eben *nur* dort: scheitert die
+ // Untersuchung, kehrt sie vor dem Speichern um. Ein Bestand aus
+ // Dateien, die ffprobe nicht lesen kann, bliebe bis zum Schluss
+ // unsichtbar — und wer dabei zusieht, hält den Scan für hängend.
+ //
+ // Fünfzig, nicht jede: ein Schreibvorgang je Datei kostet bei
+ // vierzigtausend mehr, als die Sichtbarkeit wert ist.
+ //
+ // Für das Aufräumen weiter unten ist das unbedenklich. Es
+ // vergleicht gegen `laufZeit`, und die steht vor der Schleife
+ // fest; jede hier geschriebene Datei trägt genau diesen Wert.
+ if (++seitLetztemSchreiben >= 50)
+ {
+ await db.SaveChangesAsync(ct);
+ seitLetztemSchreiben = 0;
+ }
}
await db.SaveChangesAsync(ct);
diff --git a/server/tests/Flaro.Api.Tests/ScanTests.cs b/server/tests/Flaro.Api.Tests/ScanTests.cs
index 4e0ba08..94699bc 100644
--- a/server/tests/Flaro.Api.Tests/ScanTests.cs
+++ b/server/tests/Flaro.Api.Tests/ScanTests.cs
@@ -118,6 +118,42 @@ public class ScanTests
Assert.That(filme!.Items.Select(m => m.Title), Is.EqualTo(new[] { "The Matrix" }));
}
+ /// Das Aufräumen darf nicht wegräumen, was derselbe Lauf schrieb.
+ ///
+ ///
+ /// Mehr als fünfzig Dateien, und das ist der Punkt. Seit der Scan
+ /// alle fünfzig Dateien zwischenspeichert, liegt ein Teil des Bestands
+ /// schon in der Datenbank, während der Lauf noch läuft. Direkt danach
+ /// löscht er alles, was eine ältere SeenAt-Zeit trägt als dieser
+ /// Lauf — würde die Zeit irgendwo neu genommen statt vor der Schleife
+ /// festgehalten, risse dieser Schritt den halben Bestand mit.
+ ///
+ /// Die Dateien hier sind Nullbytes, an denen ffprobe scheitert. Damit
+ /// greift das Zwischenspeichern aus der Schleife und nicht das aus der
+ /// Untersuchung — also genau der Weg, der sonst ungeprüft bliebe.
+ ///
+ [Test]
+ public async Task Ein_grosser_Bestand_ueberlebt_das_Aufraeumen()
+ {
+ for (var i = 1; i <= 120; i++) Datei($"Prüffilm {i:000} (2024).mkv");
+
+ using var server = new FlaroServer();
+ var (client, bibliothek) = await MitBibliothek(server, "Movies");
+
+ var erster = await Scannen(client, bibliothek);
+
+ Assert.That(erster.Added, Is.EqualTo(120));
+ Assert.That(erster.Removed, Is.Zero, "Ein erster Lauf darf nichts entfernen.");
+ Assert.That((await client.GetFromJsonAsync>("/api/v1/movies"))!.Total,
+ Is.EqualTo(120));
+
+ // Und ein zweiter Lauf bleibt ebenso vollständig.
+ var zweiter = await Scannen(client, bibliothek);
+ Assert.That(zweiter.Removed, Is.Zero);
+ Assert.That((await client.GetFromJsonAsync>("/api/v1/movies"))!.Total,
+ Is.EqualTo(120));
+ }
+
/// 4K und 1080p sind ein Film mit zwei Dateien, nicht zwei Filme.
[Test]
public async Task Zwei_Fassungen_desselben_Films_bleiben_ein_Eintrag()