Ein Klick, der nichts tat, und niemand sagte warum

Eine Serienkachel kannte der Server als Titel nicht: „als gesehen markieren"
lief in eine 404, und weil das Kontextmenü die Ablehnung verschluckte, geschah
sichtbar gar nichts. Dasselbe beim Favoriten. UserItemState bekommt eine
Serienkennung, das Häkchen zieht alle Folgen mit, und was wirft, erscheint als
Meldung statt in der Browserkonsole.

Dazu eine Mehrfachauswahl in der Mediathek — gesehen, ungesehen, Metadaten
neu holen für das Gewählte, in einer Anfrage statt in zweihundert. Ein neues
Plakat steht sofort auf der Kachel, und ein Titel, den es nicht mehr gibt,
endet nicht mehr in einer Sackgasse.

Gemessen: ein POST auf /refresh ohne Rumpf antwortete nach der Erweiterung mit
404 — also genau der Aufruf „alles auffrischen", den es länger gibt als die
Auswahl. Der Rumpf wird jetzt von Hand gelesen, und beide Formen stehen in
einer Prüfung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-05 18:28:33 +02:00
co-authored by Claude Opus 5
parent c854c4a09a
commit 557708b769
21 changed files with 2085 additions and 78 deletions
@@ -18,6 +18,34 @@ public static class LibraryContentEndpoints
/// </remarks>
private const int HöchstensJeSeite = 200;
/// <summary>Wie viele Titel eine Auswahl höchstens umfasst.</summary>
private const int HöchstensJeZug = 1000;
/// <summary>Welche Titel im Rumpf stehen — oder <c>null</c>, wenn keiner da ist.</summary>
///
/// <remarks>
/// <b>Ein fehlender Rumpf ist kein Fehler, sondern die Voreinstellung.</b>
/// „Alles auffrischen" schickt nichts mit, und das muss so bleiben: der
/// Knopf dafür gibt es länger als die Auswahl.
/// </remarks>
private static async Task<IReadOnlyList<Guid>?> Auswahl(HttpRequest anfrage)
{
if (anfrage.ContentLength is null or 0) return null;
if (!anfrage.HasJsonContentType()) return null;
try
{
var gelesen = await anfrage.ReadFromJsonAsync<RefreshRequest>();
return gelesen?.Ids;
}
catch (System.Text.Json.JsonException)
{
// Unlesbarer Rumpf heisst: keine Auswahl. Alles aufzufrischen ist
// hier die harmlosere Auslegung — und sie kostet nur Zeit.
return null;
}
}
/// <summary>Wonach eine Liste geordnet wird.</summary>
///
/// <remarks>
@@ -104,22 +132,25 @@ public static class LibraryContentEndpoints
/// noch niemand angefasst hat, und die Antwort trägt dort <c>null</c>.
/// </remarks>
private static async Task<Dictionary<Guid, ItemStateResponse>> ZuständeZu(
FlaroContext db, ClaimsPrincipal wer, List<Guid>? filme = null, List<Guid>? folgen = null)
FlaroContext db, ClaimsPrincipal wer,
List<Guid>? filme = null, List<Guid>? folgen = null, List<Guid>? serien = null)
{
if (PlaybackEndpoints.Benutzer(wer) is not { } benutzer) return [];
var gesucht = (filme ?? folgen)!;
var gesucht = (filme ?? folgen ?? serien)!;
if (gesucht.Count == 0) return [];
var zeilen = await db.UserItemStates.AsNoTracking()
.Where(z => z.UserId == benutzer)
.Where(z => filme != null
? z.MovieId != null && gesucht.Contains(z.MovieId!.Value)
: z.EpisodeId != null && gesucht.Contains(z.EpisodeId!.Value))
: folgen != null
? z.EpisodeId != null && gesucht.Contains(z.EpisodeId!.Value)
: z.SeriesId != null && gesucht.Contains(z.SeriesId!.Value))
.ToListAsync();
return zeilen.ToDictionary(
z => (filme != null ? z.MovieId : z.EpisodeId)!.Value,
z => (filme != null ? z.MovieId : folgen != null ? z.EpisodeId : z.SeriesId)!.Value,
PlaybackEndpoints.Abbild);
}
@@ -160,12 +191,33 @@ public static class LibraryContentEndpoints
// erst beim nächsten neuen Film. Geholt wird über die bekannte
// Kennung, nicht über eine neue Suche; eine Zuordnung von Hand
// bleibt damit stehen.
//
// **Mit einer Auswahl im Rumpf nur diese Titel.** Zehn ausgewählte
// Filme kosten zehn Anfragen bei TMDB; der ganze Bestand viertausend
// und eine Viertelstunde. Ohne Rumpf bleibt es beim ganzen Bestand.
//
// **Der Rumpf wird von Hand gelesen, und das hat einen Grund.** Als
// `RefreshRequest?` im Kopf der Methode lehnte der Server jede Anfrage
// ohne Rumpf mit 404 ab — also genau den Aufruf „die ganze Mediathek
// auffrischen", den es vorher schon gab. Gemessen, nicht vermutet: ohne
// Rumpf 404, mit `{}` 202.
app.MapPost("/api/v1/libraries/{id:guid}/refresh", async (
Guid id, FlaroContext db, ScanCoordinator koordinator) =>
Guid id, HttpRequest anfrage, FlaroContext db, ScanCoordinator koordinator) =>
{
if (!await db.Libraries.AnyAsync(l => l.Id == id)) return Results.NotFound();
return koordinator.TryStart(id, erneut: true)
IReadOnlySet<Guid>? nurDiese = null;
if (await Auswahl(anfrage) is { Count: > 0 } gewaehlt)
{
if (gewaehlt.Count > HöchstensJeZug)
return Results.Problem(
$"Mehr als {HöchstensJeZug} Titel auf einmal gehen nicht. "
+ "Nimm weniger und wiederhole es.",
statusCode: StatusCodes.Status400BadRequest);
nurDiese = gewaehlt.ToHashSet();
}
return koordinator.TryStart(id, erneut: true, nurDiese: nurDiese)
? Results.AcceptedAtRoute("Scanstand", new { id })
: Results.Problem(
"Für diese Bibliothek läuft bereits ein Lauf.",
@@ -401,6 +453,7 @@ public static class LibraryContentEndpoints
// zusammen. Je Serie zu fragen wäre bei fünfzig Serien
// einundfünfzig Abfragen.
var gesehen = await GeseheneFolgen(db, wer, folgen.Select(f => f.Id).ToList());
var stände = await ZuständeZu(db, wer, serien: ids);
var items = serien.Select(s =>
{
@@ -411,7 +464,8 @@ public static class LibraryContentEndpoints
meine.Count(f => !gesehen.Contains(f.Id)),
s.LogoImage, s.RuntimeMinutes, s.Rating, null, s.ContentRating,
OriginalTitle: s.OriginalTitle,
PublishAt: s.PublishAt);
PublishAt: s.PublishAt,
State: stände.GetValueOrDefault(s.Id));
}).ToList();
return Results.Ok(new Page<SeriesResponse>(items, gesamt));
@@ -538,7 +592,8 @@ public static class LibraryContentEndpoints
uebersetzt?.Tagline ?? serie.Tagline, serie.ContentRating,
await GenresZu(db, film: null, serie: id),
await MitwirkendeZu(db, film: null, serie: id),
OriginalTitle: serie.OriginalTitle));
OriginalTitle: serie.OriginalTitle,
State: (await ZuständeZu(db, wer, serien: [id])).GetValueOrDefault(id)));
}).RequireAuthorization();
// **Die nächste Folge nach dieser.** Reihenfolge ist Staffel, dann