Files
Flaro/server/tests/Flaro.Core.Tests/ProgressRuleTests.cs
T
ScarriffleandClaude Opus 5 1d70be0f99 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>
2026-10-12 21:44:43 +02:00

328 lines
11 KiB
C#

using Flaro.Core.Playback;
namespace Flaro.Core.Tests;
/// <summary>Die Regel, um derentwillen dieser Server existiert.</summary>
[TestFixture]
public sealed class ProgressRuleTests
{
private static readonly TimeSpan Zweistünder = TimeSpan.FromHours(2);
/// <summary>Die Vorgabe des Servers — was gilt, wenn niemand etwas einstellt.</summary>
private static readonly Wiedergaberegeln Regeln = Wiedergaberegeln.Vorgabe;
/// <summary>Kurzschreibweise, damit die Prüfungen lesbar bleiben.</summary>
private static bool BelongsInContinueWatching(UserItemState zustand) =>
ProgressRule.BelongsInContinueWatching(zustand, Regeln);
/// <summary>Der Fall, der den Ausschlag gab.</summary>
///
/// <remarks>
/// Jellyfins <c>MinResumePct</c> steht auf 5. Bei zwei Stunden sind das
/// sechs Minuten, und wer nach fünf abbricht, findet beim nächsten Mal
/// nichts vor. Hier ist genau diese Stelle abgelegt.
/// </remarks>
[Test]
public void Die_ersten_Minuten_zaehlen_auch()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(5), Zweistünder, Regeln);
Assert.Multiple(() =>
{
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.FromMinutes(5)));
Assert.That(ergebnis.Watched, Is.False);
});
}
[Test]
public void Eine_einzige_Sekunde_zaehlt_auch()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(1), Zweistünder, Regeln);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.FromSeconds(1)));
}
/// <summary>Jellyfins <c>MinResumeDurationSeconds</c> steht auf 300.</summary>
///
/// <remarks>
/// Dort gilt eine vierminütige Folge nach dreissig Sekunden als
/// vollständig gesehen. Hier ist sie das, was sie ist: angefangen.
/// </remarks>
[Test]
public void Ein_kurzer_Titel_gilt_nicht_sofort_als_gesehen()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(4), Regeln);
Assert.Multiple(() =>
{
Assert.That(ergebnis.Watched, Is.False);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.FromSeconds(30)));
});
}
[Test]
public void Kurz_vor_Schluss_gilt_er_als_gesehen()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(115), Zweistünder, Regeln);
Assert.That(ergebnis.Watched, Is.True);
}
/// <summary>Und die Stelle bleibt trotzdem stehen.</summary>
///
/// <remarks>
/// Jellyfin setzt sie an dieser Stelle auf null. Wer den Abspann sehen
/// oder die letzte Szene nochmal will, fängt dort von vorn an.
/// </remarks>
[Test]
public void Gesehen_wirft_die_Stelle_nicht_weg()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(118), Zweistünder, Regeln);
Assert.Multiple(() =>
{
Assert.That(ergebnis.Watched, Is.True);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.FromMinutes(118)));
});
}
[Test]
public void Genau_an_der_Grenze_gilt_er_als_gesehen()
{
var ergebnis = ProgressRule.Apply(Zweistünder * Regeln.GesehenAnteil, Zweistünder, Regeln);
Assert.That(ergebnis.Watched, Is.True);
}
[Test]
public void Eine_Sekunde_davor_noch_nicht()
{
var ergebnis = ProgressRule.Apply(
Zweistünder * Regeln.GesehenAnteil - TimeSpan.FromSeconds(1), Zweistünder, Regeln);
Assert.That(ergebnis.Watched, Is.False);
}
/// <summary>Ohne Laufzeit gibt es keinen Anteil — also kein Urteil.</summary>
///
/// <remarks>
/// Jellyfin nimmt in diesem Fall an, der Titel sei vollständig gesehen
/// („we'll just have to assume it was fully played"). Das ist die
/// Annahme, die man nicht zurücknehmen kann, ohne sie zu bemerken.
/// </remarks>
[Test]
public void Ohne_Laufzeit_wird_nichts_vermutet()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(90), null, Regeln);
Assert.Multiple(() =>
{
Assert.That(ergebnis.Watched, Is.False);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.FromMinutes(90)));
});
}
[Test]
public void Eine_Laufzeit_von_null_ist_keine()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(3), TimeSpan.Zero, Regeln);
Assert.That(ergebnis.Watched, Is.False);
}
/// <summary>Spieler melden beim Spulen und beim Beenden Unsinn.</summary>
[Test]
public void Eine_negative_Stelle_wird_zu_null()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(-5), Zweistünder, Regeln);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.Zero));
}
[Test]
public void Jenseits_des_Endes_wird_auf_das_Ende_gekuerzt()
{
var ergebnis = ProgressRule.Apply(Zweistünder + TimeSpan.FromSeconds(3), Zweistünder, Regeln);
Assert.Multiple(() =>
{
Assert.That(ergebnis.Position, Is.EqualTo(Zweistünder));
Assert.That(ergebnis.Watched, Is.True);
});
}
// --- Weiterschauen ---------------------------------------------------
[Test]
public void Angefangen_gehoert_in_Weiterschauen()
{
Assert.That(BelongsInContinueWatching(
Zustand(TimeSpan.FromMinutes(20), Zweistünder)), Is.True);
}
[Test]
public void Nie_angefangen_gehoert_nicht_hinein()
{
Assert.That(BelongsInContinueWatching(
Zustand(TimeSpan.Zero, Zweistünder)), Is.False);
}
[Test]
public void Durchgeschaut_gehoert_nicht_hinein()
{
Assert.That(BelongsInContinueWatching(
Zustand(TimeSpan.FromMinutes(118), Zweistünder)), Is.False);
}
[Test]
public void Weggelegtes_gehoert_nicht_hinein()
{
var zustand = Zustand(TimeSpan.FromMinutes(20), Zweistünder);
zustand.Dismissed = true;
Assert.That(BelongsInContinueWatching(zustand), Is.False);
}
/// <summary>Ein zweites Mal angefangen — dann steht er wieder da.</summary>
///
/// <remarks>
/// Das Häkchen bleibt: gesehen hat man ihn ja. Beides nebeneinander ist
/// genau das, was ohne weggeworfene Position möglich wird.
/// </remarks>
[Test]
public void Ein_gesehener_Titel_kommt_beim_zweiten_Mal_zurueck()
{
var zustand = Zustand(TimeSpan.FromMinutes(10), Zweistünder);
zustand.Watched = true;
Assert.That(BelongsInContinueWatching(zustand), Is.True);
}
/* ---------- Die einstellbare Untergrenze ---------- */
/// <summary>Hineingeschaut ist noch nicht angefangen.</summary>
///
/// <remarks>
/// Der Anlass: eine Sekunde im Aufmacher, und der Titel stand danach in
/// „Weiterschauen". Die Stelle wird trotzdem abgelegt — nur die Reihe
/// bleibt frei. Das ist der Unterschied zu Jellyfin, der oben steht.
/// </remarks>
[Test]
public void Unter_der_Schwelle_gehoert_er_nicht_hinein()
{
var zustand = Zustand(TimeSpan.FromSeconds(1), Zweistünder);
Assert.Multiple(() =>
{
Assert.That(ProgressRule.BelongsInContinueWatching(zustand, Regeln), Is.False);
Assert.That(zustand.Position, Is.EqualTo(TimeSpan.FromSeconds(1)),
"die Stelle bleibt — weggeworfen wird sie nie");
});
}
[Test]
public void Genau_auf_der_Schwelle_gehoert_er_hinein()
{
Assert.That(ProgressRule.BelongsInContinueWatching(
Zustand(Regeln.FortsetzenAb, Zweistünder), Regeln), Is.True);
}
/// <summary>„Sofort" ist eine gültige Einstellung — dann zählt jede Sekunde.</summary>
[Test]
public void Ohne_Schwelle_zaehlt_die_erste_Sekunde()
{
var ohne = Regeln with { FortsetzenAb = TimeSpan.Zero };
Assert.That(ProgressRule.BelongsInContinueWatching(
Zustand(TimeSpan.FromSeconds(1), Zweistünder), ohne), Is.True);
}
/// <summary>Und der Anteil wirkt auch.</summary>
[Test]
public void Ein_hoeherer_Anteil_laesst_ihn_laenger_stehen()
{
var zustand = Zustand(TimeSpan.FromMinutes(112), Zweistünder); // 93,3 %
Assert.Multiple(() =>
{
Assert.That(ProgressRule.BelongsInContinueWatching(zustand, Regeln), Is.False,
"bei 90 % ist er durch");
Assert.That(ProgressRule.BelongsInContinueWatching(
zustand, Regeln with { GesehenAnteil = 0.95 }), Is.True,
"bei 95 % noch nicht");
});
}
/// <summary>Unsinn aus der Datenbank wird eingefangen, nicht übernommen.</summary>
[Test]
public void Geprueft_bindet_den_Anteil_ein()
{
Assert.Multiple(() =>
{
Assert.That(new Wiedergaberegeln(TimeSpan.FromSeconds(-5), 0).Geprueft(),
Is.EqualTo(new Wiedergaberegeln(TimeSpan.Zero, 0.5)));
Assert.That(new Wiedergaberegeln(TimeSpan.Zero, 7).Geprueft().GesehenAnteil,
Is.EqualTo(1.0));
});
}
private static UserItemState Zustand(TimeSpan stelle, TimeSpan? laenge) =>
new() { UserId = Guid.CreateVersion7(), MovieId = Guid.CreateVersion7(), Position = stelle, Runtime = laenge };
/// <summary>Ein erkannter Abspann zieht das Häkchen nach vorn.</summary>
///
/// <remarks>
/// <b>Der Fall aus dem Gebrauch.</b> Eine 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.
/// </remarks>
[Test]
public void Der_Abspann_zaehlt_als_Schluss()
{
var laenge = TimeSpan.FromMinutes(45);
var abspann = TimeSpan.FromMinutes(39);
var regeln = new Wiedergaberegeln { GesehenAnteil = 0.9 };
var amAbspann = ProgressRule.Apply(abspann, laenge, regeln, abspann);
var davor = ProgressRule.Apply(abspann - TimeSpan.FromSeconds(30), laenge, regeln, abspann);
Assert.Multiple(() =>
{
Assert.That(amAbspann.Watched, Is.True, "ab dem Abspann ist die Handlung vorbei");
Assert.That(davor.Watched, Is.False, "eine halbe Minute davor noch nicht");
});
}
/// <summary>Eine Marke hinter der Schwelle verschiebt nichts nach hinten.</summary>
///
/// <remarks>
/// Sonst machte eine falsch erkannte Marke aus einem Titel, der bei
/// neunzig Prozent fertig war, einen, der es erst bei fünfundneunzig
/// ist — und niemand sähe, warum.
/// </remarks>
[Test]
public void Eine_spaete_Marke_schiebt_nichts_nach_hinten()
{
var laenge = TimeSpan.FromMinutes(45);
var regeln = new Wiedergaberegeln { GesehenAnteil = 0.9 };
var ergebnis = ProgressRule.Apply(
TimeSpan.FromMinutes(41), laenge, regeln, abspannAb: TimeSpan.FromMinutes(44));
Assert.That(ergebnis.Watched, Is.True);
}
/// <summary>Ohne Marke bleibt alles, wie es war.</summary>
[Test]
public void Ohne_Abspann_gilt_der_Anteil()
{
var laenge = TimeSpan.FromMinutes(45);
var regeln = new Wiedergaberegeln { GesehenAnteil = 0.9 };
Assert.That(ProgressRule.Apply(TimeSpan.FromMinutes(39), laenge, regeln).Watched, Is.False);
Assert.That(ProgressRule.Apply(TimeSpan.FromMinutes(41), laenge, regeln).Watched, Is.True);
}
}