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; } }