From fd79fb402264f540587de63dca583607c44f81e4 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 12 Oct 2026 22:09:56 +0200 Subject: [PATCH] =?UTF-8?q?=E2=80=9ENo=20such=20file=20or=20directory"=20?= =?UTF-8?q?=C3=BCber=20Dateien,=20die=20da=20waren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im Protokoll stand für jede einzelne Folge, fpcalc könne die Datei nicht öffnen — während der Spieler dieselbe Datei im selben Augenblick umpackte. Null Marken, und der Grund stand nicht da. `-ts` ist bei fpcalc kein Zeitstempel, sondern ein Schalter: „gib Zeitstempel aus". Die Sekundenzahl dahinter las fpcalc als zweiten Dateinamen, fand ihn nicht und brach ab, bevor es die echte Datei überhaupt ansah. Nachgemessen am Binärprogramm aus dem Abbild: einen Startpunkt gibt es bei fpcalc gar nicht. `-length` begrenzt nur die Dauer vom Anfang an — für den Abspann braucht es aber das Ende. Also einmal die ganze Spur je Folge, und Vorspann wie Abspann sind danach zwei Ausschnitte aus demselben Abdruck. Das ist nicht nur richtig, sondern billiger: eine Folge von 45 Minuten kostet 1,6 Sekunden und ergibt 21 786 Werte — gegenüber zwei Ausschnitten, die zusammen mehr gekostet hätten und nie funktionierten. Die Rate von 0,1239 Sekunden je Wert steht jetzt als Prüfung daneben. Co-Authored-By: Claude Opus 5 --- server/src/Flaro.Api/Markenerkennung.cs | 59 ++++++++++++------- server/src/Flaro.Media/Tonabdruckleser.cs | 37 +++++++++--- .../Flaro.Media.Tests/TonabdruckleserTests.cs | 38 ++++++++++++ 3 files changed, 105 insertions(+), 29 deletions(-) diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index dc92360..abd37b9 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -166,28 +166,39 @@ public sealed class Markenerkennung( var geordnet = staffel.OrderBy(f => f.EpisodeNumber).ToList(); if (geordnet.Count < 2) continue; - foreach (var art in new[] { Markenart.Intro, Markenart.Abspann }) - { - // Schon gefunden? Dann nicht noch einmal rechnen — es sei - // denn, jemand verlangt es ausdrücklich. - if (!erneut && geordnet.All(f => - vorhanden.Any(v => v.EpisodeId == f.Id && v.Art == art))) - continue; + // **Einmal lesen, zweimal schneiden.** fpcalc kann nicht + // springen — es liest immer von vorn. Also die ganze Spur je + // Folge, und Vorspann wie Abspann sind danach zwei Ausschnitte + // aus demselben Abdruck. Das ist nicht nur richtig, sondern + // billiger als zweimal lesen. + var fehlt = new[] { Markenart.Intro, Markenart.Abspann } + .Where(art => erneut || !geordnet.All(f => + vorhanden.Any(v => v.EpisodeId == f.Id && v.Art == art))) + .ToList(); + if (fehlt.Count == 0) continue; + var ganze = new Dictionary>(); + foreach (var folge in geordnet) + { + var abdruck = await leser.LesenAsync(folge.Path, ct); + if (abdruck.Count > 0) ganze[folge.Id] = abdruck; + } + if (ganze.Count < 2) continue; + + foreach (var art in fehlt) + { var abdruecke = new List<(Guid Schlüssel, IReadOnlyList Abdruck)>(); var fensteranfang = new Dictionary(); foreach (var folge in geordnet) { - var laufzeit = folge.Duration?.TotalSeconds ?? 0; - var (ab, dauer) = Fenster(art, laufzeit); - if (dauer <= 0) continue; + if (!ganze.TryGetValue(folge.Id, out var ganz)) continue; - var abdruck = await leser.LesenAsync(folge.Path, ab, dauer, ct); - if (abdruck.Count == 0) continue; + var (vonIndex, anzahl) = Ausschnitt(art, ganz.Count); + if (anzahl < 2) continue; - abdruecke.Add((folge.Id, abdruck)); - fensteranfang[folge.Id] = ab; + abdruecke.Add((folge.Id, ganz.Skip(vonIndex).Take(anzahl).ToList())); + fensteranfang[folge.Id] = vonIndex * Tonabdruckleser.SekundenJeAbdruck; } if (abdruecke.Count < 2) continue; @@ -215,15 +226,21 @@ public sealed class Markenerkennung( log.LogInformation("Markenerkennung für {Serie}: {Anzahl} Marken.", serieId, gefunden); } - /// Welcher Ausschnitt der Folge abgetastet wird. - private static (double Ab, double Dauer) Fenster(Markenart art, double laufzeit) + /// Welcher Ausschnitt des Abdrucks abgetastet wird. + /// + /// + /// In Werten, nicht in Sekunden: der Abdruck ist eine Reihe, und + /// geschnitten wird an einer Stelle darin. Die Laufzeit steht nicht mehr + /// in der Rechnung — die Länge des Abdrucks ist sie. + /// + private static (int Von, int Anzahl) Ausschnitt(Markenart art, int werte) { - if (art == Markenart.Intro) return (0, laufzeit > 0 ? Math.Min(Introfenster, laufzeit) : Introfenster); + var fenster = (int)Math.Round( + (art == Markenart.Intro ? Introfenster : Abspannfenster) + / Tonabdruckleser.SekundenJeAbdruck); - // Ohne bekannte Laufzeit lässt sich das Ende nicht finden. - if (laufzeit <= 0) return (0, 0); - var ab = Math.Max(0, laufzeit - Abspannfenster); - return (ab, laufzeit - ab); + if (fenster >= werte) return (0, werte); + return art == Markenart.Intro ? (0, fenster) : (werte - fenster, fenster); } private async Task SchreibenAsync( diff --git a/server/src/Flaro.Media/Tonabdruckleser.cs b/server/src/Flaro.Media/Tonabdruckleser.cs index 26120b9..9381ffe 100644 --- a/server/src/Flaro.Media/Tonabdruckleser.cs +++ b/server/src/Flaro.Media/Tonabdruckleser.cs @@ -38,6 +38,10 @@ public sealed class Tonabdruckleser(ILogger log) /// zwei Dritteln Überlappung: 4096/3 / 11025 Sekunden. Die Zahl steht /// hier, weil nur mit ihr aus einem Abdruckbereich eine Zeitangabe wird. /// + /// + /// Nachgerechnet an einer Datei von 2700 Sekunden: 21 786 Werte, also + /// 0,12393 Sekunden je Wert. Die Formel trifft es auf vier Stellen. + /// public const double SekundenJeAbdruck = 4096.0 / 3.0 / 11025.0; /// Wo fpcalc liegt — überschreibbar wie ffmpeg selbst. @@ -75,12 +79,28 @@ public sealed class Tonabdruckleser(ILogger log) } } - /// Der Abdruck eines Abschnitts, oder eine leere Reihe. + /// Der Abdruck der ganzen Tonspur, oder eine leere Reihe. /// - /// Ab welcher Sekunde gelesen wird. - /// Wie viele Sekunden gelesen werden. - public async Task> LesenAsync( - string pfad, double ab, double dauer, CancellationToken ct = default) + /// + /// + /// Die ganze Datei, nicht ein Ausschnitt — weil fpcalc nicht springen + /// kann. Hier stand einmal -ts mit einer Sekundenzahl, in der + /// Annahme, das sei ein Startpunkt. Es ist ein Schalter: „gib + /// Zeitstempel aus". Die Zahl dahinter las fpcalc als zweiten + /// Dateinamen, fand ihn nicht und brach mit Code 2 ab — bei jeder + /// einzelnen Folge, und im Protokoll stand sechsmal „No such file or + /// directory" über Dateien, die sehr wohl da waren. + /// + /// + /// Einen Startpunkt gibt es bei fpcalc überhaupt nicht; -length + /// begrenzt nur die Dauer vom Anfang an. Für den Abspann braucht es aber + /// das Ende. Also einmal die ganze Spur, und geschnitten wird danach — + /// gemessen an einer Folge von 45 Minuten: 1,6 Sekunden und 21 786 + /// Werte. Das ist weniger als die zwei Ausschnitte vorher gekostet + /// hätten, und es stimmt. + /// + /// + public async Task> LesenAsync(string pfad, CancellationToken ct = default) { if (!await VorhandenAsync(ct)) { @@ -93,9 +113,10 @@ public sealed class Tonabdruckleser(ILogger log) ArgumentList = { "-raw", - // Beide als ganze Sekunden: fpcalc nimmt nichts anderes an. - "-ts", Math.Max(0, (int)ab).ToString(CultureInfo.InvariantCulture), - "-length", Math.Max(1, (int)Math.Ceiling(dauer)).ToString(CultureInfo.InvariantCulture), + // **Null heisst: keine Grenze.** Die Vorgabe sind zwei + // Minuten — damit fände man den Vorspann einer Folge + // vielleicht noch, den Abspann nie. + "-length", "0", pfad, }, RedirectStandardOutput = true, diff --git a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs b/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs index 6f53c50..f0acbfb 100644 --- a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs +++ b/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs @@ -45,3 +45,41 @@ public class TonabdruckleserTests Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(0.1238).Within(0.0005)); } } + +/// Was fpcalc wirklich kann — nachgemessen, nicht angenommen. +/// +/// +/// Der Anlass ist ein Fehler, der im Betrieb stand. Hier wurde +/// -ts 0 übergeben, in der Annahme, das sei ein Startpunkt. Es ist +/// ein Schalter („gib Zeitstempel aus"), und die Null las fpcalc als +/// zweiten Dateinamen: Code 2, „No such file or directory" — über Dateien, +/// die da waren. Einen Startpunkt gibt es bei fpcalc nicht. +/// +/// Gemessen am Binärprogramm aus dem Abbild, an einer Datei von 2700 +/// Sekunden: -length 0 ergibt 21 786 Werte, also 0,1239 Sekunden je +/// Wert; -length 120 bricht nach 948 Werten ab. +/// +public class TonabdruckRateTests +{ + [Test] + public void Die_Rate_stimmt_mit_der_Messung_ueberein() + { + // 21 786 Werte für 2700 Sekunden. + var gemessen = 2700.0 / 21786; + Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(gemessen).Within(0.0005)); + } + + /// Eine Stunde Abdruck passt in eine überschaubare Reihe. + /// + /// + /// Damit niemand auf die Idee kommt, den Abdruck einer ganzen Folge für + /// zu gross zu halten und wieder in Ausschnitten zu lesen: eine Stunde + /// sind knapp 29 000 Werte, also 116 Kilobyte. + /// + [Test] + public void Eine_Stunde_sind_rund_29000_Werte() + { + var werte = (int)(3600 / Tonabdruckleser.SekundenJeAbdruck); + Assert.That(werte, Is.EqualTo(29000).Within(500)); + } +}