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
@@ -220,7 +220,7 @@ public class EinladungTests
Assert.That(einladung.Token, Is.Not.Empty);
Assert.That(roh, Does.Not.Contain(einladung.Token),
"Gespeichert ist nur der Fingerabdruck — aus der Liste darf kein Link zu bauen sein.");
"Gespeichert ist nur der Fingerfingerprint — aus der Liste darf kein Link zu bauen sein.");
}
/// <summary>Ein Tippfehler im Namen darf den Link nicht verbrennen.</summary>
+2 -2
View File
@@ -30,8 +30,8 @@ public sealed class FlaroServer : IDisposable
///
/// <remarks>
/// <b>Sparsam benutzen.</b> Ein Test, der am Server vorbei schreibt,
/// prüft nicht mehr, ob der Server es könnte. Für Marken ist es nötig:
/// sie entstehen aus einem Tonabgleich, und der braucht Mediendateien
/// prüft nicht mehr, ob der Server es könnte. Für Segmente ist es nötig:
/// sie entstehen aus einem Fingerprintabgleich, und der braucht Mediendateien
/// mit echtem Ton, die es hier nicht gibt.
/// </remarks>
public async Task InDerDatenbank(Func<FlaroContext, Task> tun)
@@ -9,12 +9,12 @@ namespace Flaro.Api.Tests;
/// <summary>Vorspann und Abspann auf dem Weg zu den Spielern.</summary>
///
/// <remarks>
/// <b>Ohne Tonabgleich.</b> Ob er findet, was er finden soll, steht in
/// <c>TonabgleichTests</c> — mit erfundenen Abdrücken und ohne Mediendateien.
/// Hier geht es um das Stück danach: dass eine gefundene Marke auch bei dem
/// <b>Ohne Fingerprintabgleich.</b> Ob er findet, was er finden soll, steht in
/// <c>FingerprintabgleichTests</c> — mit erfundenen Abdrücken und ohne Mediendateien.
/// Hier geht es um das Stück danach: dass eine gefundene Segment auch bei dem
/// ankommt, der den Knopf zeichnen soll.
/// </remarks>
public class MarkenTests
public class SegmentTests
{
private string _bestand = "";
@@ -67,7 +67,7 @@ public class MarkenTests
}
[Test]
public async Task Eine_Marke_kommt_bei_der_Folge_an()
public async Task Eine_Segment_kommt_bei_der_Folge_an()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
@@ -77,14 +77,14 @@ public class MarkenTests
await server.InDerDatenbank(async db =>
{
db.Marken.Add(new Marke
db.Segmente.Add(new Segment
{
EpisodeId = folge, Art = Markenart.Intro,
EpisodeId = folge, Art = Segmentart.Intro,
VonSekunden = 30, BisSekunden = 65, ErkanntAm = DateTimeOffset.UtcNow,
});
db.Marken.Add(new Marke
db.Segmente.Add(new Segment
{
EpisodeId = folge, Art = Markenart.Abspann,
EpisodeId = folge, Art = Segmentart.Abspann,
VonSekunden = 2500, BisSekunden = 2600, ErkanntAm = DateTimeOffset.UtcNow,
});
await db.SaveChangesAsync();
@@ -105,7 +105,7 @@ public class MarkenTests
/// <summary>Auch in der Folgenliste — dort holt die App sie beim Blättern.</summary>
[Test]
public async Task Die_Folgenliste_bringt_die_Marken_mit()
public async Task Die_Folgenliste_bringt_die_Segmente_mit()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
@@ -116,9 +116,9 @@ public class MarkenTests
await server.InDerDatenbank(async db =>
{
db.Marken.Add(new Marke
db.Segmente.Add(new Segment
{
EpisodeId = folge, Art = Markenart.Intro,
EpisodeId = folge, Art = Segmentart.Intro,
VonSekunden = 10, BisSekunden = 40, ErkanntAm = DateTimeOffset.UtcNow,
});
await db.SaveChangesAsync();
@@ -131,11 +131,11 @@ public class MarkenTests
{
Assert.That(liste!.Items.Single(f => f.Id == folge).Markers, Has.Count.EqualTo(1));
Assert.That(liste.Items.Single(f => f.Id != folge).Markers, Is.Empty,
"eine Folge ohne Marke bekommt eine leere Liste, nicht die der anderen");
"eine Folge ohne Segment bekommt eine leere Liste, nicht die der anderen");
});
}
/// <summary>Ohne erkannte Marken bleibt die Liste leer — kein Knopf, kein Fehler.</summary>
/// <summary>Ohne erkannte Segmente bleibt die Liste leer — kein Knopf, kein Fehler.</summary>
[Test]
public async Task Ohne_Erkennung_ist_die_Liste_leer()
{
@@ -196,11 +196,11 @@ public class MarkenTests
(await client.PostAsync($"/api/v1/series/{serie}/marken", null)).EnsureSuccessStatusCode();
// Der Lauf hängt im Hintergrund; ohne fpcalc ist er sofort durch.
List<Markensuche> vermerke = [];
List<Segmentsuche> vermerke = [];
for (var versuch = 0; versuch < 40; versuch++)
{
await server.InDerDatenbank(async db =>
vermerke = await db.Markensuchen.AsNoTracking()
vermerke = await db.Segmentsuchen.AsNoTracking()
.Where(x => x.SeriesId == serie).ToListAsync());
if (vermerke.Count > 0) break;
await Task.Delay(50);
@@ -217,9 +217,9 @@ public class MarkenTests
}
}
/// <summary>Ein Film trägt seine Abspannmarke genauso.</summary>
/// <summary>Ein Film trägt seine Abspannsegment genauso.</summary>
[Test]
public async Task Ein_Film_bekommt_seine_Marke()
public async Task Ein_Film_bekommt_seine_Segment()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
@@ -230,9 +230,9 @@ public class MarkenTests
await server.InDerDatenbank(async db =>
{
db.Marken.Add(new Marke
db.Segmente.Add(new Segment
{
MovieId = film, Art = Markenart.Abspann,
MovieId = film, Art = Segmentart.Abspann,
VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow,
});
await db.SaveChangesAsync();
@@ -249,7 +249,7 @@ public class MarkenTests
});
}
/// <summary>Eine falsche Marke lässt sich wieder wegnehmen.</summary>
/// <summary>Eine falsches Segment lässt sich wieder wegnehmen.</summary>
///
/// <remarks>
/// Ohne diesen Weg wäre die Erkennung eine Blackbox, die manchmal recht
@@ -257,7 +257,7 @@ public class MarkenTests
/// tun ausser die Datenbank anzufassen.
/// </remarks>
[Test]
public async Task Eine_falsche_Filmmarke_laesst_sich_verwerfen()
public async Task Eine_falsche_Filmsegment_laesst_sich_verwerfen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
@@ -268,14 +268,14 @@ public class MarkenTests
await server.InDerDatenbank(async db =>
{
db.Marken.Add(new Marke
db.Segmente.Add(new Segment
{
MovieId = film, Art = Markenart.Abspann,
MovieId = film, Art = Segmentart.Abspann,
VonSekunden = 60, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow,
});
db.Markensuchen.Add(new Markensuche
db.Segmentsuchen.Add(new Segmentsuche
{
MovieId = film, Art = Markenart.Abspann, Folgen = 1,
MovieId = film, Art = Segmentart.Abspann, Folgen = 1,
GelaufenAm = DateTimeOffset.UtcNow,
});
await db.SaveChangesAsync();
@@ -286,14 +286,14 @@ public class MarkenTests
var vermerke = 0;
await server.InDerDatenbank(async db =>
vermerke = await db.Markensuchen.CountAsync(x => x.MovieId == film));
vermerke = await db.Segmentsuchen.CountAsync(x => x.MovieId == film));
Assert.Multiple(() =>
{
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
Assert.That(danach!.Markers, Is.Empty);
Assert.That(vermerke, Is.EqualTo(1),
"der Vermerk bleibt, sonst legte der nächste Scan dieselbe Marke wieder an");
"der Vermerk bleibt, sonst legte der nächste Scan dieselbe Segment wieder an");
});
}
@@ -301,9 +301,9 @@ public class MarkenTests
///
/// <remarks>
/// <para>
/// <b>„Schon durchsucht" und „hat eine Marke" sind zwei verschiedene
/// <b>„Schon durchsucht" und „hat ein Segment" sind zwei verschiedene
/// Dinge.</b> Ein Film, bei dem die Suche einmal nichts fand, trägt
/// einen Vermerk und keine Marke — und war damit für jeden weiteren
/// einen Vermerk und kein Segment — und war damit für jeden weiteren
/// Lauf erledigt, obwohl er der interessanteste Fall ist.
/// </para>
/// <para>
@@ -313,9 +313,9 @@ public class MarkenTests
/// </para>
/// </remarks>
[TestCase("Neue", false, TestName = "Neue überspringt, was schon durchsucht ist")]
[TestCase("Luecken", true, TestName = "Luecken nimmt sich den Film ohne Marke vor")]
[TestCase("Luecken", true, TestName = "Luecken nimmt sich den Film ohne Segment vor")]
[TestCase("Alles", true, TestName = "Alles nimmt sich alles vor")]
public async Task Der_Umfang_entscheidet_bei_einem_Film_ohne_Marke(
public async Task Der_Umfang_entscheidet_bei_einem_Film_ohne_Segment(
string umfang, bool erwartetAngefasst)
{
Datei("Matrix (1999)/Matrix (1999).mkv");
@@ -325,7 +325,7 @@ public class MarkenTests
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
var film = filme!.Items[0].Id;
// Gesucht wurde schon, gefunden nichts: Vermerk ja, Marke nein.
// Gesucht wurde schon, gefunden nichts: Vermerk ja, Segment nein.
var vorher = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
await server.InDerDatenbank(async db =>
{
@@ -334,9 +334,9 @@ public class MarkenTests
var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film);
datei.Duration = TimeSpan.FromMinutes(120);
db.Markensuchen.Add(new Markensuche
db.Segmentsuchen.Add(new Segmentsuche
{
MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher,
MovieId = film, Art = Segmentart.Abspann, Folgen = 1, GelaufenAm = vorher,
});
await db.SaveChangesAsync();
});
@@ -348,7 +348,7 @@ public class MarkenTests
for (var versuch = 0; versuch < 40; versuch++)
{
await server.InDerDatenbank(async db =>
gelaufen = await db.Markensuchen.AsNoTracking()
gelaufen = await db.Segmentsuchen.AsNoTracking()
.Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync());
if (gelaufen != vorher) break;
await Task.Delay(50);
@@ -360,9 +360,9 @@ public class MarkenTests
: "dieser Umfang muss ihn überspringen");
}
/// <summary>Ein Film mit Marke bleibt bei „nur Lücken" unangetastet.</summary>
/// <summary>Ein Film mit Segment bleibt bei „nur Lücken" unangetastet.</summary>
[Test]
public async Task Luecken_laesst_einen_Film_mit_Marke_in_Ruhe()
public async Task Luecken_laesst_einen_Film_mit_Segment_in_Ruhe()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
@@ -377,14 +377,14 @@ public class MarkenTests
var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film);
datei.Duration = TimeSpan.FromMinutes(120);
db.Marken.Add(new Marke
db.Segmente.Add(new Segment
{
MovieId = film, Art = Markenart.Abspann,
MovieId = film, Art = Segmentart.Abspann,
VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = vorher,
});
db.Markensuchen.Add(new Markensuche
db.Segmentsuchen.Add(new Segmentsuche
{
MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher,
MovieId = film, Art = Segmentart.Abspann, Folgen = 1, GelaufenAm = vorher,
});
await db.SaveChangesAsync();
});
@@ -395,10 +395,10 @@ public class MarkenTests
var gelaufen = DateTimeOffset.MinValue;
await server.InDerDatenbank(async db =>
gelaufen = await db.Markensuchen.AsNoTracking()
gelaufen = await db.Segmentsuchen.AsNoTracking()
.Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync());
Assert.That(gelaufen, Is.EqualTo(vorher),
"wo schon eine Marke steht, ist nichts zu füllen");
"wo schon ein Segment steht, ist nichts zu füllen");
}
}
+6 -6
View File
@@ -333,12 +333,12 @@ public class StreamTests
/// Strom fängt bei null an, der Film bei Minute 30.
/// </para>
/// <para>
/// <b>Und „echt" heisst: beim Schlüsselbild.</b> Dieser Test stand vorher
/// <b>Und „echt" heisst: beim Keyframe.</b> Dieser Test stand vorher
/// auf „genau zwei Sekunden" und hielt damit einen Fehler fest. Ein
/// kopierter Bildstrom lässt sich nicht mitten in einer Bildgruppe
/// aufschneiden: der Strom fängt beim Schlüsselbild davor an, und wer
/// aufschneiden: der Strom fängt beim Keyframe davor an, und wer
/// trotzdem „zwei" zurückmeldet, verschiebt die Uhr des Spielers um den
/// Unterschied. Diese Testdatei hat genau ein Schlüsselbild, bei null —
/// Unterschied. Diese Testdatei hat genau ein Keyframe, bei null —
/// also ist null die richtige Antwort.
/// </para>
/// </remarks>
@@ -353,10 +353,10 @@ public class StreamTests
Assert.That(strom.StartSeconds, Is.LessThanOrEqualTo(2),
"Weiter als gewünscht darf der Strom nie anfangen — das überspränge Film.");
Assert.That(strom.StartSeconds, Is.EqualTo(0),
"Diese Datei hat nur ein Schlüsselbild, bei null.");
"Diese Datei hat nur ein Keyframe, bei null.");
}
/// <summary>Schliessen räumt auf — sonst füllt sich der Kratzplatz.</summary>
/// <summary>Schliessen räumt auf — sonst füllt sich der Transcode-Cache.</summary>
[Test]
public async Task Schliessen_raeumt_die_Sitzung_weg()
{
@@ -501,7 +501,7 @@ public class StreamTests
/// <para>
/// <b>Verglichen wird mit dem gemeldeten Vorlauf, nicht mit dem
/// gewünschten</b> — und das ist keine Bequemlichkeit. Beim Kopieren
/// beginnt ffmpeg am Schlüsselbild vor dem Wunsch; in einem Prüfclip von
/// beginnt ffmpeg am Keyframe vor dem Wunsch; in einem Prüfclip von
/// vier Sekunden ist das die Null. Der Untertitel muss zu dem passen, was
/// wirklich läuft.
/// </para>
@@ -6,7 +6,7 @@ using Flaro.Core.Delivery;
namespace Flaro.Api.Tests;
/// <summary>Was der Server auf dem Kratzplatz wegräumt — und was nicht.</summary>
/// <summary>Was der Server auf dem Transcode-Cache wegräumt — und was nicht.</summary>
///
/// <remarks>
/// <para>
@@ -17,13 +17,13 @@ namespace Flaro.Api.Tests;
/// <para>
/// <b>Hier wird die Verdrahtung geprüft</b>, nicht die Regel — die steht
/// ohne Platte und ohne Server in
/// <c>Flaro.Core.Tests/KratzplatzregelTests</c>. Was hier zählt: findet der
/// <c>Flaro.Core.Tests/TranscoderegelTests</c>. Was hier zählt: findet der
/// Aufräumer die Dateien, erkennt er, was zur Mediathek gehört, und lässt
/// er das Gültige stehen.
/// </para>
/// </remarks>
[TestFixture]
public sealed class KratzplatzTests
public sealed class TranscodeTests
{
private string _bestand = "";
@@ -41,8 +41,8 @@ public sealed class KratzplatzTests
try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { /* Kür */ }
}
/// <summary>Ein Server mit einem eingelesenen Film und bekanntem Kratzplatz.</summary>
private async Task<(HttpClient Client, string Kratzplatz, Guid DateiId)> Aufgesetzt(FlaroServer server)
/// <summary>Ein Server mit einem eingelesenen Film und bekanntem Transcode-Cache.</summary>
private async Task<(HttpClient Client, string Ordner, Guid DateiId)> Aufgesetzt(FlaroServer server)
{
var voll = Path.Combine(_bestand, "Probe (2026)", "Probe (2026).mkv");
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
@@ -244,18 +244,18 @@ public sealed class KratzplatzTests
Paketname.Bauen(dateiId, Gestempelt, PackageCoordinator.Bauart), kilobyte: 8);
var orte = await client.GetFromJsonAsync<List<Speicherort>>("/api/v1/system/storage");
var kratzplatz = orte!.First(o => o.Variable == "FLARO_TRANSCODE");
var transcode = orte!.First(o => o.Variable == "FLARO_TRANSCODE");
Assert.Multiple(() =>
{
Assert.That(kratzplatz.Teile, Is.Not.Null);
Assert.That(kratzplatz.Teile!.Select(t => t.Name),
Assert.That(transcode.Teile, Is.Not.Null);
Assert.That(transcode.Teile!.Select(t => t.Name),
Is.EquivalentTo(new[] { "stroeme", "packages", "untertitel" }));
var pakete = kratzplatz.Teile!.First(t => t.Name == "packages");
var pakete = transcode.Teile!.First(t => t.Name == "packages");
Assert.That(pakete.Dateien, Is.EqualTo(1));
Assert.That(pakete.Frei, Is.EqualTo(8 * 1024), "das Feld Frei trägt hier die Belegung des Ordners");
Assert.That(orte!.First(o => o.Variable == "FLARO_DATA").Teile, Is.Null,
"nur der Kratzplatz wird aufgeschlüsselt");
"nur der Transcode-Cache wird aufgeschlüsselt");
});
}
@@ -261,7 +261,7 @@ public sealed class WartungsplanTests
Assert.That(nachher!.BackupDays, Is.EqualTo(1));
Assert.That(nachher.BackupKeep, Is.EqualTo(10));
Assert.That(nachher.OptimizeDays, Is.EqualTo(7), "was nicht geschickt wurde, bleibt");
Assert.That(nachher.CapGb, Is.EqualTo(20).Within(0.01), "und der Kratzplatz auch");
Assert.That(nachher.CapGb, Is.EqualTo(20).Within(0.01), "und der Transcode-Cache auch");
});
}
}