Files
Flaro/server/tests/Flaro.Core.Tests/ProgressRuleTests.cs
T
ScarriffleandClaude Opus 5 9622da0284 Eine Sekunde ist kein Anfang
Drei Zahlen standen als Konstante im Quelltext und bestimmten, wie sich
der Server anfühlt. Jetzt stehen sie in der Verwaltung, unter
„Wiedergabe".

**Weiterschauen ab.** Gemessen: `BelongsInContinueWatching` verlangte nur
`Position.Ticks > 0` — ein Tick genügte. Wer im Aufmacher eine Sekunde
hineinschaute, fand den Titel danach in „Fortsetzen". Jetzt gibt es eine
Untergrenze, voreingestellt auf 1½ Minuten.

Was sich dabei **nicht** ändert: die Stelle wird weiter gemerkt. Das ist
der Unterschied zu Jellyfin, um dessentwillen es diesen Server gibt — die
Schwelle entscheidet, ob der Titel in der Reihe steht, nicht ob man ihn
wiederfindet. Nachgemessen: nach dem Umstellen ist „Heat" aus der Reihe
verschwunden und steht weiterhin bei Sekunde 1.

**Gesehen ab.** War 90 % als `const`, ist jetzt 85/90/95.

**Umkodieren.** `-preset veryfast -crf 21` und VAAPI `-qp 23` waren fest
verdrahtet, ohne Einstellung und ohne Umgebungsvariable. Drei Stufen:
Qualität (medium/19, qp 21), Ausgewogen (wie bisher), Tempo
(ultrafast/23, qp 26). Mit dem Warnsatz daneben, und der ist ernst
gemeint: was in Echtzeit laufen muss, darf nicht langsamer rechnen als es
abspielt.

**Und das Vorhandene wird neu bewertet.** `InProgress` ist eine
gespeicherte Spalte — die Bruchrechnung lässt sich nicht nach SQL
übersetzen. Eine neue Schwelle wirkte sonst nur auf künftige Meldungen.
Der Endpunkt läuft deshalb einmal in Stapeln durch und meldet zurück, wie
viele Zeilen das umgeworfen hat: „Übernommen — 2 Einträge wurden neu
bewertet." Ohne diesen Satz änderte sich die Startseite scheinbar von
selbst.

`ProgressRule` bleibt ein reiner Typ: die Regeln kommen als Wert herein.
Nebenbei zieht der doppelte `zeile()`-Bauer nach `teile.ts` — er stand
zweimal wortgleich da.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 16:52:53 +02:00

272 lines
9.1 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 };
}