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>
104 lines
4.2 KiB
C#
104 lines
4.2 KiB
C#
namespace Flaro.Core.Playback;
|
|
|
|
/// <summary>Was ein Benutzer mit einem Titel zu tun hatte.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Je Benutzer, nicht je Server.</b> Zwei Leute im selben Haushalt stehen
|
|
/// an verschiedenen Stellen desselben Films, und wessen Stand gilt, darf
|
|
/// nicht davon abhängen, wer zuletzt geschaut hat.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Der Verweis zeigt auf Film <i>oder</i> Folge</b>, wie bei
|
|
/// <c>MediaFile</c>. Eine gemeinsame Kennung mit einem Typfeld daneben wäre
|
|
/// kürzer, aber die Datenbank könnte dann nicht mehr dafür einstehen, dass
|
|
/// der Verweis irgendwohin zeigt.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class UserItemState
|
|
{
|
|
public Guid Id { get; init; } = Guid.CreateVersion7();
|
|
public required Guid UserId { get; init; }
|
|
|
|
public Guid? MovieId { get; init; }
|
|
public Guid? EpisodeId { get; init; }
|
|
|
|
/// <summary>Die Serie als Ganzes — für Favorit, Bewertung und das Häkchen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Eine Serie wird nicht angeschaut, ihre Folgen werden es</b> — das
|
|
/// bleibt richtig, und deshalb steht hier kein Fortschritt. Was hier steht,
|
|
/// ist, was sich an einer Folge nicht festmachen lässt: ob die Serie ein
|
|
/// Favorit ist, wie der Benutzer sie bewertet, und ob er sie für erledigt
|
|
/// erklärt hat.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Warum überhaupt eine Zeile und nicht nur gezählte Folgen.</b> Ohne
|
|
/// sie lief jede Aktion auf einer Serienkachel — Häkchen, Favorit — in eine
|
|
/// 404: der Server kannte zu dieser Kennung keinen Zustand. Sichtbar war
|
|
/// davon nichts; der Klick tat einfach nichts.
|
|
/// </para>
|
|
/// </remarks>
|
|
public Guid? SeriesId { get; init; }
|
|
|
|
/// <summary>Wo der Benutzer steht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Ab der ersten Sekunde, ohne untere Schwelle.</b> Das ist der
|
|
/// Unterschied, um dessentwillen dieser Server existiert — siehe
|
|
/// <see cref="ProgressRule"/>.
|
|
/// </remarks>
|
|
public TimeSpan Position { get; set; }
|
|
|
|
/// <summary>Wie lang der Titel ist, so wie der Client es gemeldet hat.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Hier abgelegt, damit „wie weit ist er" ohne einen Blick in die Dateien
|
|
/// zu beantworten ist. Bei zwei Fassungen eines Films — 4K und 1080p —
|
|
/// können sich die Laufzeiten um Sekunden unterscheiden; was zählt, ist
|
|
/// die, gegen die gemessen wurde.
|
|
/// </remarks>
|
|
public TimeSpan? Runtime { get; set; }
|
|
|
|
public bool Watched { get; set; }
|
|
public int PlayCount { get; set; }
|
|
public DateTimeOffset? LastPlayedAt { get; set; }
|
|
|
|
/// <summary>Aus „Weiterschauen" genommen, ohne als gesehen zu gelten.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Der Fall, für den es sonst nichts gibt.</b> Nach einer halben Stunde
|
|
/// keine Lust mehr — der Film ist weder gesehen noch ungesehen, er soll
|
|
/// nur nicht weiter oben stehen. „Als gesehen" wäre eine Lüge, „als
|
|
/// ungesehen" täte nichts: die Position bliebe.
|
|
///
|
|
/// Die Position bleibt auch hier erhalten. Wer doch weitermacht, fängt
|
|
/// nicht von vorn an.
|
|
/// </remarks>
|
|
public bool Dismissed { get; set; }
|
|
|
|
public bool Favourite { get; set; }
|
|
|
|
/// <summary>Eine eigene Bewertung von 1 bis 10, oder keine.</summary>
|
|
public int? Rating { get; set; }
|
|
|
|
/// <summary>Ob dieser Titel in „Weiterschauen" gehört.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Kein zweites Urteil, sondern das festgehaltene Ergebnis des
|
|
/// ersten.</b> Geschrieben wird es ausschliesslich aus
|
|
/// <see cref="ProgressRule.BelongsInContinueWatching"/>, bei jeder
|
|
/// Änderung an dieser Zeile.
|
|
///
|
|
/// Warum überhaupt eine Spalte: die Regel vergleicht die Stelle mit einem
|
|
/// Anteil der Laufzeit, und diese Rechnung lässt sich nicht nach SQL
|
|
/// übersetzen. Die Regel daneben ein zweites Mal als Abfrage zu schreiben
|
|
/// wäre die Variante, bei der beide irgendwann auseinanderlaufen — und
|
|
/// zwar unbemerkt, weil keine von beiden dabei falsch aussieht.
|
|
/// </remarks>
|
|
public bool InProgress { get; set; }
|
|
|
|
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
|
}
|