Fünf Aufmacher je Titel, und der Abspann zählt als Schluss

**Der Aufmacher darf wechseln.** Bisher hing hinter jedem Titel genau ein
Hintergrundbild, und das Karussell war nach einer Woche ein Standbild.
Jetzt lassen sich bis zu fünf Aufnahmen aussuchen — im Bilderdialog unter
einem vierten Reiter, gespeist aus denselben Hintergründen.

Welches gerade gilt, entscheidet der Server aus Uhrzeit und Kennung: alle
fünf Stunden ein anderes, überall dasselbe — im Web, in der App, nach
einem Neustart. Ein Zufall je Abruf hiesse, dass das Bild beim Blättern
springt; eine gespeicherte Wahl hiesse einen Schreibvorgang je Titel und
Fenster. Die Kennung geht mit in die Rechnung, damit nicht alle Titel im
selben Augenblick umschalten.

Plakat und Hintergrund bleiben unberührt. Sie stehen hinter Kacheln und
Kopfzeilen, und dort soll nichts wechseln.

**Und der Abspann zählt als Schluss.** Eine Serie hängt sechs Minuten
Abspann an eine Folge von fünfundvierzig; neunzig Prozent liegen dann
mittendrin, und die Folge gilt erst als gesehen, wenn man ihn sich ganz
ansieht. Die Schwelle herunterzudrehen hiesse, jeden anderen Titel zu
früh abzuhaken. Der erkannte Abspann ist die genauere Antwort auf
dieselbe Frage — er zieht die Schwelle nach vorn, nie nach hinten.

