using Flaro.Core.Playback;
using Flaro.Data;
using Microsoft.EntityFrameworkCore;
namespace Flaro.Api;
/// Bringt die gemerkten Sehstände wieder mit der Wirklichkeit in Einklang.
///
///
///
/// Der Anlass. Ein Film stand bei 1:32, und in „Fortsetzen" tauchte
/// er trotzdem nicht auf — bis er einmal neu gestartet wurde. Der Grund:
/// die Reihe fragt nicht die Regel, sondern deren festgehaltenes Ergebnis,
/// die Spalte InProgress. Die Bruchrechnung „Stelle kleiner als
/// Anteil der Laufzeit" lässt sich nicht nach SQL übersetzen, deshalb steht
/// sie als Wahrheitswert in der Zeile.
///
///
/// Und berechnet wird sie nur, wenn eine Meldung eintrifft. Die
/// zerstörerische Meldung von vorgestern — zu kurze Laufzeit, Stelle
/// beschnitten, Häkchen gesetzt — hat dort false hinterlassen. Die
/// Korrektur am Server verhindert neuen Schaden, räumt den alten aber
/// nicht weg: dafür müsste jemand jeden betroffenen Titel einmal anspielen.
///
///
/// Zwei Schritte, und die Reihenfolge zählt. Erst die Laufzeit aus
/// der Datei nachtragen, dann neu bewerten — eine Neubewertung auf Basis
/// eines Stummels käme wieder zu false.
///
///
public sealed class Sehstandpflege(
IServiceScopeFactory scopes,
TimeProvider uhr,
ILogger log)
{
/// Unter diesem Schlüssel steht, für welchen Stand es schon lief.
///
///
/// Einmal je Abbild. Nach jedem Update einmal durchrechnen ist
/// billig — es ist ein Durchlauf über eine Tabelle mit ein paar tausend
/// Zeilen — und fängt genau diese Art Fehler ab: eine Regel ändert sich,
/// und das Festgehaltene bleibt stehen.
///
public const string Schluessel = "wartung.sehstaende.stand";
public async Task NachUpdateAsync(CancellationToken ct = default)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService();
var stand = await db.Settings.AsNoTracking().FirstOrDefaultAsync(s => s.Key == Schluessel, ct);
if (stand?.Value == Buildkennung.Build) return 0;
var geaendert = await LaufenAsync(ct);
var zeile = await db.Settings.FindAsync([Schluessel], ct);
if (zeile is null) db.Settings.Add(new Core.ServerSetting { Key = Schluessel, Value = Buildkennung.Build });
else zeile.Value = Buildkennung.Build;
await db.SaveChangesAsync(ct);
if (geaendert > 0)
{
log.LogInformation(
"Sehstände nachgerechnet: {Anzahl} Eintrag/Einträge stehen jetzt richtig.", geaendert);
}
return geaendert;
}
/// Trägt fehlende Laufzeiten nach und bewertet neu.
public async Task LaufenAsync(CancellationToken ct = default)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService();
var regeln = await raum.ServiceProvider
.GetRequiredService().RegelnAsync();
var jetzt = uhr.GetUtcNow();
const int Stapel = 500;
var geaendert = 0;
for (var sprung = 0; ; sprung += Stapel)
{
var zeilen = await db.UserItemStates
.OrderBy(z => z.Id).Skip(sprung).Take(Stapel).ToListAsync(ct);
if (zeilen.Count == 0) break;
// **Die Laufzeiten in einem Zug, nicht je Zeile.** Fünfhundert
// Einzelabfragen für fünfhundert Zeilen wären bei achttausend
// Sehständen sechzehntausend Rundreisen.
var filme = zeilen.Where(z => z.MovieId is not null).Select(z => z.MovieId!.Value).ToList();
var folgen = zeilen.Where(z => z.EpisodeId is not null).Select(z => z.EpisodeId!.Value).ToList();
var filmlaufzeit = await db.MediaFiles.AsNoTracking()
.Where(d => d.MovieId != null && filme.Contains(d.MovieId!.Value))
.GroupBy(d => d.MovieId!.Value)
.Select(g => new { Id = g.Key, Dauer = g.Max(d => d.Duration) })
.ToDictionaryAsync(x => x.Id, x => x.Dauer, ct);
var folgenlaufzeit = await db.MediaFiles.AsNoTracking()
.Where(d => d.EpisodeId != null && folgen.Contains(d.EpisodeId!.Value))
.GroupBy(d => d.EpisodeId!.Value)
.Select(g => new { Id = g.Key, Dauer = g.Max(d => d.Duration) })
.ToDictionaryAsync(x => x.Id, x => x.Dauer, ct);
foreach (var z in zeilen)
{
var gemessen = z.MovieId is { } f
? filmlaufzeit.GetValueOrDefault(f)
: z.EpisodeId is { } e ? folgenlaufzeit.GetValueOrDefault(e) : null;
var vorher = (z.Runtime, z.InProgress);
// **Die gemessene Laufzeit gewinnt.** Steht in der Zeile ein
// Stummel, weil ein Client ihn einmal gemeldet hat, bleibt
// der sonst für immer stehen.
if (gemessen is { Ticks: > 0 }) z.Runtime = gemessen;
z.InProgress = ProgressRule.BelongsInContinueWatching(z, regeln);
if (vorher == (z.Runtime, z.InProgress)) continue;
// **`UpdatedAt` bleibt, wo es war.** Danach ist „Fortsetzen"
// sortiert; eine Reparatur, die alles auf heute setzt,
// mischte die Reihenfolge neu und sähe aus wie ein Fehler.
geaendert++;
}
await db.SaveChangesAsync(ct);
if (zeilen.Count < Stapel) break;
}
return geaendert;
}
}