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
@@ -0,0 +1,48 @@
namespace Flaro.Core.Delivery;
/// <summary>Wie der Transcoder zwischen Bild und Tempo abwägt.</summary>
///
/// <remarks>
/// <para>
/// <b>Drei Stufen, keine freie Zahl</b> — wie bei
/// <see cref="Stromentscheid.Stufen"/>. „crf 19" sagt niemandem etwas, der
/// nicht ffmpeg kennt; „Qualität" und „Tempo" sagen genau das, worum es geht.
/// </para>
/// <para>
/// <b>Und es ist eine echte Abwägung.</b> Was in Echtzeit laufen muss, darf
/// nicht langsamer rechnen als es abspielt. Auf einer schwachen Maschine kann
/// „Qualität" dazu führen, dass das Bild stockt — das ist kein Fehler, das ist
/// die Stufe. Deshalb steht der Satz auch in der Oberfläche.
/// </para>
/// </remarks>
public enum Kodierstufe
{
/// <summary>Schöneres Bild, langsamere Rechnung.</summary>
Qualitaet,
/// <summary>Was Flaro immer getan hat.</summary>
Ausgewogen,
/// <summary>Startet schneller, kostet Bild.</summary>
Tempo,
}
/// <summary>Hin und zurück zwischen Stufe und gespeichertem Wort.</summary>
public static class Kodierstufen
{
/// <summary>Unbekanntes wird ausgewogen — das war schon immer das Verhalten.</summary>
public static Kodierstufe Lesen(string? wert) => wert?.Trim().ToLowerInvariant() switch
{
"qualitaet" or "qualität" => Kodierstufe.Qualitaet,
"tempo" => Kodierstufe.Tempo,
_ => Kodierstufe.Ausgewogen,
};
/// <summary>Wie die Stufe in der Datenbank steht.</summary>
public static string AlsWort(this Kodierstufe stufe) => stufe switch
{
Kodierstufe.Qualitaet => "qualitaet",
Kodierstufe.Tempo => "tempo",
_ => "ausgewogen",
};
}
+17 -18
View File
@@ -25,23 +25,15 @@ namespace Flaro.Core.Playback;
/// eine Stelle, an der man weitermachen kann, schliessen einander nicht aus —
/// wer einen Film ein zweites Mal anfängt, soll beides haben.
/// </para>
/// <para>
/// <b>Was einstellbar ist, steht in <see cref="Wiedergaberegeln"/>.</b> Es
/// gibt inzwischen eine Untergrenze fürs Weiterschauen — aber sie entscheidet
/// nur, ob der Titel in der Reihe steht, nicht ob die Stelle überlebt. Das
/// bleibt der Unterschied.
/// </para>
/// </remarks>
public static class ProgressRule
{
/// <summary>Ab welchem Anteil ein Titel als gesehen gilt.</summary>
///
/// <remarks>
/// <b>Eine Schwelle ist hier unvermeidlich</b>, und das ist kein
/// Widerspruch zum Obigen: Niemand sieht einen Film bis zur letzten
/// Sekunde des Abspanns. Ohne diese Regel stünde jeder zu Ende geschaute
/// Film für immer bei 98 % in „Weiterschauen".
///
/// Der Unterschied zu Jellyfin ist nicht, <i>dass</i> es diese Grenze
/// gibt, sondern was sie anrichtet: hier wird ein Häkchen gesetzt, dort
/// zusätzlich die Position weggeworfen.
/// </remarks>
public const double WatchedFraction = 0.90;
/// <summary>Was aus einer Meldung folgt.</summary>
/// <param name="Position">Die Stelle, die abgelegt wird — nie null gesetzt.</param>
/// <param name="Watched">Ob der Titel damit als gesehen gilt.</param>
@@ -57,7 +49,8 @@ public static class ProgressRule
/// Laufzeit fehlt, ist der Fehler, den man nicht zurücknehmen kann, ohne
/// es zu merken.
/// </param>
public static Outcome Apply(TimeSpan position, TimeSpan? runtime)
/// <param name="regeln">Ab welchem Anteil ein Titel als gesehen gilt.</param>
public static Outcome Apply(TimeSpan position, TimeSpan? runtime, Wiedergaberegeln regeln)
{
// Eine negative Stelle gibt es nicht, und eine jenseits des Endes
// auch nicht. Beides kommt vor: der eine Player meldet beim Spulen
@@ -69,7 +62,7 @@ public static class ProgressRule
if (stelle > laenge) stelle = laenge;
return new Outcome(stelle, Watched: stelle >= laenge * WatchedFraction);
return new Outcome(stelle, Watched: stelle >= laenge * regeln.GesehenAnteil);
}
/// <summary>Ob ein Zustand in „Weiterschauen" gehört.</summary>
@@ -80,9 +73,15 @@ public static class ProgressRule
/// aber die Prüfung kann beides gegeneinanderhalten. Läuft das auseinander,
/// fällt es hier auf und nicht beim Benutzer.
/// </remarks>
public static bool BelongsInContinueWatching(UserItemState zustand) =>
public static bool BelongsInContinueWatching(UserItemState zustand, Wiedergaberegeln regeln) =>
zustand is { Dismissed: false, Position.Ticks: > 0 }
// **Die Untergrenze ist einstellbar und war es nicht.** Ein Tick
// genügte: wer im Aufmacher eine Sekunde hineinschaute, fand den
// Titel danach in „Weiterschauen". Was oben über Jellyfin steht,
// gilt weiter — die Stelle wird nicht weggeworfen, der Titel steht
// nur nicht in der Reihe.
&& zustand.Position >= regeln.FortsetzenAb
&& (zustand.Runtime is not { } laenge
|| laenge <= TimeSpan.Zero
|| zustand.Position < laenge * WatchedFraction);
|| zustand.Position < laenge * regeln.GesehenAnteil);
}
@@ -0,0 +1,47 @@
namespace Flaro.Core.Playback;
/// <summary>Wann ein Titel angefangen ist und wann er als gesehen gilt.</summary>
///
/// <remarks>
/// <para>
/// <b>Zwei Zahlen, die niemand raten kann.</b> Wie lange man hineinschauen
/// muss, damit es zählt, und ab welchem Anteil ein Film durch ist — das hängt
/// davon ab, wie jemand schaut, nicht davon, was technisch richtig wäre. Beide
/// standen als Konstante im Quelltext; jetzt stehen sie in den Einstellungen.
/// </para>
/// <para>
/// <b>Als Wert, nicht als Abfrage.</b> <see cref="ProgressRule"/> bleibt damit
/// ein reiner Typ: er rechnet, er liest nichts. Prüfbar ohne Datenbank, und
/// derselbe Satz Regeln gilt für eine Meldung und für die Neubewertung aller
/// vorhandenen Zeilen.
/// </para>
/// </remarks>
///
/// <param name="FortsetzenAb">
/// Wie weit man gekommen sein muss, damit ein Titel in „Weiterschauen" steht.
/// <see cref="TimeSpan.Zero"/> heisst: jede angefangene Sekunde zählt.
/// </param>
/// <param name="GesehenAnteil">Ab welchem Anteil der Laufzeit ein Titel gesehen ist.</param>
public readonly record struct Wiedergaberegeln(TimeSpan FortsetzenAb, double GesehenAnteil)
{
/// <summary>Was gilt, solange niemand etwas anderes eingestellt hat.</summary>
///
/// <remarks>
/// <b>Neunzig Sekunden, nicht null.</b> Ohne Untergrenze genügte ein
/// einziger Tick: wer im Aufmacher eine Sekunde hineinschaute, fand den
/// Titel danach in „Weiterschauen" wieder. Eineinhalb Minuten sind lang
/// genug, dass niemand aus Versehen hineinfällt, und kurz genug, dass eine
/// abgebrochene Folge noch zählt.
/// </remarks>
public static readonly Wiedergaberegeln Vorgabe = new(TimeSpan.FromSeconds(90), 0.90);
/// <summary>Dieselben Regeln mit geprüften Werten.</summary>
///
/// <remarks>
/// Was aus der Datenbank kommt, hat dort irgendwann jemand hineingeschrieben.
/// Ein Anteil von 0 hiesse „alles ist gesehen", einer von 2 „nichts".
/// </remarks>
public Wiedergaberegeln Geprueft() => new(
FortsetzenAb < TimeSpan.Zero ? TimeSpan.Zero : FortsetzenAb,
Math.Clamp(GesehenAnteil, 0.5, 1.0));
}