Dazu der Verlauf über dem Aufmacher: er war immer noch zu dunkel. Jetzt
trägt der Schattenwurf der Schrift, was vorher eine Decke über dem
ganzen Bild tat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-12 21:44:43 +02:00
co-authored by Claude Opus 5
parent 8cf2118723
commit 1d70be0f99
20 changed files with 1956 additions and 30 deletions
@@ -302,6 +302,24 @@ public sealed class Movie : Flaro.Core.Metadata.IBebildert
/// </remarks>
public string? LogoImage { get; set; }
/// <summary>Weitere Bilder für den Aufmacher, durch Komma getrennt.</summary>
///
/// <remarks>
/// <para>
/// <b>Warum nicht eine Tabelle.</b> Es sind höchstens fünf
/// Zwischenspeicherschlüssel je Titel, sie werden immer zusammen
/// gelesen und immer zusammen gesetzt. Eine Tabelle dafür wäre eine
/// Verknüpfung, ein Löschverhalten und eine Reihenfolgespalte — für
/// eine Liste, die in eine Zeile passt.
/// </para>
/// <para>
/// <b>Getrennt vom Hintergrund.</b> Der bleibt, was er ist: das eine
/// Bild hinter Kacheln und Kopfzeilen. Hier stehen die grossen
/// Aufnahmen für das Karussell, und nur die wechseln.
/// </para>
/// </remarks>
public string? Aufmacherbilder { get; set; }
public DateTimeOffset AddedAt { get; init; } = DateTimeOffset.UtcNow;
}
@@ -497,6 +515,24 @@ public sealed class Series : Flaro.Core.Metadata.IBebildert
/// </remarks>
public string? LogoImage { get; set; }
/// <summary>Weitere Bilder für den Aufmacher, durch Komma getrennt.</summary>
///
/// <remarks>
/// <para>
/// <b>Warum nicht eine Tabelle.</b> Es sind höchstens fünf
/// Zwischenspeicherschlüssel je Titel, sie werden immer zusammen
/// gelesen und immer zusammen gesetzt. Eine Tabelle dafür wäre eine
/// Verknüpfung, ein Löschverhalten und eine Reihenfolgespalte — für
/// eine Liste, die in eine Zeile passt.
/// </para>
/// <para>
/// <b>Getrennt vom Hintergrund.</b> Der bleibt, was er ist: das eine
/// Bild hinter Kacheln und Kopfzeilen. Hier stehen die grossen
/// Aufnahmen für das Karussell, und nur die wechseln.
/// </para>
/// </remarks>
public string? Aufmacherbilder { get; set; }
public DateTimeOffset AddedAt { get; init; } = DateTimeOffset.UtcNow;
}
@@ -0,0 +1,75 @@
namespace Flaro.Core.Media;
/// <summary>Welches der gewählten Aufmacherbilder gerade an der Reihe ist.</summary>
///
/// <remarks>
/// <para>
/// <b>Warum es wechselt.</b> Ein Karussell, das jedes Mal dieselben fünf
/// Bilder zeigt, ist nach einer Woche ein Standbild. Wer zu einem Titel
/// mehrere Aufnahmen ausgesucht hat, soll sie auch zu sehen bekommen.
/// </para>
/// <para>
/// <b>Aus der Uhr gerechnet, nicht gewürfelt und nicht gespeichert.</b> Ein
/// Zufall je Abruf hiesse, dass das Bild beim Blättern springt und beim
/// Zurückkommen ein drittes ist. Eine gespeicherte Wahl hiesse, einen
/// Schreibvorgang je Titel und Fenster. Aus Zeit und Kennung gerechnet ist
/// beides nicht nötig: innerhalb eines Fensters kommt überall dieselbe
/// Antwort heraus — im Web, in der App, nach einem Neustart.
/// </para>
/// <para>
/// <b>Die Kennung geht mit in die Rechnung</b>, damit nicht alle Titel im
/// selben Augenblick umschalten. Sonst sähe eine Startseite alle fünf
/// Stunden vollständig anders aus statt nach und nach.
/// </para>
/// </remarks>
public static class Aufmacherwahl
{
/// <summary>Wie lange ein Bild stehen bleibt.</summary>
public static readonly TimeSpan Fenster = TimeSpan.FromHours(5);
/// <summary>Wie viele Bilder je Titel höchstens gewählt werden dürfen.</summary>
///
/// <remarks>
/// Fünf. Mehr hiesse, dass ein einzelnes kaum noch vorkommt: bei zehn
/// Bildern und fünf Stunden Fenster sähe man dasselbe zweimal im Monat.
/// </remarks>
public const int Hoechstens = 5;
/// <summary>Zerlegt die gespeicherte Zeile in die einzelnen Schlüssel.</summary>
public static IReadOnlyList<string> Lesen(string? gespeichert) =>
string.IsNullOrWhiteSpace(gespeichert)
? []
: gespeichert.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
/// <summary>Setzt die Zeile zusammen — gekürzt und ohne Doppelte.</summary>
public static string? Schreiben(IEnumerable<string> schluessel)
{
var sauber = schluessel
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim())
// Ein Komma im Schlüssel zerlegte die Zeile beim Lesen in zwei.
.Where(s => !s.Contains(','))
.Distinct(StringComparer.Ordinal)
.Take(Hoechstens)
.ToList();
return sauber.Count == 0 ? null : string.Join(',', sauber);
}
/// <summary>Das Bild, das jetzt gilt — oder <c>null</c>, wenn keines gewählt ist.</summary>
public static string? Jetziges(string? gespeichert, Guid titel, DateTimeOffset jetzt)
{
var bilder = Lesen(gespeichert);
if (bilder.Count == 0) return null;
if (bilder.Count == 1) return bilder[0];
var fenster = jetzt.ToUnixTimeSeconds() / (long)Fenster.TotalSeconds;
// Die ersten vier Bytes der Kennung als Versatz. **Nicht
// `GetHashCode`**: der ist je Programmlauf anders gestreut, und das
// Bild spränge bei jedem Neustart des Servers.
var versatz = BitConverter.ToUInt32(titel.ToByteArray(), 0);
var stelle = (long)((ulong)fenster + versatz) % bilder.Count;
return bilder[(int)stelle];
}
}
+23 -2
View File
@@ -50,7 +50,21 @@ public static class ProgressRule
/// es zu merken.
/// </param>
/// <param name="regeln">Ab welchem Anteil ein Titel als gesehen gilt.</param>
public static Outcome Apply(TimeSpan position, TimeSpan? runtime, Wiedergaberegeln regeln)
/// <param name="abspannAb">
/// Wann der Abspann anfängt, falls er erkannt wurde.
///
/// <b>Dann zählt er, nicht der Anteil.</b> Manche Serie hängt sechs
/// Minuten Abspann an eine Folge von fünfundvierzig; neunzig Prozent
/// liegen dann mitten im Abspann, und die Folge gilt erst als gesehen,
/// wenn man ihn sich ganz ansieht. Die Schwelle dafür herunterzudrehen
/// hiesse, jeden anderen Titel zu früh abzuhaken. Der Abspann ist die
/// genauere Antwort auf dieselbe Frage: hier ist die Handlung vorbei.
///
/// Der Anteil bleibt daneben stehen und gilt weiter — wer über ihn
/// hinauskommt, ohne dass ein Abspann erkannt wäre, ist genauso fertig.
/// </param>
public static Outcome Apply(
TimeSpan position, TimeSpan? runtime, Wiedergaberegeln regeln, TimeSpan? abspannAb = null)
{
// Eine negative Stelle gibt es nicht, und eine jenseits des Endes
// auch nicht. Beides kommt vor: der eine Player meldet beim Spulen
@@ -62,7 +76,14 @@ public static class ProgressRule
if (stelle > laenge) stelle = laenge;
return new Outcome(stelle, Watched: stelle >= laenge * regeln.GesehenAnteil);
var schwelle = laenge * regeln.GesehenAnteil;
// Der Abspann zieht die Schwelle nach vorn, nie nach hinten: eine
// falsch erkannte Marke bei Minute vierzig darf nicht dazu führen,
// dass eine Folge erst nach der Marke als gesehen gilt.
if (abspannAb is { } ab && ab > TimeSpan.Zero && ab < schwelle) schwelle = ab;
return new Outcome(stelle, Watched: stelle >= schwelle);
}
/// <summary>Ob ein Zustand in „Weiterschauen" gehört.</summary>