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,72 @@
namespace Flaro.Core.Media;
/// <summary>Ein Kapitel, wie es in der Datei steht.</summary>
///
/// <param name="Von">Anfang in Sekunden.</param>
/// <param name="Bis">Ende in Sekunden.</param>
/// <param name="Titel">Was draufsteht — oft leer oder „Chapter 12".</param>
public readonly record struct Kapitel(double Von, double Bis, string Titel);
/// <summary>Findet den Abspann, wenn die Datei ihn selbst benennt.</summary>
///
/// <remarks>
/// <para>
/// <b>Warum neben der Helligkeit.</b> <see cref="Abspannkante"/> findet,
/// was dunkel ist — und daran scheitert sie zweimal gemessen: „Alice im
/// Wunderland" legt den Abspann über verzierte Hintergründe, „(T)Raumschiff
/// Surprise" zeigt darin Outtakes. Beides ist volles Bild. Ein Kapitel
/// sagt es dagegen genau, kostet nichts und ist nicht geraten.
/// </para>
/// <para>
/// <b>Nur, wenn der Name es hergibt.</b> „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.
/// </para>
/// </remarks>
public static class Abspannkapitel
{
/// <summary>Woran ein Abspannkapitel zu erkennen ist.</summary>
///
/// <remarks>
/// Englisch und deutsch, weil beides vorkommt — oft in derselben
/// Sammlung. „Ende" steht bewusst nicht dabei: „Das Ende" ist bei
/// einem Film die Schlussszene, nicht der Abspann.
/// </remarks>
private static readonly string[] Woerter =
[
"abspann", "nachspann", "credit", "closing", "outro", "schlusstitel",
];
/// <summary>Ab welchem Anteil der Laufzeit ein Kapitel in Frage kommt.</summary>
///
/// <remarks>
/// Ein Abspann liegt am Ende. Ein Kapitel namens „Opening Credits" bei
/// Minute zwei trägt dasselbe Wort und ist das Gegenteil.
/// </remarks>
private const double Spaetestens = 0.6;
/// <summary>Wo der Abspann laut Datei anfängt — oder <c>null</c>.</summary>
public static double? Suchen(
IReadOnlyList<Kapitel> kapitel, double laufzeit, Abspannregeln regeln)
{
if (laufzeit <= 0) return null;
var frühestens = laufzeit * Spaetestens;
return kapitel
.Where(k => k.Von >= frühestens)
.Where(k => laufzeit - k.Von >= regeln.Mindestlänge)
.Where(k => Passt(k.Titel))
// Der früheste Treffer: ein Abspann, der in „Credits" und
// „Studio-Logo" zerfällt, fängt beim ersten an.
.OrderBy(k => k.Von)
.Select(k => (double?)k.Von)
.FirstOrDefault();
}
private static bool Passt(string titel) =>
!string.IsNullOrWhiteSpace(titel)
&& Array.Exists(Woerter,
w => titel.Contains(w, StringComparison.OrdinalIgnoreCase));
}