„No such file or directory" über Dateien, die da waren

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-12 22:09:56 +02:00
co-authored by Claude Opus 5
parent 2c535e6f47
commit fd79fb4022
3 changed files with 105 additions and 29 deletions
+38 -21
View File
@@ -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<Guid, IReadOnlyList<uint>>();
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<uint> Abdruck)>();
var fensteranfang = new Dictionary<Guid, double>();
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);
}
/// <summary>Welcher Ausschnitt der Folge abgetastet wird.</summary>
private static (double Ab, double Dauer) Fenster(Markenart art, double laufzeit)
/// <summary>Welcher Ausschnitt des Abdrucks abgetastet wird.</summary>
///
/// <remarks>
/// 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.
/// </remarks>
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<bool> SchreibenAsync(