Neue Funde erst prüfen, dann veröffentlichen

Je Mediathek einstellbar, Vorgabe aus: wer eine Mediathek anlegt, will sie
sehen. Angeschaltet warten neue Funde unter "Zu prüfen", bis jemand sie
freigibt — oder ihnen einen Termin gibt.

Ein Zeitpunkt, kein Zustandsfeld. PublishAt null heisst wartet, in der
Vergangenheit heisst veröffentlicht, in der Zukunft heisst geplant. Die
Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst
umgeschaltet; damit stimmt sie nach einem Neustart, nach einer
Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt gar
nicht lief. Ein Ticker hätte in jedem dieser Fälle das Falsche getan.

Das eigentliche Risiko waren die zwanzig Abfragestellen in vier Dateien.
Schriebe jede ihren eigenen Filter, reichte eine vergessene — und
auffallen würde es nicht beim Programmieren, sondern wenn jemand im
Haushalt einen Titel sieht, den er nicht sehen sollte. Deshalb ein
globaler Abfragefilter in FlaroContext, der sich nicht vergessen lässt,
und eine Middleware, die je Anfrage setzt, wer Unveröffentlichtes sehen
darf. Vorgabe am Kontext ist "alles sehen", damit Scanner und Metadaten
ohne Benutzer arbeiten können; für jede HTTP-Anfrage entscheidet die
Middleware.

Der Test zählt die Inhaltsrouten auf und prüft jede einzeln auf die
Abwesenheit eines wartenden Titels. Eine neue Route, die den Filter
umgeht, fällt dort durch.

Die Migration trägt für alle vorhandenen Titel das Hinzufügedatum als
Veröffentlichung nach. Ohne diese zwei Zeilen stünde jeder auf null — und
der gesamte Bestand wäre für alle ausser Administratoren verschwunden,
sofort beim Start im Container und ohne Meldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 13:03:24 +02:00
co-authored by Claude Opus 5
parent eb63271e92
commit 632d3efa35
18 changed files with 1726 additions and 11 deletions
@@ -17,4 +17,14 @@ public sealed class Library
public required LibraryKind Kind { get; set; }
public required string Path { get; set; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
/// <summary>Ob neue Funde erst geprüft werden, bevor sie erscheinen.</summary>
///
/// <remarks>
/// <b>Vorgabe: aus.</b> Wer eine Mediathek anlegt, will sie sehen. Die
/// Prüfung ist für den Fall gedacht, dass Zuordnung und Bilder stimmen
/// sollen, bevor jemand im Haushalt den Film findet — oder dass etwas
/// erst zu einem bestimmten Zeitpunkt erscheinen soll.
/// </remarks>
public bool ReviewNewItems { get; set; }
}
@@ -110,6 +110,24 @@ public sealed class Movie : Flaro.Core.Metadata.IBebildert
/// <summary>Ohne führenden Artikel — siehe <see cref="SortTitle"/>.</summary>
public required string SortTitleValue { get; set; }
/// <summary>Ab wann dieser Titel für alle sichtbar ist.</summary>
///
/// <remarks>
/// <para>
/// <c>null</c> heisst: wartet auf Prüfung. Ein Zeitpunkt in der
/// Vergangenheit heisst veröffentlicht, einer in der Zukunft geplant.
/// </para>
/// <para>
/// <b>Ein Zeitpunkt, kein Zustandsfeld, und das ist der Kern.</b> Die
/// Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst
/// umgeschaltet. Damit stimmt sie nach einem Neustart, nach einer
/// Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt
/// gar nicht lief — ein Ticker hätte in jedem dieser Fälle das Falsche
/// getan oder gar nichts.
/// </para>
/// </remarks>
public DateTimeOffset? PublishAt { get; set; }
/// <summary>Von Hand gewählt — ein Auffrischen überschreibt es nicht.</summary>
///
/// <remarks>
@@ -229,6 +247,24 @@ public sealed class Series : Flaro.Core.Metadata.IBebildert
public required string Title { get; set; }
public required string SortTitleValue { get; set; }
/// <summary>Ab wann dieser Titel für alle sichtbar ist.</summary>
///
/// <remarks>
/// <para>
/// <c>null</c> heisst: wartet auf Prüfung. Ein Zeitpunkt in der
/// Vergangenheit heisst veröffentlicht, einer in der Zukunft geplant.
/// </para>
/// <para>
/// <b>Ein Zeitpunkt, kein Zustandsfeld, und das ist der Kern.</b> Die
/// Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst
/// umgeschaltet. Damit stimmt sie nach einem Neustart, nach einer
/// Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt
/// gar nicht lief — ein Ticker hätte in jedem dieser Fälle das Falsche
/// getan oder gar nichts.
/// </para>
/// </remarks>
public DateTimeOffset? PublishAt { get; set; }
/// <summary>Von Hand gewählt — ein Auffrischen überschreibt es nicht.</summary>
///
/// <remarks>
@@ -326,5 +362,15 @@ public sealed class Episode
/// <summary>Das Standbild der Folge.</summary>
public string? StillImage { get; set; }
/// <summary>Ab wann diese Folge für alle sichtbar ist — siehe <see cref="Movie.PublishAt"/>.</summary>
///
/// <remarks>
/// <b>An der Folge, nicht an der Serie.</b> Eine neue Staffel einer
/// laufenden Serie soll sich einzeln planen lassen; eine Serie ist
/// sichtbar, sobald eine ihrer Folgen es ist, und braucht damit kein
/// eigenes Feld.
/// </remarks>
public DateTimeOffset? PublishAt { get; set; }
public DateTimeOffset AddedAt { get; init; } = DateTimeOffset.UtcNow;
}