using Flaro.Core.Playback;
namespace Flaro.Core.Tests;
/// Die Regel, um derentwillen dieser Server existiert.
[TestFixture]
public sealed class ProgressRuleTests
{
private static readonly TimeSpan Zweistünder = TimeSpan.FromHours(2);
/// Die Vorgabe des Servers — was gilt, wenn niemand etwas einstellt.
private static readonly Wiedergaberegeln Regeln = Wiedergaberegeln.Vorgabe;
/// Kurzschreibweise, damit die Prüfungen lesbar bleiben.
private static bool BelongsInContinueWatching(UserItemState zustand) =>
ProgressRule.BelongsInContinueWatching(zustand, Regeln);
/// Der Fall, der den Ausschlag gab.
///
///
/// Jellyfins MinResumePct 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.
///
[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)));
}
/// Jellyfins MinResumeDurationSeconds steht auf 300.
///
///
/// Dort gilt eine vierminütige Folge nach dreissig Sekunden als
/// vollständig gesehen. Hier ist sie das, was sie ist: angefangen.
///
[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);
}
/// Und die Stelle bleibt trotzdem stehen.
///
///
/// Jellyfin setzt sie an dieser Stelle auf null. Wer den Abspann sehen
/// oder die letzte Szene nochmal will, fängt dort von vorn an.
///
[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);
}
/// Ohne Laufzeit gibt es keinen Anteil — also kein Urteil.
///
///
/// 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.
///
[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);
}
/// Spieler melden beim Spulen und beim Beenden Unsinn.
[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);
}
/// Ein zweites Mal angefangen — dann steht er wieder da.
///
///
/// Das Häkchen bleibt: gesehen hat man ihn ja. Beides nebeneinander ist
/// genau das, was ohne weggeworfene Position möglich wird.
///
[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 ---------- */
/// Hineingeschaut ist noch nicht angefangen.
///
///
/// 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.
///
[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);
}
/// „Sofort" ist eine gültige Einstellung — dann zählt jede Sekunde.
[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);
}
/// Und der Anteil wirkt auch.
[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");
});
}
/// Unsinn aus der Datenbank wird eingefangen, nicht übernommen.
[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 };
/// Ein erkannter Abspann zieht das Häkchen nach vorn.
///
///
/// Der Fall aus dem Gebrauch. 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.
///
[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");
});
}
/// Eine Marke hinter der Schwelle verschiebt nichts nach hinten.
///
///
/// 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.
///
[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);
}
/// Ohne Marke bleibt alles, wie es war.
[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);
}
}