Erst fragen, ob die Datei den Abspann selbst benennt

Zweimal gemessen scheitert die Helligkeitssuche am selben: „Alice im
Wunderland" legt den Abspann über verzierte Hintergründe, „(T)Raumschiff
Surprise" zeigt darin Outtakes. Deine Zeile sagt es genau — Schwelle
49,4, letzte Probe 1, dunkle Strecke drei Sekunden: der Film endet
schwarz, und davor ist volles Bild. Helligkeit kann dazu nichts sagen,
und das ist kein Fehler, sondern ihre Grenze.

Ein Kapitel sagt es dagegen genau. Die Suche fragt jetzt zuerst danach:
ffprobe liest dafür nur den Kopf der Datei, Millisekunden gegen zwanzig
Minuten Schlüsselbilder. Findet sie ein benanntes Kapitel am Ende, ist
sie fertig; sonst geht es wie bisher über das Bild.

**Nur bei einem Namen, der es hergibt** — „Chapter 12" ist keine
Auskunft, und das letzte Kapitel ist nicht automatisch der Abspann:
bei vielen Dateien ist es die Schlussszene. Lieber kein Fund als ein
Knopf, der die letzten zehn Minuten Handlung überspringt. „Opening
Credits" bei Minute zwei trägt dasselbe Wort und ist das Gegenteil,
also zählt nur, was im letzten Drittel liegt.

**Und wo nichts gefunden wird, steht jetzt die Kapitellage dabei** —
„Keine Kapitel.", „14 Kapitel, alle ohne Namen." oder „12 Kapitel,
letztes „Finale"." Ob sich an diesem Weg weiterbauen lohnt, sieht man
erst an vielen Dateien, und genau das misst diese Zeile.

