using System.Collections.Concurrent;
using Halo.Data;
namespace Halo.Api;
public sealed record ScanState(
string Status,
DateTimeOffset StartedAt,
DateTimeOffset? FinishedAt,
ScanReport? Report,
EnrichReport? Enrichment,
string? Error);
/// Lässt Scans im Hintergrund laufen und weiss, wie weit sie sind.
///
///
///
/// Nicht in der Anfrage. Ein Bestand mit 40 000 Titeln braucht Minuten;
/// eine HTTP-Anfrage, die so lange offen steht, läuft hinter jedem Reverse
/// Proxy ins Zeitlimit — und der Anwender sieht einen Fehler, während der Scan
/// in Wahrheit tadellos weiterläuft.
///
///
/// Einer je Bibliothek, nicht mehr. Zwei gleichzeitige Läufe über
/// dasselbe Verzeichnis würden sich beim Aufräumen gegenseitig die Einträge
/// löschen: der eine schreibt seine Startzeit, der andere hält sie für alt.
///
///
/// Der Zustand liegt im Arbeitsspeicher und nicht in der Datenbank. Ein
/// Neustart bricht den Scan ohnehin ab, und ein „läuft" aus einem früheren
/// Leben wäre dann eine Lüge, die niemand mehr aufräumt.
///
///
public sealed class ScanCoordinator(
IServiceScopeFactory scopes,
TimeProvider clock,
ILogger log)
{
private readonly ConcurrentDictionary _zustand = new();
public ScanState? State(Guid libraryId) => _zustand.GetValueOrDefault(libraryId);
public bool IsRunning(Guid libraryId) => State(libraryId)?.Status == "running";
/// Startet einen Lauf — oder sagt, dass schon einer läuft.
public bool TryStart(Guid libraryId)
{
var start = new ScanState("running", clock.GetUtcNow(), null, null, null, null);
var vorher = _zustand.AddOrUpdate(
libraryId,
start,
(_, alt) => alt.Status == "running" ? alt : start);
// Lief schon einer, steht hier dessen Zustand und nicht unserer.
if (!ReferenceEquals(vorher, start)) return false;
_ = Task.Run(() => Laufen(libraryId));
return true;
}
private async Task Laufen(Guid libraryId)
{
try
{
using var bereich = scopes.CreateScope();
var scanner = bereich.ServiceProvider.GetRequiredService();
var bericht = await scanner.ScanAsync(libraryId);
// Der Scan ist durch, die Dateien stehen in der Mediathek. Was
// jetzt noch kommt, kann scheitern, ohne dass davon etwas verloren
// geht — deshalb erst hier und in eigenem Zustand.
_zustand[libraryId] = _zustand[libraryId] with { Status = "enriching", Report = bericht };
var metadaten = await bereich.ServiceProvider
.GetRequiredService().EnrichAsync(libraryId);
_zustand[libraryId] = _zustand[libraryId] with
{
Status = "finished",
FinishedAt = clock.GetUtcNow(),
Enrichment = metadaten,
};
}
catch (Exception fehler)
{
log.LogError(fehler, "Scan der Bibliothek {Kennung} ist gescheitert.", libraryId);
_zustand[libraryId] = _zustand[libraryId] with
{
Status = "failed",
FinishedAt = clock.GetUtcNow(),
Error = fehler.Message,
};
}
}
}