Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe

„Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund"
für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für
das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und
das ist auch das Wort, in dem sich darüber reden lässt.

    Kratzplatz*       → Transcode*          (was es ist, steht in FLARO_TRANSCODE)
    Marke, Marken     → Segment, Segmente
    Markenerkennung   → Segmenterkennung
    Markenart/-suche  → Segmentart/-suche
    Tonabdruck        → Fingerprint         (Chromaprint nennt es so)
    Tonabgleich       → Fingerprintabgleich
    Schlüsselbild     → Keyframe
    Bildabtaster      → Frameabtaster

Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe,
Meldung, Abspann, Vorspann. Das war nie das Problem.

**Was sich nicht ändert, und zwar mit Absicht.** Die drei
Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind —
sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte
des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für
die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen
wäre Diff ohne Gewinn.

**Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst
erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine
verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre
jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg
gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile
anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1
und 1, nachher 1 und 1, Inhalt unverändert.

Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und
das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine
Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn
Stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-16 16:26:57 +02:00
co-authored by Claude Opus 5
parent 32f592b83e
commit ed13d5fa43
73 changed files with 2137 additions and 694 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ namespace Flaro.Core.Auth;
/// </para>
/// <para>
/// <b>Der Code allein holt nie ein Token ab.</b> Dafür gibt es den Abholschein,
/// den nur das anfragende Gerät kennt und der hier nur als Fingerabdruck
/// den nur das anfragende Gerät kennt und der hier nur als Fingerfingerprint
/// liegt. Ohne ihn könnte jemand sechsstellige Zahlen durchprobieren — eine
/// Million Versuche sind nichts, und am Ende stünde ein fremdes Gerät in der
/// Mediathek.
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Flaro.Core.Auth;
/// </para>
/// <para>
/// <b>Übernommen ist die Geheimnisdisziplin.</b> Wie bei
/// <see cref="DeviceToken"/> liegt hier nur der Fingerabdruck; den Link
/// <see cref="DeviceToken"/> liegt hier nur der Fingerfingerprint; den Link
/// selbst bekommt man genau einmal zu sehen, beim Erstellen. Wer die
/// Datenbank liest, kann daraus keine Einladung bauen.
/// </para>
+2 -2
View File
@@ -6,7 +6,7 @@ namespace Flaro.Core.Auth;
/// <summary>Erzeugt und verfingert Geheimnisse, die Clients vorzeigen.</summary>
///
/// <remarks>
/// <b>Abgelegt wird nur der Fingerabdruck, nie das Geheimnis.</b> Wer die
/// <b>Abgelegt wird nur der Fingerfingerprint, nie das Geheimnis.</b> Wer die
/// Datenbank erbeutet, hat damit Hashes und keine Zugänge. Dass das überhaupt
/// erwähnt werden muss: bei Medienservern liegen Token regelmässig im
/// Klartext, und ein einziges Datenbankleck gibt dann jedem alles.
@@ -23,7 +23,7 @@ public static class Secrets
public static string Create() =>
Base64UrlEncode(RandomNumberGenerator.GetBytes(Bytes));
/// <summary>Der Fingerabdruck, wie er in der Datenbank steht.</summary>
/// <summary>Der Fingerfingerprint, wie er in der Datenbank steht.</summary>
public static string Fingerprint(string secret) =>
Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(secret)));
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Flaro.Core.Delivery;
///
/// <remarks>
/// <para>
/// <b>Warum der Name alles trägt.</b> Im Kratzplatz liegen Dateien, zu denen
/// <b>Warum der Name alles trägt.</b> Im Transcode-Cache liegen Dateien, zu denen
/// es keine Tabelle gibt. Ein Aufräumer muss allein am Namen erkennen können,
/// wozu eine Datei gehört und ob sie noch gilt — sonst bliebe ihm nur,
/// alles zu behalten oder alles wegzuwerfen.
@@ -21,7 +21,7 @@ namespace Flaro.Core.Delivery;
/// <param name="ObergrenzeBytes">Wie viel insgesamt liegen bleiben darf; <c>0</c> heisst unbegrenzt.</param>
/// <param name="Unbenutzt">Ab wann eine unbenutzte Datei auch unterhalb der Grenze geht; <c>Zero</c> heisst nie.</param>
/// <param name="Schonfrist">Wie lange eine frisch gebrauchte Datei in jedem Fall bleibt.</param>
public sealed record Kratzplatzregeln(
public sealed record Transcoderegeln(
long ObergrenzeBytes,
TimeSpan Unbenutzt,
TimeSpan Schonfrist)
@@ -34,7 +34,7 @@ public sealed record Kratzplatzregeln(
/// Schonfrist decken die Lücke zwischen zwei Teilanfragen und einen
/// Neustart, der den Benutzungszähler verliert.
/// </remarks>
public static Kratzplatzregeln Vorgabe { get; } =
public static Transcoderegeln Vorgabe { get; } =
new(20L * 1024 * 1024 * 1024, TimeSpan.FromDays(7), TimeSpan.FromMinutes(30));
/// <summary>Nimmt entgegen, was eingestellt wurde, und biegt Unsinn zurecht.</summary>
@@ -45,13 +45,13 @@ public sealed record Kratzplatzregeln(
/// Datei weg — deshalb eine Untergrenze von fünf Minuten. Negative Werte
/// gelten als „aus".
/// </remarks>
public static Kratzplatzregeln Geprueft(double obergrenzeGb, int unbenutztTage, int schonfristMinuten) =>
public static Transcoderegeln Geprueft(double obergrenzeGb, int unbenutztTage, int schonfristMinuten) =>
new(obergrenzeGb <= 0 ? 0 : (long)(obergrenzeGb * 1024 * 1024 * 1024),
unbenutztTage <= 0 ? TimeSpan.Zero : TimeSpan.FromDays(Math.Min(unbenutztTage, 3650)),
TimeSpan.FromMinutes(Math.Clamp(schonfristMinuten, 5, 1440)));
}
/// <summary>Eine Datei im Kratzplatz, so wie sie dort liegt.</summary>
/// <summary>Eine Datei im Transcode-Cache, so wie sie dort liegt.</summary>
///
/// <param name="Benutzt">
/// Wann sie zuletzt gebraucht wurde — die Änderungszeit der Datei selbst.
@@ -66,7 +66,7 @@ public readonly record struct Paketakte(
Paketname.Teile? Name);
/// <summary>Welche Dateien weggeräumt werden — reine Rechnung, ohne Platte.</summary>
public static class Kratzplatzauswahl
public static class Transcodeauswahl
{
/// <param name="bekannt">Kennung → Änderungszeitpunkt der Quelle, aus der Mediathek.</param>
/// <param name="bauart">Wie der Server heute baut.</param>
@@ -75,7 +75,7 @@ public static class Kratzplatzauswahl
IReadOnlyList<Paketakte> akten,
IReadOnlyDictionary<Guid, long> bekannt,
int bauart,
Kratzplatzregeln regeln,
Transcoderegeln regeln,
DateTimeOffset jetzt,
Func<string, bool> inBenutzung)
{
@@ -124,8 +124,8 @@ public sealed class Stichwortlink
public Guid? SeriesId { get; init; }
}
/// <summary>Was für ein Abschnitt eine Marke bezeichnet.</summary>
public enum Markenart
/// <summary>Was für ein Abschnitt ein Segment bezeichnet.</summary>
public enum Segmentart
{
/// <summary>Der Vorspann.</summary>
Intro,
@@ -148,7 +148,7 @@ public enum Markenart
/// im Film ist dieselbe.
/// </para>
/// </remarks>
public sealed class Marke
public sealed class Segment
{
public Guid Id { get; init; } = Guid.CreateVersion7();
/// <summary>Zu welcher Folge sie gehört — oder zu welchem Film.</summary>
@@ -162,7 +162,7 @@ public sealed class Marke
/// </remarks>
public Guid? EpisodeId { get; init; }
public Guid? MovieId { get; init; }
public required Markenart Art { get; init; }
public required Segmentart Art { get; init; }
public required double VonSekunden { get; set; }
public required double BisSekunden { get; set; }
@@ -195,21 +195,21 @@ public sealed class Marke
/// Lauf.
/// </para>
/// </remarks>
public sealed class Markensuche
public sealed class Segmentsuche
{
public Guid Id { get; init; } = Guid.CreateVersion7();
/// <summary>Eine Staffel einer Serie — oder ein Film.</summary>
///
/// <remarks>
/// Genau eines von beiden, wie bei <see cref="Marke"/>. Bei einer
/// Genau eines von beiden, wie bei <see cref="Segment"/>. Bei einer
/// Staffel steht die Nummer daneben; ein Film hat keine.
/// </remarks>
public Guid? SeriesId { get; init; }
public int? Staffel { get; init; }
public Guid? MovieId { get; init; }
public required Markenart Art { get; init; }
public required Segmentart Art { get; init; }
/// <summary>Wie viele Folgen die Staffel hatte, als gesucht wurde.</summary>
///
+2 -2
View File
@@ -28,7 +28,7 @@ public enum Scanphase
/// über Stunden, und ohne eine Zeile, die mitzählt, sieht die
/// Verwaltung minutenlang so aus, als täte sich nichts.
/// </remarks>
Marken,
Segmente,
/// <summary>Eine Notiz fürs Protokoll — nicht für die Fortschrittszeile.</summary>
///
@@ -79,7 +79,7 @@ public readonly record struct Scanschritt(Scanphase Phase, string Was, int Erled
Scanphase.Holen => $"Hole Metadaten zu {Was}{zaehler}",
Scanphase.Bilder => $"Lade Bilder zu {Was}{zaehler}",
Scanphase.Aufraeumen => "Räume auf und lege Doppeltes zusammen",
Scanphase.Marken => $"Suche Vorspann und Abspann: {Was}{zaehler}",
Scanphase.Segmente => $"Suche Vorspann und Abspann: {Was}{zaehler}",
Scanphase.Vermerk => Was,
_ => Was,
};
@@ -18,7 +18,7 @@ namespace Flaro.Core.Library;
/// <para>
/// <b>Transcode-Zwischenstände gehören ausdrücklich nicht hierher.</b> Die
/// liegen unter <c>FLARO_TRANSCODE</c>, auf einem eigenen Volume. Ein
/// Medienordner ist kein Kratzplatz.
/// Medienordner ist kein Transcode-Cache.
/// </para>
/// </remarks>
public static class SidecarNames
+12 -12
View File
@@ -20,7 +20,7 @@ namespace Flaro.Core.Media;
///
/// <param name="Sekunde">Die Stelle im Film.</param>
/// <param name="Dunkelanteil">Wie viel Prozent des Bildes dunkel sind, 0 bis 100.</param>
public readonly record struct Bildprobe(double Sekunde, double Dunkelanteil);
public readonly record struct Frameprobe(double Sekunde, double Dunkelanteil);
/// <summary>Ein Stück Abspann: von wann bis wann.</summary>
///
@@ -80,10 +80,10 @@ public readonly record struct Abspannregeln
/// <b>In Sekunden, nicht in Proben</b> — dieselbe Lehre wie bei
/// <see cref="Schlusstafel"/>, hier eine Zeile höher übersehen. Als
/// Probenzahl hing die Nachsicht daran, wie dicht die Datei ihre
/// Schlüsselbilder setzt: gemessen an „(T)Raumschiff Surprise" kommen
/// Keyframes setzt: gemessen an „(T)Raumschiff Surprise" kommen
/// 1318 Proben auf zwanzig Minuten, also eine alle 0,9 Sekunden —
/// zwei Proben waren dort 1,8 Sekunden Nachsicht. Bei einer Datei mit
/// einem Schlüsselbild alle zehn Sekunden waren es zwanzig. Dasselbe
/// einem Keyframe alle zehn Sekunden waren es zwanzig. Dasselbe
/// Zugeständnis, zehnfach verschieden.
/// </para>
/// </remarks>
@@ -94,7 +94,7 @@ public readonly record struct Abspannregeln
/// <remarks>
/// <b>In Sekunden, nicht in Proben.</b> Eine Logotafel dauert, was sie
/// dauert; wie viele Messpunkte darauf fallen, hängt allein daran, wie
/// dicht die Datei ihre Schlüsselbilder setzt. In Proben gezählt wäre
/// dicht die Datei ihre Keyframes setzt. In Proben gezählt wäre
/// dieselbe Tafel bei der einen Datei verziehen und bei der nächsten
/// ein Abbruch.
/// </remarks>
@@ -156,7 +156,7 @@ public readonly record struct Abspannregeln
/// <b>Reine Rechnung, ohne Datei.</b> Das Abtasten braucht ffmpeg; das
/// Beurteilen braucht nichts. Getrennt lässt sich das Schwierige prüfen,
/// ohne einen Film vorzuhalten — dieselbe Naht wie bei
/// <see cref="Tonabgleich"/>.
/// <see cref="Fingerprintabgleich"/>.
/// </para>
/// </remarks>
public static class Abspannkante
@@ -165,10 +165,10 @@ public static class Abspannkante
///
/// <param name="proben">
/// Messpunkte über das Ende des Films, aufsteigend nach Zeit. Sie dürfen
/// ungleichmässig liegen: Schlüsselbilder tun das.
/// ungleichmässig liegen: Keyframes tun das.
/// </param>
public static IReadOnlyList<Abspannstück> Suchen(
IReadOnlyList<Bildprobe> proben, Abspannregeln regeln) =>
IReadOnlyList<Frameprobe> proben, Abspannregeln regeln) =>
Suchen(proben, regeln, out _);
/// <param name="grund">
@@ -178,7 +178,7 @@ public static class Abspannkante
/// und ohne diese Zeile beginnt jede Nachfrage wieder beim Raten.
/// </param>
public static IReadOnlyList<Abspannstück> Suchen(
IReadOnlyList<Bildprobe> proben, Abspannregeln regeln, out string grund)
IReadOnlyList<Frameprobe> proben, Abspannregeln regeln, out string grund)
{
grund = "gefunden";
if (proben.Count < 2)
@@ -244,11 +244,11 @@ public static class Abspannkante
/// <remarks>
/// <b>Die Nachsicht zählt Sekunden, nicht Proben.</b> Eine Schrifttafel
/// blitzt auf, ein Logo steht zwei Sekunden im Bild; wie viele
/// Messpunkte darauf fallen, hängt allein an der Schlüsselbilddichte
/// Messpunkte darauf fallen, hängt allein an der Keyframedichte
/// der Datei und schwankt um eine Zehnerpotenz.
/// </remarks>
private static List<Abspannstück> Läufe(
IReadOnlyList<Bildprobe> proben, Abspannregeln regeln)
IReadOnlyList<Frameprobe> proben, Abspannregeln regeln)
{
var läufe = new List<Abspannstück>();
double? von = null;
@@ -281,7 +281,7 @@ public static class Abspannkante
/// <summary>Die letzte helle Probe vor dem Fund — der Suchbereich fürs Nachmessen.</summary>
public static double? HellDavor(
IReadOnlyList<Bildprobe> proben, double dunkelAb, Abspannregeln regeln)
IReadOnlyList<Frameprobe> proben, double dunkelAb, Abspannregeln regeln)
{
double? letzteHelle = null;
foreach (var p in proben)
@@ -297,7 +297,7 @@ public static class Abspannkante
/// <remarks>
/// <para>
/// <b>Binäre Suche mit kurzen Sonden, nicht noch ein Durchlauf.</b> Die
/// grobe Reihe stammt aus Schlüsselbildern, und die liegen je nach
/// grobe Reihe stammt aus Keyframesn, und die liegen je nach
/// Aufnahme fünf bis zehn Sekunden auseinander. Den ganzen Bereich fein
/// abzutasten hiesse, ihn zu dekodieren; zehn Sonden von je einer
/// Sekunde kosten einen Bruchteil davon. Denselben Weg geht Jellyfins
@@ -24,7 +24,7 @@ public sealed record Gemeinsamkeit(double AVon, double ABis, double BVon, double
/// </para>
/// <para>
/// <b>Verglichen werden Tonabdrücke, keine Töne.</b> Was hereinkommt, ist je
/// Folge eine Reihe von 32-Bit-Zahlen, eine je Zeitscheibe — der Abdruck,
/// Folge eine Reihe von 32-Bit-Zahlen, eine je Zeitscheibe — der Fingerprint,
/// den Chromaprint aus dem Ton rechnet. Zwei Aufnahmen desselben Stücks
/// ergeben darin fast dieselben Zahlen, auch bei anderer Lautstärke, anderem
/// Codec und anderer Bitrate. Deshalb wird nicht auf Gleichheit geprüft,
@@ -36,7 +36,7 @@ public sealed record Gemeinsamkeit(double AVon, double ABis, double BVon, double
/// sich das Schwierige prüfen, ohne eine Mediendatei vorzuhalten.
/// </para>
/// </remarks>
public static class Tonabgleich
public static class Fingerprintabgleich
{
/// <summary>Wie viele Bits zweier Abdrücke sich unterscheiden dürfen.</summary>
///
@@ -67,7 +67,7 @@ public static class Tonabgleich
/// <summary>Sucht das längste Stück, das <paramref name="a"/> und <paramref name="b"/> teilen.</summary>
///
/// <param name="sekundenJeAbdruck">
/// <param name="sekundenJeFingerprint">
/// Wie viel Zeit eine Zeitscheibe abdeckt. Chromaprint rechnet mit
/// 11025 Hz und 4096 Abtastwerten bei zwei Dritteln Überlappung — das
/// sind rund 0,1238 Sekunden.
@@ -75,14 +75,14 @@ public static class Tonabgleich
public static Gemeinsamkeit? Finden(
IReadOnlyList<uint> a,
IReadOnlyList<uint> b,
double sekundenJeAbdruck,
double sekundenJeFingerprint,
double mindestlänge = StandardMindestlänge,
int bitabstand = StandardBitabstand,
int lücke = StandardLücke)
{
if (a.Count == 0 || b.Count == 0 || sekundenJeAbdruck <= 0) return null;
if (a.Count == 0 || b.Count == 0 || sekundenJeFingerprint <= 0) return null;
var mindestens = (int)Math.Ceiling(mindestlänge / sekundenJeAbdruck);
var mindestens = (int)Math.Ceiling(mindestlänge / sekundenJeFingerprint);
if (mindestens <= 0) mindestens = 1;
Gemeinsamkeit? beste = null;
@@ -90,7 +90,7 @@ public static class Tonabgleich
// **Jede Verschiebung durchprobieren.** Das Intro steht in beiden
// Folgen an einer anderen Stelle; gesucht ist die Verschiebung, bei
// der die meisten Zeitscheiben aufeinanderpassen. Bei zwei Minuten
// Abdruck sind das ein paar Millionen Vergleiche — ein Wimpernschlag
// Fingerprint sind das ein paar Millionen Vergleiche — ein Wimpernschlag
// gegen das Lesen der Datei, aus der sie stammen.
for (var versatz = -(b.Count - 1); versatz < a.Count; versatz++)
{
@@ -102,13 +102,13 @@ public static class Tonabgleich
var lauf = LängsterLauf(a, b, vonA, vonB, länge, bitabstand, lücke);
if (lauf is not var (start, dauer) || dauer < mindestens) continue;
if (beste is not null && dauer * sekundenJeAbdruck <= beste.Laenge) continue;
if (beste is not null && dauer * sekundenJeFingerprint <= beste.Laenge) continue;
beste = new Gemeinsamkeit(
(vonA + start) * sekundenJeAbdruck,
(vonA + start + dauer) * sekundenJeAbdruck,
(vonB + start) * sekundenJeAbdruck,
(vonB + start + dauer) * sekundenJeAbdruck);
(vonA + start) * sekundenJeFingerprint,
(vonA + start + dauer) * sekundenJeFingerprint,
(vonB + start) * sekundenJeFingerprint,
(vonB + start + dauer) * sekundenJeFingerprint);
}
return beste is not null && beste.Laenge >= mindestlänge ? beste : null;
@@ -176,8 +176,8 @@ public static class Tonabgleich
/// </para>
/// </remarks>
public static IReadOnlyDictionary<TSchlüssel, (double Von, double Bis)> ReiheAbgleichen<TSchlüssel>(
IReadOnlyList<(TSchlüssel Schlüssel, IReadOnlyList<uint> Abdruck)> folgen,
double sekundenJeAbdruck,
IReadOnlyList<(TSchlüssel Schlüssel, IReadOnlyList<uint> Fingerprint)> folgen,
double sekundenJeFingerprint,
double mindestlänge = StandardMindestlänge)
where TSchlüssel : notnull
{
@@ -186,10 +186,10 @@ public static class Tonabgleich
for (var i = 0; i + 1 < folgen.Count; i++)
{
var (schlüsselA, abdruckA) = folgen[i];
var (schlüsselB, abdruckB) = folgen[i + 1];
var (schlüsselA, fingerprintA) = folgen[i];
var (schlüsselB, fingerprintB) = folgen[i + 1];
var treffer = Finden(abdruckA, abdruckB, sekundenJeAbdruck, mindestlänge);
var treffer = Finden(fingerprintA, fingerprintB, sekundenJeFingerprint, mindestlänge);
if (treffer is null) continue;
// Der erste Fund je Folge gewinnt: er stammt vom Vergleich mit
@@ -79,8 +79,8 @@ public static class ProgressRule
var schwelle = laenge * regeln.GesehenAnteil;
// Der Abspann zieht die Schwelle nach vorn, nie nach hinten: eine
// falsch erkannte Marke bei Minute vierzig darf nicht dazu führen,
// dass eine Folge erst nach der Marke als gesehen gilt.
// falsch erkannte Segment bei Minute vierzig darf nicht dazu führen,
// dass eine Folge erst nach dem Segment als gesehen gilt.
if (abspannAb is { } ab && ab > TimeSpan.Zero && ab < schwelle) schwelle = ab;
return new Outcome(stelle, Watched: stelle >= schwelle);