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); } }