Ein doppelter Platzhalter riss jede Serie mit

`log.LogInformation("… {Folgen} … {Folgen}.", a, b, c, d, e)` — sechs
Platzhalter, fünf Argumente. Vorlagen sind positionsbezogen: auch ein
wiederholter Name verbraucht ein weiteres Argument. Der Logger warf eine
Ausnahme, die riss `StaffelMeldenAsync` mit, und die wiederum den ganzen
Serienlauf. Gefunden war dann alles umsonst — „A Knight of the Seven
Kingdoms" und „Alice in Borderland" haben ihre Marken deshalb verloren,
nachdem sie längst berechnet waren.

**CA2017 hatte das gemeldet.** Als Warnung, unter hundert anderen Zeilen,
und ich habe beim Bauen nach „warning CS" gegriffen. Eine neue
`Directory.Build.props` macht genau diese eine Regel zum Fehler — nicht
alle Warnungen, das wäre eine andere Entscheidung. Gegengeprüft: mit der
kaputten Zeile bricht der Bau jetzt ab.

**Und der zweite Fund, aus „Alice im Wunderland".** Die Zeile meldete
„Schwelle 247,6", und das kann bei 0–255 nicht entstehen: die Schwelle
ist Grundton mal 0,55, das wären 450 gewesen. `signalstats` rechnet in
der Skala des Bildformats — bei acht Bit 0–255, bei zehn Bit 0–1023.
Gemessen am selben erzeugten Bild: YAVG 127 gegen YAVG 500. Alle
Schwellen hier sind an acht Bit gemessen; bei einer 10-Bit-Datei bedeutete
dieselbe Zahl ein Viertel.

`format=gray` vor `signalstats` bringt jede Datei auf dieselbe Skala. Es
rechnet dabei auch den Fernsehbereich 16–235 auf 0–255 um — linear, und
die dunklen Werte trifft das am stärksten: der gemessene Abspann liegt
jetzt bei 28 statt bei 40. Der Boden wandert entsprechend von 35 auf 22.

