Ein Film stand bei 1:32 und tauchte in „Fortsetzen" trotzdem nicht auf. 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, also steht sie als Wahrheitswert in der Zeile — und berechnet wird sie nur, wenn eine Fortschrittsmeldung eintrifft. Die kaputte Meldung von vorgestern hatte dort `false` hinterlassen; die Korrektur am Server verhinderte neuen Schaden, räumte den alten aber nicht weg. `Sehstandpflege` rechnet nach jedem Update einmal durch: erst die Laufzeit aus der Datei nachtragen, dann neu bewerten — in dieser Reihenfolge, denn eine Neubewertung auf Basis eines Stummels käme wieder zu `false`. `UpdatedAt` bleibt, wo es war; danach ist die Reihe sortiert. Und der Knopf heisst jetzt so wie die Reihe, über der er steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.7 KiB
C#
136 lines
5.7 KiB
C#
using Flaro.Core.Playback;
|
|
using Flaro.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Flaro.Api;
|
|
|
|
/// <summary>Bringt die gemerkten Sehstände wieder mit der Wirklichkeit in Einklang.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Der Anlass.</b> 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 <c>InProgress</c>. Die Bruchrechnung „Stelle kleiner als
|
|
/// Anteil der Laufzeit" lässt sich nicht nach SQL übersetzen, deshalb steht
|
|
/// sie als Wahrheitswert in der Zeile.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Und berechnet wird sie nur, wenn eine Meldung eintrifft.</b> Die
|
|
/// zerstörerische Meldung von vorgestern — zu kurze Laufzeit, Stelle
|
|
/// beschnitten, Häkchen gesetzt — hat dort <c>false</c> 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Zwei Schritte, und die Reihenfolge zählt.</b> Erst die Laufzeit aus
|
|
/// der Datei nachtragen, dann neu bewerten — eine Neubewertung auf Basis
|
|
/// eines Stummels käme wieder zu <c>false</c>.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class Sehstandpflege(
|
|
IServiceScopeFactory scopes,
|
|
TimeProvider uhr,
|
|
ILogger<Sehstandpflege> log)
|
|
{
|
|
/// <summary>Unter diesem Schlüssel steht, für welchen Stand es schon lief.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Einmal je Abbild.</b> 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.
|
|
/// </remarks>
|
|
public const string Schluessel = "wartung.sehstaende.stand";
|
|
|
|
public async Task<int> NachUpdateAsync(CancellationToken ct = default)
|
|
{
|
|
using var raum = scopes.CreateScope();
|
|
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>Trägt fehlende Laufzeiten nach und bewertet neu.</summary>
|
|
public async Task<int> LaufenAsync(CancellationToken ct = default)
|
|
{
|
|
using var raum = scopes.CreateScope();
|
|
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
|
|
var regeln = await raum.ServiceProvider
|
|
.GetRequiredService<Wiedergabeeinstellungen>().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;
|
|
}
|
|
}
|