„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>
248 lines
8.5 KiB
C#
248 lines
8.5 KiB
C#
using Flaro.Core.Delivery;
|
|
|
|
namespace Flaro.Core.Tests;
|
|
|
|
/// <summary>Welche umgepackten Dateien weggeräumt werden.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Der Anlass: 39 Gigabyte Transcode-Cache.</b> Für jede Mitnahme baut der
|
|
/// Server eine MP4 in Quellgrösse und löschte sie nie. Hier steht die Regel,
|
|
/// die das beendet — ohne Platte, ohne Server, damit die Entscheidung selbst
|
|
/// nachlesbar ist und nicht nur ihr Ergebnis.
|
|
/// </remarks>
|
|
public class TranscoderegelTests
|
|
{
|
|
private static readonly DateTimeOffset Jetzt = new(2026, 10, 8, 12, 0, 0, TimeSpan.Zero);
|
|
private const int Bauart = 2;
|
|
|
|
private static readonly Guid EinsId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
|
private static readonly Guid ZweiId = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
|
|
|
private const long Gigabyte = 1024L * 1024 * 1024;
|
|
|
|
/// <summary>Eine Datei, wie sie im Transcode-Cache läge.</summary>
|
|
private static Paketakte Akte(
|
|
Guid id, long gigabyte, TimeSpan vorWieLange, long? geaendertMs = null, int bauart = Bauart)
|
|
{
|
|
var ms = geaendertMs ?? 1_000;
|
|
var name = Paketname.Bauen(id, DateTimeOffset.FromUnixTimeMilliseconds(ms), bauart);
|
|
return new Paketakte($"/kratz/packages/{name}", gigabyte * Gigabyte,
|
|
Jetzt - vorWieLange, Paketname.Lesen(name));
|
|
}
|
|
|
|
/// <summary>Die Mediathek kennt beide Dateien in genau diesem Stand.</summary>
|
|
private static readonly Dictionary<Guid, long> Bekannt =
|
|
new() { [EinsId] = 1_000, [ZweiId] = 1_000 };
|
|
|
|
private static IReadOnlyList<string> Weg(
|
|
IReadOnlyList<Paketakte> akten,
|
|
Transcoderegeln? regeln = null,
|
|
IReadOnlyDictionary<Guid, long>? bekannt = null,
|
|
Func<string, bool>? inBenutzung = null) =>
|
|
Transcodeauswahl.Wegzuwerfen(
|
|
akten, bekannt ?? Bekannt, Bauart,
|
|
regeln ?? Transcoderegeln.Vorgabe, Jetzt,
|
|
inBenutzung ?? (_ => false))
|
|
.Select(a => a.Pfad).ToList();
|
|
|
|
/// <summary>Was nie wieder ausgeliefert wird, geht — auch wenn Platz wäre.</summary>
|
|
[Test]
|
|
public void Eine_Datei_ohne_Eintrag_in_der_Mediathek_geht_weg()
|
|
{
|
|
var akten = new[] { Akte(EinsId, 1, TimeSpan.FromHours(2)) };
|
|
|
|
Assert.That(Weg(akten, bekannt: new Dictionary<Guid, long>()), Has.Count.EqualTo(1),
|
|
"der Titel ist aus der Mediathek verschwunden, das Paket ist wertlos");
|
|
}
|
|
|
|
[Test]
|
|
public void Ein_veralteter_Zeitstempel_im_Namen_gilt_als_verwaist()
|
|
{
|
|
var akten = new[] { Akte(EinsId, 1, TimeSpan.FromHours(2), geaendertMs: 999) };
|
|
|
|
Assert.That(Weg(akten), Has.Count.EqualTo(1),
|
|
"die Quelle wurde ersetzt; dieses Paket gehört zum alten Stand");
|
|
}
|
|
|
|
[Test]
|
|
public void Eine_andere_Bauart_gilt_als_verwaist()
|
|
{
|
|
var akten = new[] { Akte(EinsId, 1, TimeSpan.FromHours(2), bauart: 1) };
|
|
|
|
Assert.That(Weg(akten), Has.Count.EqualTo(1),
|
|
"so gebaut würde der Server es heute nicht mehr ausliefern");
|
|
}
|
|
|
|
[Test]
|
|
public void Ein_unleserlicher_Name_gilt_als_verwaist()
|
|
{
|
|
var akten = new[]
|
|
{
|
|
new Paketakte("/kratz/packages/irgendwas.mp4", Gigabyte, Jetzt, Paketname.Lesen("irgendwas.mp4")),
|
|
};
|
|
|
|
Assert.That(Weg(akten), Has.Count.EqualTo(1));
|
|
}
|
|
|
|
/// <summary>Die Schonfrist schützt auch Verwaiste nicht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Sie schützt das, was gleich gebraucht wird. Eine Datei, die niemand
|
|
/// mehr ausliefern kann, wird nicht gleich gebraucht — sie frisch
|
|
/// liegenzulassen hiesse, auf etwas zu warten, das nicht kommt.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_frische_Verwaiste_geht_trotzdem()
|
|
{
|
|
var akten = new[] { Akte(EinsId, 1, TimeSpan.Zero, bauart: 1) };
|
|
|
|
Assert.That(Weg(akten), Has.Count.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void Unter_der_Obergrenze_bleibt_alles_liegen()
|
|
{
|
|
var akten = new[]
|
|
{
|
|
Akte(EinsId, 5, TimeSpan.FromHours(2)),
|
|
Akte(ZweiId, 5, TimeSpan.FromHours(3)),
|
|
};
|
|
|
|
Assert.That(Weg(akten), Is.Empty);
|
|
}
|
|
|
|
/// <summary>Über der Obergrenze geht das am längsten Ungenutzte zuerst.</summary>
|
|
[Test]
|
|
public void Ueber_der_Obergrenze_geht_das_aelteste_zuerst()
|
|
{
|
|
var jung = Akte(EinsId, 15, TimeSpan.FromHours(2));
|
|
var alt = Akte(ZweiId, 15, TimeSpan.FromHours(20));
|
|
|
|
var weg = Weg([jung, alt]);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(weg, Has.Count.EqualTo(1), "eines von beiden reicht, um unter 20 GB zu kommen");
|
|
Assert.That(weg[0], Is.EqualTo(alt.Pfad));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void Was_in_der_Schonfrist_liegt_bleibt_auch_ueber_der_Obergrenze()
|
|
{
|
|
var eben = Akte(EinsId, 15, TimeSpan.FromMinutes(1));
|
|
var auchEben = Akte(ZweiId, 15, TimeSpan.FromMinutes(2));
|
|
|
|
Assert.That(Weg([eben, auchEben]), Is.Empty,
|
|
"beide wurden gerade gebraucht — wegräumen hiesse, jemandem den Download abzuschneiden");
|
|
}
|
|
|
|
[Test]
|
|
public void Was_in_Benutzung_ist_bleibt_auch_ueber_der_Obergrenze()
|
|
{
|
|
var laeuft = Akte(EinsId, 15, TimeSpan.FromDays(2));
|
|
var daneben = Akte(ZweiId, 15, TimeSpan.FromHours(2));
|
|
|
|
var weg = Weg([laeuft, daneben], inBenutzung: p => p == laeuft.Pfad);
|
|
|
|
Assert.That(weg, Does.Not.Contain(laeuft.Pfad),
|
|
"ein Download über Mobilfunk kann Stunden dauern, ohne dass eine Anfrage kommt");
|
|
}
|
|
|
|
/// <summary>Unbenutztes geht auch, wenn noch Platz wäre.</summary>
|
|
[Test]
|
|
public void Lange_Unbenutzte_gehen_auch_unter_der_Obergrenze()
|
|
{
|
|
var akten = new[] { Akte(EinsId, 1, TimeSpan.FromDays(30)) };
|
|
|
|
Assert.That(Weg(akten), Has.Count.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void Eine_Obergrenze_von_null_heisst_unbegrenzt()
|
|
{
|
|
var regeln = Transcoderegeln.Vorgabe with { ObergrenzeBytes = 0 };
|
|
var akten = new[]
|
|
{
|
|
Akte(EinsId, 500, TimeSpan.FromHours(2)),
|
|
Akte(ZweiId, 500, TimeSpan.FromHours(3)),
|
|
};
|
|
|
|
Assert.That(Weg(akten, regeln), Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void Ohne_Altersabbau_bleibt_auch_sehr_Altes_liegen()
|
|
{
|
|
var regeln = Transcoderegeln.Vorgabe with { Unbenutzt = TimeSpan.Zero };
|
|
|
|
Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromDays(400))], regeln), Is.Empty);
|
|
}
|
|
|
|
/// <summary>Auch mit abgeschalteten Regeln fliegt Verwaistes.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// „Unbegrenzt" heisst „so viel du willst", nicht „sammle auch Müll".
|
|
/// </remarks>
|
|
[Test]
|
|
public void Verwaistes_geht_auch_bei_abgeschalteten_Grenzen()
|
|
{
|
|
var regeln = new Transcoderegeln(0, TimeSpan.Zero, TimeSpan.FromMinutes(30));
|
|
|
|
Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromHours(2), bauart: 1)], regeln), Has.Count.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void Eine_zu_kurze_Schonfrist_wird_hochgesetzt()
|
|
{
|
|
var regeln = Transcoderegeln.Geprueft(20, 7, 1);
|
|
|
|
Assert.That(regeln.Schonfrist, Is.EqualTo(TimeSpan.FromMinutes(5)),
|
|
"eine Minute löschte die Datei mitten im Herunterladen weg");
|
|
}
|
|
|
|
[Test]
|
|
public void Null_und_negative_Werte_heissen_abgeschaltet()
|
|
{
|
|
var regeln = Transcoderegeln.Geprueft(0, -3, 30);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(regeln.ObergrenzeBytes, Is.Zero);
|
|
Assert.That(regeln.Unbenutzt, Is.EqualTo(TimeSpan.Zero));
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>Der Name einer umgepackten Datei, in beide Richtungen.</summary>
|
|
public class PaketnameTests
|
|
{
|
|
[Test]
|
|
public void Ein_gebauter_Name_laesst_sich_wieder_lesen()
|
|
{
|
|
var id = Guid.CreateVersion7();
|
|
var geaendert = DateTimeOffset.FromUnixTimeMilliseconds(1_700_000_000_123);
|
|
|
|
var gelesen = Paketname.Lesen(Paketname.Bauen(id, geaendert, 2));
|
|
|
|
Assert.That(gelesen, Is.EqualTo(new Paketname.Teile(id, 1_700_000_000_123, 2)));
|
|
}
|
|
|
|
[Test]
|
|
public void Der_Pfad_davor_stoert_nicht()
|
|
{
|
|
var id = Guid.CreateVersion7();
|
|
var name = Paketname.Bauen(id, DateTimeOffset.FromUnixTimeMilliseconds(5), 2);
|
|
|
|
Assert.That(Paketname.Lesen($"/kratz/packages/{name}")?.DateiId, Is.EqualTo(id));
|
|
}
|
|
|
|
[TestCase("irgendwas.mp4")]
|
|
[TestCase("abc-123-2.mp4")]
|
|
[TestCase("00000000000000000000000000000001-123.mp4")]
|
|
[TestCase("00000000000000000000000000000001-123-2.teil")]
|
|
[TestCase("00000000000000000000000000000001-nichtzahl-2.mp4")]
|
|
public void Ein_fremder_Name_ergibt_nichts(string name) =>
|
|
Assert.That(Paketname.Lesen(name), Is.Null);
|
|
}
|