Dazu: die Schlusszeile je Serie trennt Vorspann und Abspann, statt eine
Zahl aus beidem zu machen — „14 Marken" beantwortet nicht, ob die Serie
ihre Vorspänne bekommen hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-15 14:06:16 +02:00
co-authored by Claude Opus 5
parent 259996c5ad
commit f2752e6c05
5 changed files with 147 additions and 12 deletions
+23
View File
@@ -0,0 +1,23 @@
<Project>
<!--
Eine kaputte Protokollvorlage ist kein Schönheitsfehler.
`log.LogInformation("… {Folgen} … {Folgen}.", a, b, c, d, e)` wirft zur
Laufzeit — jeder Platzhalter verbraucht ein Argument, auch ein
wiederholter. Geschieht das in einem Hintergrundlauf, reisst die
Ausnahme den ganzen Lauf mit, und alles, was er bis dahin gefunden
hatte, ist verloren. Genau so ist es der Markenerkennung ergangen.
CA2017 hatte das längst gemeldet — als Warnung unter hundert anderen
Zeilen, und damit übersehen. Als Fehler ist es nicht zu übersehen.
Nur diese eine Regel, nicht alle Warnungen: ein pauschales
`TreatWarningsAsErrors` wäre eine andere Entscheidung und gehört nicht
nebenbei getroffen.
-->
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);CA2017</WarningsAsErrors>
</PropertyGroup>
</Project>
+24 -10
View File
@@ -510,7 +510,11 @@ public sealed class Markenerkennung(
log.LogInformation("„{Serie}“: {Staffeln} Staffel(n), {Folgen} Folge(n).", log.LogInformation("„{Serie}“: {Staffeln} Staffel(n), {Folgen} Folge(n).",
serienname, staffeln.Count, folgen.Count); serienname, staffeln.Count, folgen.Count);
var gefunden = 0; // Getrennt gezählt: „14 Marken" beantwortet nicht, ob diese Serie
// ihre Vorspänne bekommen hat — und das ist die häufigere Frage.
var gefundenIntro = 0;
var gefundenAbspann = 0;
var bildfunde = 0;
var angefasst = false; var angefasst = false;
foreach (var staffel in staffeln) foreach (var staffel in staffeln)
@@ -595,9 +599,10 @@ public sealed class Markenerkennung(
// Ende an; ohne diesen Aufschlag stünde der Knopf bei // Ende an; ohne diesen Aufschlag stünde der Knopf bei
// Minute zwei. // Minute zwei.
var versatz = fensteranfang.GetValueOrDefault(folgenId); var versatz = fensteranfang.GetValueOrDefault(folgenId);
if (await SchreibenAsync(db, folgenId, istFilm: false, art, if (!await SchreibenAsync(db, folgenId, istFilm: false, art,
stelle.Von + versatz, stelle.Bis + versatz, ct)) stelle.Von + versatz, stelle.Bis + versatz, ct)) continue;
gefunden++;
if (art == Markenart.Intro) gefundenIntro++; else gefundenAbspann++;
} }
} }
@@ -644,7 +649,8 @@ public sealed class Markenerkennung(
Name(i), folge.Path, dauer.TotalSeconds, ct)) Name(i), folge.Path, dauer.TotalSeconds, ct))
{ {
ueberBild.Add(folge.Id); ueberBild.Add(folge.Id);
gefunden++; gefundenAbspann++;
bildfunde++;
} }
} }
} }
@@ -660,7 +666,10 @@ public sealed class Markenerkennung(
if (angefasst) if (angefasst)
{ {
log.LogInformation("„{Serie}“ fertig: {Anzahl} Marke(n).", serienname, gefunden); log.LogInformation(
"„{Serie}“ fertig: {Intro} Vorspann, {Abspann} Abspann{Bild}.",
serienname, gefundenIntro, gefundenAbspann,
bildfunde > 0 ? $", davon {bildfunde} über das Bild" : "");
} }
else else
{ {
@@ -668,7 +677,7 @@ public sealed class Markenerkennung(
serienname); serienname);
} }
return new Serienstand(gefunden, angefasst); return new Serienstand(gefundenIntro + gefundenAbspann, angefasst);
} }
/// <summary>Was eine Staffel ergeben hat — erst zusammengefasst, dann je Folge.</summary> /// <summary>Was eine Staffel ergeben hat — erst zusammengefasst, dann je Folge.</summary>
@@ -694,9 +703,14 @@ public sealed class Markenerkennung(
var mitAbspann = marken.Count(m => m.Art == Markenart.Abspann); var mitAbspann = marken.Count(m => m.Art == Markenart.Abspann);
log.LogInformation( log.LogInformation(
"„{Serie}“, Staffel {Staffel}: Vorspann in {Intro} von {Folgen} Folge(n), " // **Jeder Platzhalter verbraucht ein Argument, auch ein
+ "Abspann in {Abspann} von {Folgen}.", // wiederholter.** Hier stand `{Folgen}` zweimal und wurde
serienname, staffel, mitIntro, folgen.Count, mitAbspann); // einmal bedient; der Logger warf eine Ausnahme, und die riss
// den ganzen Serienlauf mit — gefunden war dann alles umsonst.
// CA2017 meldet das jetzt als Fehler, siehe Directory.Build.props.
"„{Serie}“, Staffel {Staffel}: {Intro} Vorspann und {Abspann} Abspann "
+ "in {Folgen} Folge(n).",
serienname, staffel, mitIntro, mitAbspann, folgen.Count);
foreach (var (id, name) in folgen) foreach (var (id, name) in folgen)
{ {
+8 -1
View File
@@ -25,6 +25,13 @@ public readonly record struct Abspannregeln
/// realistischer Schriftdichte 32, bei einem dichten Abspann 47. Die /// realistischer Schriftdichte 32, bei einem dichten Abspann 47. Die
/// Fünfunddreissig lag also mitten zwischen den Fällen und schnitt /// Fünfunddreissig lag also mitten zwischen den Fällen und schnitt
/// sie auseinander. /// sie auseinander.
/// </para>
/// <para>
/// <b>Und seitdem im vollen Wertebereich.</b> Der Abtaster rechnet
/// jede Datei auf 0–255 um, gleich ob sie acht oder zehn Bit hat und
/// gleich ob sie den Fernsehbereich 16–235 benutzt. Dieselben
/// gemessenen Abspänne liegen darin bei 19 und 36 statt bei 32 und 47;
/// der Boden wandert entsprechend von 35 auf 22.
/// </remarks> /// </remarks>
public double Dunkelschwelle { get; init; } public double Dunkelschwelle { get; init; }
@@ -83,7 +90,7 @@ public readonly record struct Abspannregeln
public static Abspannregeln Vorgabe => new() public static Abspannregeln Vorgabe => new()
{ {
Dunkelschwelle = 35, Dunkelschwelle = 22,
Anteil = 0.55, Anteil = 0.55,
Nachsicht = 10, Nachsicht = 10,
Schlusstafel = 20, Schlusstafel = 20,
+13 -1
View File
@@ -109,8 +109,20 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger<Bildabt
start.ArgumentList.Add("-an"); start.ArgumentList.Add("-an");
start.ArgumentList.Add("-sn"); start.ArgumentList.Add("-sn");
start.ArgumentList.Add("-vf"); start.ArgumentList.Add("-vf");
// **`format=gray` vor `signalstats`, und das ist nicht kosmetisch.**
//
// `signalstats` rechnet in der Skala des Bildformats: bei acht Bit
// 0–255, bei zehn Bit 0–1023. Gemessen am selben erzeugten Bild
// einmal als 8-Bit und einmal als 10-Bit: YAVG 127 gegen YAVG 500.
// Alle Schwellen hier sind an acht Bit gemessen; bei einer
// 10-Bit-Datei bedeutete `Dunkelschwelle = 35` plötzlich ein
// Viertel davon, und im Protokoll standen Zahlen, die sich mit
// denen anderer Dateien nicht vergleichen liessen — „Schwelle
// 247,6" war der Hinweis, dem das hier zu verdanken ist.
start.ArgumentList.Add( start.ArgumentList.Add(
$"scale={Breite}:-2,signalstats,metadata=print:key=lavfi.signalstats.YAVG:file=-"); $"scale={Breite}:-2,format=gray,signalstats,"
+ "metadata=print:key=lavfi.signalstats.YAVG:file=-");
start.ArgumentList.Add("-f"); start.ArgumentList.Add("-f");
start.ArgumentList.Add("null"); start.ArgumentList.Add("null");
start.ArgumentList.Add("-"); start.ArgumentList.Add("-");
@@ -184,3 +184,82 @@ public class BildabtasterLaufTests
}); });
} }
} }
/// <summary>Dieselbe Helligkeit, gleich welche Bittiefe die Datei hat.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Fehler, für den diese Prüfung steht.</b> <c>signalstats</c>
/// rechnet in der Skala des Bildformats: bei acht Bit 0–255, bei zehn Bit
/// 0–1023. Alle Schwellen der Abspannsuche sind an acht Bit gemessen —
/// bei einer 10-Bit-Datei bedeutete dieselbe Zahl ein Viertel.
/// </para>
/// <para>
/// Aufgefallen ist es an einer Protokollzeile: „Schwelle 247,6". Die kann
/// bei 0–255 nicht entstehen, denn sie ist Grundton mal 0,55 — das wären
/// 450 gewesen.
/// </para>
/// </remarks>
public class BildabtasterBittiefeTests
{
private string _ordner = "";
[OneTimeSetUp]
public void Erzeugen() =>
_ordner = Directory.CreateDirectory(Path.Combine(
Path.GetTempPath(), "flaro-bittiefe-" + Guid.CreateVersion7().ToString("N"))).FullName;
[OneTimeTearDown]
public void Aufraeumen()
{
try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { }
}
private string Erzeugen(string pixelformat)
{
var datei = Path.Combine(_ordner, $"{pixelformat}.mp4");
var start = new ProcessStartInfo("ffmpeg")
{
ArgumentList =
{
"-hide_banner", "-loglevel", "error", "-y",
"-f", "lavfi", "-i", "testsrc2=size=320x180:rate=24:duration=4",
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", pixelformat, datei,
},
RedirectStandardError = true,
UseShellExecute = false,
};
try
{
using var prozess = Process.Start(start);
prozess!.WaitForExit(60_000);
if (prozess.ExitCode != 0 || !File.Exists(datei))
Assert.Ignore($"ffmpeg kann {pixelformat} hier nicht erzeugen.");
}
catch (Exception)
{
Assert.Ignore("Ohne ffmpeg lässt sich das nicht prüfen.");
}
return datei;
}
[Test]
public async Task Acht_und_zehn_Bit_ergeben_dieselbe_Zahl()
{
var abtaster = new Bildabtaster(
FfmpegOptions.FromEnvironment(), NullLogger<Bildabtaster>.Instance);
var acht = await abtaster.HelligkeitAsync(Erzeugen("yuv420p"), bei: 2);
var zehn = await abtaster.HelligkeitAsync(Erzeugen("yuv420p10le"), bei: 2);
Assert.Multiple(() =>
{
Assert.That(acht, Is.Not.Null);
Assert.That(zehn, Is.Not.Null.And.EqualTo(acht!.Value).Within(3),
"ohne `format=gray` stünde hier das Vierfache");
Assert.That(zehn, Is.LessThan(256), "die Skala muss 0–255 sein");
});
}
}