From f2752e6c05fc2f9bd4e1eb64110772b1fe26aa28 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 15 Oct 2026 14:06:16 +0200 Subject: [PATCH] Ein doppelter Platzhalter riss jede Serie mit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- server/Directory.Build.props | 23 ++++++ server/src/Flaro.Api/Markenerkennung.cs | 34 +++++--- server/src/Flaro.Core/Media/Abspannkante.cs | 9 ++- server/src/Flaro.Media/Bildabtaster.cs | 14 +++- .../Flaro.Media.Tests/BildabtasterTests.cs | 79 +++++++++++++++++++ 5 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 server/Directory.Build.props diff --git a/server/Directory.Build.props b/server/Directory.Build.props new file mode 100644 index 0000000..3a171ee --- /dev/null +++ b/server/Directory.Build.props @@ -0,0 +1,23 @@ + + + + + $(WarningsAsErrors);CA2017 + + + diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index b281f48..998ebff 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -510,7 +510,11 @@ public sealed class Markenerkennung( log.LogInformation("„{Serie}“: {Staffeln} Staffel(n), {Folgen} Folge(n).", 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; foreach (var staffel in staffeln) @@ -595,9 +599,10 @@ public sealed class Markenerkennung( // Ende an; ohne diesen Aufschlag stünde der Knopf bei // Minute zwei. var versatz = fensteranfang.GetValueOrDefault(folgenId); - if (await SchreibenAsync(db, folgenId, istFilm: false, art, - stelle.Von + versatz, stelle.Bis + versatz, ct)) - gefunden++; + if (!await SchreibenAsync(db, folgenId, istFilm: false, art, + stelle.Von + versatz, stelle.Bis + versatz, ct)) continue; + + if (art == Markenart.Intro) gefundenIntro++; else gefundenAbspann++; } } @@ -644,7 +649,8 @@ public sealed class Markenerkennung( Name(i), folge.Path, dauer.TotalSeconds, ct)) { ueberBild.Add(folge.Id); - gefunden++; + gefundenAbspann++; + bildfunde++; } } } @@ -660,7 +666,10 @@ public sealed class Markenerkennung( 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 { @@ -668,7 +677,7 @@ public sealed class Markenerkennung( serienname); } - return new Serienstand(gefunden, angefasst); + return new Serienstand(gefundenIntro + gefundenAbspann, angefasst); } /// Was eine Staffel ergeben hat — erst zusammengefasst, dann je Folge. @@ -694,9 +703,14 @@ public sealed class Markenerkennung( var mitAbspann = marken.Count(m => m.Art == Markenart.Abspann); log.LogInformation( - "„{Serie}“, Staffel {Staffel}: Vorspann in {Intro} von {Folgen} Folge(n), " - + "Abspann in {Abspann} von {Folgen}.", - serienname, staffel, mitIntro, folgen.Count, mitAbspann); + // **Jeder Platzhalter verbraucht ein Argument, auch ein + // wiederholter.** Hier stand `{Folgen}` zweimal und wurde + // 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) { diff --git a/server/src/Flaro.Core/Media/Abspannkante.cs b/server/src/Flaro.Core/Media/Abspannkante.cs index 4e8faf2..36c32c7 100644 --- a/server/src/Flaro.Core/Media/Abspannkante.cs +++ b/server/src/Flaro.Core/Media/Abspannkante.cs @@ -25,6 +25,13 @@ public readonly record struct Abspannregeln /// realistischer Schriftdichte 32, bei einem dichten Abspann 47. Die /// Fünfunddreissig lag also mitten zwischen den Fällen und schnitt /// sie auseinander. + /// + /// + /// Und seitdem im vollen Wertebereich. 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. /// public double Dunkelschwelle { get; init; } @@ -83,7 +90,7 @@ public readonly record struct Abspannregeln public static Abspannregeln Vorgabe => new() { - Dunkelschwelle = 35, + Dunkelschwelle = 22, Anteil = 0.55, Nachsicht = 10, Schlusstafel = 20, diff --git a/server/src/Flaro.Media/Bildabtaster.cs b/server/src/Flaro.Media/Bildabtaster.cs index dd2e9ec..b6d989a 100644 --- a/server/src/Flaro.Media/Bildabtaster.cs +++ b/server/src/Flaro.Media/Bildabtaster.cs @@ -109,8 +109,20 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILoggerDieselbe Helligkeit, gleich welche Bittiefe die Datei hat. +/// +/// +/// +/// Der Fehler, für den diese Prüfung steht. signalstats +/// 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. +/// +/// +/// 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. +/// +/// +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.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"); + }); + } +}