Elf Prüfungen: das Lesen der ffprobe-Antwort ohne ffprobe, und die
Regel, wann ein Kapitel zählt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-15 14:48:22 +02:00
co-authored by Claude Opus 5
parent f79ecf2757
commit 121ee62be9
6 changed files with 400 additions and 9 deletions
@@ -0,0 +1,106 @@
using Flaro.Core.Media;
namespace Flaro.Core.Tests;
/// <summary>Wann ein Kapitel der Abspann ist — und wann nicht.</summary>
///
/// <remarks>
/// <b>Der Anlass.</b> Die Helligkeitssuche findet, was dunkel ist, und
/// scheitert zweimal gemessen an genau dem Gegenteil: „Alice im
/// Wunderland" legt den Abspann über verzierte Hintergründe,
/// „(T)Raumschiff Surprise" zeigt darin Outtakes — Schwelle 49,4, letzte
/// Probe 1, und die dunkle Strecke war drei Sekunden lang. Ein Kapitel
/// sagt es genau, wenn die Datei eines hat.
/// </remarks>
public class AbspannkapitelTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
private const double Laufzeit = 7200;
[Test]
public void Ein_benanntes_Kapitel_am_Ende_zaehlt()
{
var kapitel = new List<Kapitel>
{
new(0, 600, "Opening"),
new(600, 6600, "Der Film"),
new(6600, 7200, "End Credits"),
};
Assert.That(Abspannkapitel.Suchen(kapitel, Laufzeit, Regeln), Is.EqualTo(6600));
}
/// <summary>Deutsch genauso — beides kommt in derselben Sammlung vor.</summary>
[Test]
public void Abspann_auf_Deutsch_auch()
{
var kapitel = new List<Kapitel> { new(6900, 7200, "Abspann") };
Assert.That(Abspannkapitel.Suchen(kapitel, Laufzeit, Regeln), Is.EqualTo(6900));
}
/// <summary>„Chapter 12" ist keine Auskunft.</summary>
///
/// <remarks>
/// Das letzte Kapitel ist nicht automatisch der Abspann — bei vielen
/// Dateien ist es die Schlussszene. Lieber kein Fund als ein Knopf,
/// der die letzten zehn Minuten Handlung überspringt.
/// </remarks>
[Test]
public void Namenlose_Kapitel_zaehlen_nicht()
{
var kapitel = new List<Kapitel>
{
new(0, 3600, "Chapter 1"),
new(3600, 7200, "Chapter 2"),
};
Assert.That(Abspannkapitel.Suchen(kapitel, Laufzeit, Regeln), Is.Null);
}
/// <summary>„Opening Credits" trägt dasselbe Wort und ist das Gegenteil.</summary>
[Test]
public void Ein_Vorspannkapitel_zaehlt_nicht()
{
var kapitel = new List<Kapitel> { new(60, 180, "Opening Credits") };
Assert.That(Abspannkapitel.Suchen(kapitel, Laufzeit, Regeln), Is.Null);
}
/// <summary>Ein Abspannkapitel von zehn Sekunden ist keins.</summary>
[Test]
public void Zu_kurz_zaehlt_nicht()
{
var kapitel = new List<Kapitel> { new(7190, 7200, "Credits") };
Assert.That(Abspannkapitel.Suchen(kapitel, Laufzeit, Regeln), Is.Null);
}
/// <summary>Zerfällt er in zwei, gilt der erste.</summary>
[Test]
public void Der_frueheste_Treffer_gewinnt()
{
var kapitel = new List<Kapitel>
{
new(6600, 7100, "Closing Credits"),
new(7100, 7200, "Studio-Logo mit Credits"),
};
Assert.That(Abspannkapitel.Suchen(kapitel, Laufzeit, Regeln), Is.EqualTo(6600));
}
[Test]
public void Ohne_Kapitel_kein_Fund()
{
Assert.That(Abspannkapitel.Suchen([], Laufzeit, Regeln), Is.Null);
}
/// <summary>Ohne Laufzeit lässt sich „am Ende" nicht beantworten.</summary>
[Test]
public void Ohne_Laufzeit_kein_Fund()
{
var kapitel = new List<Kapitel> { new(6600, 7200, "Credits") };
Assert.That(Abspannkapitel.Suchen(kapitel, 0, Regeln), Is.Null);
}
}
@@ -0,0 +1,54 @@
using Flaro.Core.Media;
using Flaro.Media;
namespace Flaro.Media.Tests;
/// <summary>Die Antwort von <c>ffprobe -show_chapters</c> lesen — ohne ffprobe.</summary>
public class KapitelleserTests
{
[Test]
public void Kapitel_mit_Namen_und_Zeiten()
{
var json = """
{
"chapters": [
{ "id": 0, "start_time": "0.000000", "end_time": "600.000000",
"tags": { "title": "Anfang" } },
{ "id": 1, "start_time": "6600.000000", "end_time": "7200.000000",
"tags": { "title": "End Credits" } }
]
}
""";
var kapitel = Kapitelleser.Lesen(json);
Assert.That(kapitel, Is.EqualTo(new[]
{
new Kapitel(0, 600, "Anfang"),
new Kapitel(6600, 7200, "End Credits"),
}));
}
/// <summary>Ohne <c>tags</c> bleibt der Name leer — und das ist kein Fehler.</summary>
[Test]
public void Ein_Kapitel_ohne_Namen_kommt_trotzdem_an()
{
var json = """
{ "chapters": [ { "start_time": "10.5", "end_time": "20.25" } ] }
""";
Assert.That(Kapitelleser.Lesen(json), Is.EqualTo(new[] { new Kapitel(10.5, 20.25, "") }));
}
[Test]
public void Eine_Datei_ohne_Kapitel_ergibt_nichts()
{
Assert.Multiple(() =>
{
Assert.That(Kapitelleser.Lesen("""{ "chapters": [] }"""), Is.Empty);
Assert.That(Kapitelleser.Lesen("{}"), Is.Empty);
Assert.That(Kapitelleser.Lesen(""), Is.Empty);
Assert.That(Kapitelleser.Lesen("kein JSON"), Is.Empty);
});
}
}