„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>
562 lines
21 KiB
C#
562 lines
21 KiB
C#
using Flaro.Core.Media;
|
|
|
|
namespace Flaro.Core.Tests;
|
|
|
|
/// <summary>Wo der Abspann eines Films anfängt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Mit erfundenen Messreihen statt mit Filmen.</b> Geprüft wird das
|
|
/// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen sind der Anteil
|
|
/// dunkler Pixel je Bild und stammen aus einer Messung an erzeugtem
|
|
/// 1080p-Material: Handlung 0 %, Abspann 84 bis 87 %, dunkle Nachtszene
|
|
/// 37 %.
|
|
/// </remarks>
|
|
public class AbspannkanteTests
|
|
{
|
|
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
|
|
|
|
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
|
|
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
|
|
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
|
|
/// </remarks>
|
|
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
|
|
stücke.Count == 0 ? null : stücke[0].Von;
|
|
|
|
/// <summary>Eine Reihe von Messpunkten im Abstand von zehn Sekunden.</summary>
|
|
///
|
|
/// <param name="abspannAb">Ab wann dunkel. <c>null</c> heisst: nie.</param>
|
|
private static List<Frameprobe> Reihe(double von, double bis, double? abspannAb)
|
|
{
|
|
var proben = new List<Frameprobe>();
|
|
for (var t = von; t <= bis; t += 10)
|
|
proben.Add(new Frameprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0));
|
|
return proben;
|
|
}
|
|
|
|
[Test]
|
|
public void Eine_dunkle_Strecke_bis_zum_Ende_ist_der_Abspann()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(420));
|
|
}
|
|
|
|
/// <summary>Ein Film, der hell endet, hat keinen erkennbaren Abspann.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Namen über laufendem Bild — Marvel, Pixar, jede zweite Komödie. Kein
|
|
/// Fund ist hier die richtige Antwort: ein geratener Knopf schnitte
|
|
/// Handlung weg.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Ohne_dunkles_Ende_kein_Fund()
|
|
{
|
|
Assert.That(Anfang(Abspannkante.Suchen(Reihe(0, 600, abspannAb: null), Regeln)), Is.Null);
|
|
}
|
|
|
|
/// <summary>Eine Schwarzblende mitten im Film zählt nicht.</summary>
|
|
[Test]
|
|
public void Eine_Schwarzblende_in_der_Mitte_zaehlt_nicht()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: null);
|
|
for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Dunkelanteil = 92 };
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null,
|
|
"sie hält nicht bis zum Ende durch");
|
|
}
|
|
|
|
/// <summary>Eine Szene nach dem Abspann nimmt den Fund weg.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Lieber kein Segment als eine, die über die Szene hinwegspringt.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
for (var i = proben.Count - 5; i < proben.Count; i++)
|
|
proben[i] = proben[i] with { Dunkelanteil = 0 };
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
|
|
}
|
|
|
|
/// <summary>Eine helle Schrifttafel mittendrin bricht die Strecke nicht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eine Probe, also zehn Sekunden.</b> Hier standen zwei, und das
|
|
/// war unter der alten Zählweise gedacht: nachgesehen wurde, wie viele
|
|
/// <i>Proben</i> am Stück hell sind, nicht wie lange. Zwanzig Sekunden
|
|
/// helles Bild sind keine Schrifttafel mehr, sondern eine Szene — dass
|
|
/// die Suche dort abbricht, ist richtig und steht in
|
|
/// <c>AbspannNachsichtTests</c>.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_kurze_helle_Tafel_bricht_nichts()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
proben[50] = proben[50] with { Dunkelanteil = 0 };
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(420));
|
|
}
|
|
|
|
/// <summary>Zwanzig Sekunden Dunkelheit sind kein Abspann.</summary>
|
|
[Test]
|
|
public void Zu_kurz_zaehlt_nicht()
|
|
{
|
|
Assert.That(Anfang(Abspannkante.Suchen(Reihe(0, 600, abspannAb: 580), Regeln)), Is.Null);
|
|
}
|
|
|
|
/// <summary>Ein halber Film im Dunkeln ist ein Fehlfund.</summary>
|
|
[Test]
|
|
public void Zu_lang_zaehlt_auch_nicht()
|
|
{
|
|
Assert.That(Anfang(Abspannkante.Suchen(Reihe(0, 3600, abspannAb: 600), Regeln)), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void Die_letzte_helle_Probe_begrenzt_die_Suche()
|
|
{
|
|
var proben = Reihe(0, 600, abspannAb: 420);
|
|
|
|
Assert.That(Abspannkante.HellDavor(proben, 420, Regeln), Is.EqualTo(410));
|
|
}
|
|
|
|
// ---------- Nachmessen ----------
|
|
|
|
/// <summary>Die binäre Suche findet die Kante auf vier Sekunden genau.</summary>
|
|
[Test]
|
|
public async Task Die_Kante_wird_eingegrenzt()
|
|
{
|
|
const double echteKante = 417.3;
|
|
var sonden = 0;
|
|
|
|
var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln.Mindestanteil, stelle =>
|
|
{
|
|
sonden++;
|
|
return Task.FromResult<double?>(stelle >= echteKante ? 85 : 0);
|
|
});
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(kante, Is.EqualTo(echteKante).Within(4));
|
|
Assert.That(kante, Is.GreaterThanOrEqualTo(echteKante),
|
|
"lieber ein paar Sekunden Abspann zu viel als eine Sekunde Handlung");
|
|
Assert.That(sonden, Is.LessThanOrEqualTo(4), "zehn Sekunden auf vier: zwei Halbierungen");
|
|
});
|
|
}
|
|
|
|
/// <summary>Antwortet eine Sonde nicht, gilt der grobe Fund.</summary>
|
|
[Test]
|
|
public async Task Ohne_Antwort_bleibt_es_beim_groben_Fund()
|
|
{
|
|
var kante = await Abspannkante.VerfeinernAsync(
|
|
410, 420, Regeln.Mindestanteil, _ => Task.FromResult<double?>(null));
|
|
|
|
Assert.That(kante, Is.EqualTo(420));
|
|
}
|
|
|
|
/// <summary>Ein schon enger Bereich braucht keine Sonde.</summary>
|
|
[Test]
|
|
public async Task Ein_enger_Bereich_wird_nicht_angefasst()
|
|
{
|
|
var sonden = 0;
|
|
var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln.Mindestanteil, _ =>
|
|
{
|
|
sonden++;
|
|
return Task.FromResult<double?>(85);
|
|
});
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(kante, Is.EqualTo(420));
|
|
Assert.That(sonden, Is.Zero);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>Der Anteil dunkler Pixel, an gemessenen Werten.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Wofür diese Prüfungen stehen.</b> Gemessen wurde lange der
|
|
/// Mittelwert der Helligkeit, und daran scheiterte die Erkennung
|
|
/// wiederholt: derselbe Abspann misst 19, wenn wenig Schrift darin
|
|
/// steht, und 37, wenn viel darin steht — die Schwelle musste zwischen
|
|
/// diesen Wert und die Handlung passen, und der Abstand schrumpfte mit
|
|
/// jedem Namen. Zweimal nachgezogen, zweimal wieder getroffen.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Der Anteil dunkler Pixel kennt dieses Problem nicht.</b> Schrift
|
|
/// belegt ein bis zehn Prozent der Fläche; der Rest bleibt schwarz.
|
|
/// Nachgemessen an erzeugtem 1080p-Material mit zwei Spalten Namen,
|
|
/// durch dieselbe ffmpeg-Kette wie im Betrieb
|
|
/// (<c>format=gray,blackframe=amount=0:threshold=24</c>):
|
|
/// </para>
|
|
/// <list type="table">
|
|
/// <item><description>Handlung: <b>0 %</b></description></item>
|
|
/// <item><description>Abspann, schriftreich: <b>84 %</b></description></item>
|
|
/// <item><description>Abspann, schlank: <b>87 %</b></description></item>
|
|
/// <item><description>dunkle Nachtszene: <b>37 %</b></description></item>
|
|
/// </list>
|
|
/// <para>
|
|
/// Sechzig Prozent trennen das sauber, und zwar nach beiden Seiten.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class AbspannAnteilTests
|
|
{
|
|
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
|
|
|
|
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
|
|
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
|
|
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
|
|
/// </remarks>
|
|
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
|
|
stücke.Count == 0 ? null : stücke[0].Von;
|
|
|
|
/// <summary>Ein Suchfenster, wie es im Betrieb entsteht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den
|
|
/// Keyframesn abgetastet — also alle fünf Sekunden eine Probe.
|
|
/// </remarks>
|
|
private static List<Frameprobe> Fenster(
|
|
double abspannAb, double imAbspann, double imInhalt = 0)
|
|
{
|
|
var proben = new List<Frameprobe>();
|
|
for (var t = 1500.0; t <= 2700; t += 5)
|
|
proben.Add(new Frameprobe(t, t < abspannAb ? imInhalt : imAbspann));
|
|
return proben;
|
|
}
|
|
|
|
/// <summary>Der schriftreiche Abspann — der Fall, an dem es scheiterte.</summary>
|
|
[Test]
|
|
public void Ein_dichter_Abspann_bei_vierundachtzig_Prozent_wird_gefunden()
|
|
{
|
|
var proben = Fenster(abspannAb: 2340, imAbspann: 84);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(2340.0).Within(5));
|
|
}
|
|
|
|
[Test]
|
|
public void Ein_schlanker_Abspann_bei_siebenundachtzig_Prozent_auch()
|
|
{
|
|
var proben = Fenster(abspannAb: 2340, imAbspann: 87);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(2340.0).Within(5));
|
|
}
|
|
|
|
/// <summary>**Eine dunkle Nachtszene ist kein Abspann.**</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Mit dem Mittelwert war das die heikelste Grenze: eine Nachtszene
|
|
/// misst 28, ein schlanker Abspann 19 — zwischen diesen neun Punkten
|
|
/// musste die Schwelle liegen. Nach Pixelanteilen sind es 37 gegen 87,
|
|
/// und die Frage stellt sich nicht mehr.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_duestere_Schlussszene_ist_kein_Abspann()
|
|
{
|
|
var proben = Fenster(abspannAb: 2340, imAbspann: 37, imInhalt: 12);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
|
|
}
|
|
|
|
/// <summary>Und ein heller Film bis zum Ende bleibt Handlung.</summary>
|
|
[Test]
|
|
public void Ohne_dunkles_Ende_kein_Fund()
|
|
{
|
|
var proben = Fenster(abspannAb: double.MaxValue, imAbspann: 99);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
|
|
}
|
|
}
|
|
|
|
/// <summary>Was hinter dem Abspann noch kommt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Suche verlangte,
|
|
/// dass die allerletzte Probe dunkel ist. Hinter dem Abspann steht bei
|
|
/// vielen Produktionen aber noch eine Logotafel — Sender, Studio,
|
|
/// Förderer —, oft hell. Ein einziges weisses Schlussbild machte damit den
|
|
/// ganzen Abspann unsichtbar.
|
|
/// </remarks>
|
|
public class AbspannSchlusstafelTests
|
|
{
|
|
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
|
|
|
|
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
|
|
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
|
|
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
|
|
/// </remarks>
|
|
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
|
|
stücke.Count == 0 ? null : stücke[0].Von;
|
|
|
|
private static List<Frameprobe> Fenster(double abspannAb, double tafelAb = double.MaxValue)
|
|
{
|
|
var proben = new List<Frameprobe>();
|
|
for (var t = 1500.0; t <= 2700; t += 5)
|
|
{
|
|
var dunkel = t >= tafelAb ? 0 : t < abspannAb ? 0 : 85;
|
|
proben.Add(new Frameprobe(t, dunkel));
|
|
}
|
|
return proben;
|
|
}
|
|
|
|
/// <summary>Eine helle Tafel am Schluss verdeckt den Abspann nicht.</summary>
|
|
[Test]
|
|
public void Eine_Logotafel_am_Ende_zaehlt_nicht_dagegen()
|
|
{
|
|
// Fünfzehn Sekunden weiss hinter dem Abspann.
|
|
var proben = Fenster(abspannAb: 2340, tafelAb: 2690);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(2340).Within(5));
|
|
}
|
|
|
|
/// <summary>Eine lange helle Strecke am Ende schon.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Die Nachsicht hat eine Grenze, sonst fände die Suche hinter jeder
|
|
/// Schwarzblende einen Abspann und hinge die halbe Handlung daran.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_lange_helle_Strecke_am_Ende_bricht_ab()
|
|
{
|
|
var proben = Fenster(abspannAb: 2340, tafelAb: 2600);
|
|
|
|
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
|
|
}
|
|
}
|
|
|
|
/// <summary>Dieselbe Szene, zweierlei Keyframedichte.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Nachsicht
|
|
/// gegenüber hellen Stellen mitten im Abspann war in <i>Proben</i>
|
|
/// angegeben, nicht in Sekunden. Was das bedeutet, hängt allein daran,
|
|
/// wie dicht eine Datei ihre Keyframes setzt — und das schwankt um
|
|
/// eine Zehnerpotenz.
|
|
/// </para>
|
|
/// <para>
|
|
/// Gemessen am Bestand: „(T)Raumschiff Surprise" liefert 1318 Proben auf
|
|
/// zwanzig Minuten, also eine alle 0,9 Sekunden. Zwei Proben Nachsicht
|
|
/// waren dort 1,8 Sekunden; bei einer Datei mit einem Keyframe alle
|
|
/// zehn Sekunden waren dieselben zwei Proben zwanzig Sekunden. Ein
|
|
/// fünfsekündiges Logo mitten im Abspann zerriss die Strecke deshalb nur
|
|
/// bei der dicht abgetasteten Datei — und genau die fiel durch.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class AbspannNachsichtTests
|
|
{
|
|
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
|
|
|
|
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
|
|
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
|
|
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
|
|
/// </remarks>
|
|
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
|
|
stücke.Count == 0 ? null : stücke[0].Von;
|
|
|
|
/// <summary>Abspann ab 2340 s, darin fünf Sekunden helles Logo.</summary>
|
|
private static List<Frameprobe> Fenster(double takt)
|
|
{
|
|
var proben = new List<Frameprobe>();
|
|
for (var t = 1500.0; t <= 2700; t += takt)
|
|
{
|
|
var imLogo = t >= 2500 && t < 2505;
|
|
proben.Add(new Frameprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85));
|
|
}
|
|
return proben;
|
|
}
|
|
|
|
/// <summary>Dicht abgetastet — der Fall, der im Bestand durchfiel.</summary>
|
|
[Test]
|
|
public void Ein_Logo_zerreisst_den_Abspann_nicht_bei_dichten_Proben()
|
|
{
|
|
Assert.That(Anfang(Abspannkante.Suchen(Fenster(takt: 0.9), Regeln)),
|
|
Is.EqualTo(2340.0).Within(2));
|
|
}
|
|
|
|
/// <summary>Dünn abgetastet — derselbe Film, dieselbe Antwort.</summary>
|
|
[Test]
|
|
public void Und_auch_nicht_bei_duennen_Proben()
|
|
{
|
|
Assert.That(Anfang(Abspannkante.Suchen(Fenster(takt: 10), Regeln)),
|
|
Is.EqualTo(2340.0).Within(11));
|
|
}
|
|
|
|
/// <summary>Eine Minute helles Bild mitten im Abspann ist eine Szene.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Früher brach die Suche hier ab</b> und meldete nur das Stück
|
|
/// dahinter. Genau daran scheiterten Marvel-Filme: hinter dem Abspann
|
|
/// eine Post-Credit-Szene, davor der eigentliche Abspann — und
|
|
/// gefunden wurde nur der kurze Rest, der dann als zu kurz durchfiel.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_Minute_hell_mittendrin_teilt_den_Abspann_in_zwei_Stuecke()
|
|
{
|
|
var proben = new List<Frameprobe>();
|
|
for (var t = 1500.0; t <= 2700; t += 1)
|
|
{
|
|
// Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell.
|
|
var hell = t < 2340 || (t >= 2400 && t < 2460);
|
|
proben.Add(new Frameprobe(t, hell ? 0 : 85));
|
|
}
|
|
|
|
var stücke = Abspannkante.Suchen(proben, Regeln);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(stücke, Has.Count.EqualTo(2));
|
|
Assert.That(stücke[0].Von, Is.EqualTo(2340.0).Within(2));
|
|
Assert.That(stücke[0].Bis, Is.EqualTo(2399.0).Within(2));
|
|
Assert.That(stücke[1].Von, Is.EqualTo(2460.0).Within(2));
|
|
Assert.That(stücke[1].Bis, Is.EqualTo(2700.0).Within(2),
|
|
"das letzte Stück reicht bis ans Dateiende");
|
|
});
|
|
}
|
|
|
|
/// <summary>Und der Grund steht für das Protokoll bereit.</summary>
|
|
[Test]
|
|
public void Der_Grund_wird_benannt()
|
|
{
|
|
var hell = new List<Frameprobe>();
|
|
for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Frameprobe(t, 0));
|
|
|
|
Abspannkante.Suchen(hell, Regeln, out var grund);
|
|
Assert.That(grund, Does.Contain("keine dunkle Strecke"));
|
|
|
|
var kurz = new List<Frameprobe>();
|
|
for (var t = 1500.0; t <= 2700; t += 5)
|
|
kurz.Add(new Frameprobe(t, t < 2680 ? 0 : 85));
|
|
|
|
Abspannkante.Suchen(kurz, Regeln, out var zuKurz);
|
|
Assert.That(zuKurz, Does.Contain("zusammen nur"));
|
|
}
|
|
}
|
|
|
|
/// <summary>Ein Abspann darf aus Stücken bestehen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Wofür diese Prüfungen stehen.</b> Zwei Fälle fielen durch, beide am
|
|
/// Bestand gesehen. Bei einem Marvel-Film liegt hinter dem Abspann eine
|
|
/// Post-Credit-Szene: die Suche begann am Dateiende, fand nur den kurzen
|
|
/// Rest dahinter und verwarf ihn als zu kurz. Und ein Abspann, den eine
|
|
/// Szene in der Mitte teilt, zerfiel in zwei Reste, von denen keiner lang
|
|
/// genug war.
|
|
/// </para>
|
|
/// <para>
|
|
/// Die Antwort ist dieselbe: Stücke über die Lücke hinweg zusammenfassen
|
|
/// und die <b>Summe</b> über die Mindestlänge entscheiden lassen.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class AbspannstueckeTests
|
|
{
|
|
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
|
|
|
|
/// <summary>Eine Reihe im Sekundentakt, dunkel in den angegebenen Spannen.</summary>
|
|
private static List<Frameprobe> Reihe(double bis, params (double Von, double Bis)[] dunkel)
|
|
{
|
|
var proben = new List<Frameprobe>();
|
|
for (var t = 0.0; t <= bis; t += 1)
|
|
{
|
|
var istDunkel = Array.Exists(dunkel, d => t >= d.Von && t < d.Bis);
|
|
proben.Add(new Frameprobe(t, istDunkel ? 85 : 0));
|
|
}
|
|
return proben;
|
|
}
|
|
|
|
/// <summary>Der Marvel-Fall: Abspann, Szene, Abspann.</summary>
|
|
[Test]
|
|
public void Zwei_Stuecke_mit_einer_Szene_dazwischen()
|
|
{
|
|
// 180 s Abspann, 90 s Szene, 60 s Abspann bis zum Ende.
|
|
var stücke = Abspannkante.Suchen(Reihe(600, (330, 510), (540, 601)), Regeln);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(stücke, Has.Count.EqualTo(2));
|
|
Assert.That(stücke[0].Von, Is.EqualTo(330.0).Within(2));
|
|
Assert.That(stücke[1].Von, Is.EqualTo(540.0).Within(2));
|
|
Assert.That(stücke[1].Bis, Is.EqualTo(600.0).Within(2));
|
|
});
|
|
}
|
|
|
|
/// <summary>**Die Summe entscheidet, nicht das einzelne Stück.**</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Genau hier scheiterte es vorher: 30 s plus 30 s sind zusammen 60 s
|
|
/// und damit ein Abspann, einzeln aber zweimal zu wenig.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Zwei_kurze_Stuecke_ergeben_zusammen_einen_Abspann()
|
|
{
|
|
var stücke = Abspannkante.Suchen(Reihe(600, (450, 480), (570, 601)), Regeln);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(stücke, Has.Count.EqualTo(2));
|
|
Assert.That(stücke.Sum(t => t.Länge), Is.GreaterThanOrEqualTo(Regeln.Mindestlänge));
|
|
});
|
|
}
|
|
|
|
/// <summary>Eine zu lange Lücke trennt — dann zählt nur das letzte Stück.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Sonst liefe die Suche rückwärts durch jede Nachtszene und hinge den
|
|
/// halben Film an den Abspann. Hinter einer Lücke von fünf Minuten
|
|
/// bleibt hier ein Stück von 30 s übrig, und das ist zu kurz.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Eine_zu_lange_Luecke_trennt()
|
|
{
|
|
var stücke = Abspannkante.Suchen(Reihe(900, (200, 400), (870, 901)), Regeln);
|
|
|
|
Assert.That(stücke, Is.Empty);
|
|
}
|
|
|
|
/// <summary>Ein zu kurzes Stück zwischen zwei gültigen zählt nicht mit.</summary>
|
|
[Test]
|
|
public void Ein_Stueck_von_zehn_Sekunden_faellt_heraus()
|
|
{
|
|
// Das mittlere Stück ist 10 s lang und damit unter `Mindeststück`.
|
|
var stücke = Abspannkante.Suchen(
|
|
Reihe(600, (300, 360), (450, 460), (540, 601)), Regeln);
|
|
|
|
Assert.That(stücke, Has.Count.EqualTo(2), "das Zehnsekundenstück gehört nicht dazu");
|
|
}
|
|
|
|
/// <summary>Und ein durchgehender Abspann bleibt ein Stück.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Die wichtigste Prüfung dieser Gruppe: der Normalfall darf sich durch
|
|
/// die Stückelung nicht verändern.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Ein_durchgehender_Abspann_bleibt_ein_Stueck()
|
|
{
|
|
var stücke = Abspannkante.Suchen(Reihe(600, (420, 601)), Regeln);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(stücke, Has.Count.EqualTo(1));
|
|
Assert.That(stücke[0].Von, Is.EqualTo(420.0).Within(2));
|
|
Assert.That(stücke[0].Bis, Is.EqualTo(600.0).Within(2));
|
|
});
|
|
}
|
|
}
|