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>
This commit is contained in:
Scarriffle
2026-10-06 16:52:53 +02:00
co-authored by Claude Opus 5
parent 1e4a231d1b
commit 9622da0284
19 changed files with 820 additions and 130 deletions
@@ -8,6 +8,13 @@ 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>
@@ -18,7 +25,7 @@ public sealed class ProgressRuleTests
[Test]
public void Die_ersten_Minuten_zaehlen_auch()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(5), Zweistünder);
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(5), Zweistünder, Regeln);
Assert.Multiple(() =>
{
@@ -30,7 +37,7 @@ public sealed class ProgressRuleTests
[Test]
public void Eine_einzige_Sekunde_zaehlt_auch()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(1), Zweistünder);
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(1), Zweistünder, Regeln);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.FromSeconds(1)));
}
@@ -44,7 +51,7 @@ public sealed class ProgressRuleTests
[Test]
public void Ein_kurzer_Titel_gilt_nicht_sofort_als_gesehen()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(4));
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(4), Regeln);
Assert.Multiple(() =>
{
@@ -56,7 +63,7 @@ public sealed class ProgressRuleTests
[Test]
public void Kurz_vor_Schluss_gilt_er_als_gesehen()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(115), Zweistünder);
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(115), Zweistünder, Regeln);
Assert.That(ergebnis.Watched, Is.True);
}
@@ -70,7 +77,7 @@ public sealed class ProgressRuleTests
[Test]
public void Gesehen_wirft_die_Stelle_nicht_weg()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(118), Zweistünder);
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(118), Zweistünder, Regeln);
Assert.Multiple(() =>
{
@@ -82,7 +89,7 @@ public sealed class ProgressRuleTests
[Test]
public void Genau_an_der_Grenze_gilt_er_als_gesehen()
{
var ergebnis = ProgressRule.Apply(Zweistünder * ProgressRule.WatchedFraction, Zweistünder);
var ergebnis = ProgressRule.Apply(Zweistünder * Regeln.GesehenAnteil, Zweistünder, Regeln);
Assert.That(ergebnis.Watched, Is.True);
}
@@ -91,7 +98,7 @@ public sealed class ProgressRuleTests
public void Eine_Sekunde_davor_noch_nicht()
{
var ergebnis = ProgressRule.Apply(
Zweistünder * ProgressRule.WatchedFraction - TimeSpan.FromSeconds(1), Zweistünder);
Zweistünder * Regeln.GesehenAnteil - TimeSpan.FromSeconds(1), Zweistünder, Regeln);
Assert.That(ergebnis.Watched, Is.False);
}
@@ -106,7 +113,7 @@ public sealed class ProgressRuleTests
[Test]
public void Ohne_Laufzeit_wird_nichts_vermutet()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(90), null);
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(90), null, Regeln);
Assert.Multiple(() =>
{
@@ -118,7 +125,7 @@ public sealed class ProgressRuleTests
[Test]
public void Eine_Laufzeit_von_null_ist_keine()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(3), TimeSpan.Zero);
var ergebnis = ProgressRule.Apply(TimeSpan.FromMinutes(3), TimeSpan.Zero, Regeln);
Assert.That(ergebnis.Watched, Is.False);
}
@@ -127,7 +134,7 @@ public sealed class ProgressRuleTests
[Test]
public void Eine_negative_Stelle_wird_zu_null()
{
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(-5), Zweistünder);
var ergebnis = ProgressRule.Apply(TimeSpan.FromSeconds(-5), Zweistünder, Regeln);
Assert.That(ergebnis.Position, Is.EqualTo(TimeSpan.Zero));
}
@@ -135,7 +142,7 @@ public sealed class ProgressRuleTests
[Test]
public void Jenseits_des_Endes_wird_auf_das_Ende_gekuerzt()
{
var ergebnis = ProgressRule.Apply(Zweistünder + TimeSpan.FromSeconds(3), Zweistünder);
var ergebnis = ProgressRule.Apply(Zweistünder + TimeSpan.FromSeconds(3), Zweistünder, Regeln);
Assert.Multiple(() =>
{
@@ -149,21 +156,21 @@ public sealed class ProgressRuleTests
[Test]
public void Angefangen_gehoert_in_Weiterschauen()
{
Assert.That(ProgressRule.BelongsInContinueWatching(
Assert.That(BelongsInContinueWatching(
Zustand(TimeSpan.FromMinutes(20), Zweistünder)), Is.True);
}
[Test]
public void Nie_angefangen_gehoert_nicht_hinein()
{
Assert.That(ProgressRule.BelongsInContinueWatching(
Assert.That(BelongsInContinueWatching(
Zustand(TimeSpan.Zero, Zweistünder)), Is.False);
}
[Test]
public void Durchgeschaut_gehoert_nicht_hinein()
{
Assert.That(ProgressRule.BelongsInContinueWatching(
Assert.That(BelongsInContinueWatching(
Zustand(TimeSpan.FromMinutes(118), Zweistünder)), Is.False);
}
@@ -173,7 +180,7 @@ public sealed class ProgressRuleTests
var zustand = Zustand(TimeSpan.FromMinutes(20), Zweistünder);
zustand.Dismissed = true;
Assert.That(ProgressRule.BelongsInContinueWatching(zustand), Is.False);
Assert.That(BelongsInContinueWatching(zustand), Is.False);
}
/// <summary>Ein zweites Mal angefangen — dann steht er wieder da.</summary>
@@ -188,7 +195,75 @@ public sealed class ProgressRuleTests
var zustand = Zustand(TimeSpan.FromMinutes(10), Zweistünder);
zustand.Watched = true;
Assert.That(ProgressRule.BelongsInContinueWatching(zustand), Is.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) =>