diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index a934970..b281f48 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -117,7 +117,8 @@ public sealed class Markenerkennung( } catch (Exception fehler) { - log.LogWarning(fehler, "Markenerkennung für {Serie} abgebrochen.", serieId); + log.LogWarning(fehler, "Markensuche für {Serie} abgebrochen: {Grund}", + serieId, Fehlertext.FuerMenschen(fehler)); } finally { @@ -233,7 +234,12 @@ public sealed class Markenerkennung( } catch (Exception fehler) { - log.LogWarning(fehler, "Markensuche für „{Serie}“ abgebrochen.", titel); + // **Mit dem innersten Text.** Das Tagebuch hängt nur + // `Message` an, und bei EF Core steht dort „See the inner + // exception for details." — eine Zeile, die ausdrücklich + // sagt, dass sie die Auskunft nicht enthält. + log.LogWarning(fehler, "Markensuche für „{Serie}“ abgebrochen: {Grund}", + titel, Fehlertext.FuerMenschen(fehler)); } finally { _laufend.TryRemove(id, out _); } } @@ -260,8 +266,8 @@ public sealed class Markenerkennung( try { await NachScanAsync(mediathek, erneut, CancellationToken.None); } catch (Exception fehler) { - log.LogWarning(fehler, - "Markenlauf für Mediathek {Kennung} abgebrochen.", mediathek); + log.LogWarning(fehler, "Markenlauf für Mediathek {Kennung} abgebrochen: {Grund}", + mediathek, Fehlertext.FuerMenschen(fehler)); } }); } @@ -303,7 +309,8 @@ public sealed class Markenerkennung( try { if (await FilmAsync(film.Id, erneut, ct)) gefunden++; } catch (Exception fehler) { - log.LogWarning(fehler, "Abspannsuche für „{Film}“ abgebrochen.", titel); + log.LogWarning(fehler, "Abspannsuche für „{Film}“ abgebrochen: {Grund}", + titel, Fehlertext.FuerMenschen(fehler)); } } @@ -427,12 +434,12 @@ public sealed class Markenerkennung( // allein ist keine Auskunft: ob die Datei zu wenige Schlüsselbilder // hat, ob der Abspann zu hell war oder ob am Schluss eine Tafel // steht, sieht man nur an diesen drei Zahlen — und sonst gar nicht. - if (Abspannkante.Suchen(proben, regeln) is not { } stelle) + if (Abspannkante.Suchen(proben, regeln, out var grund) is not { } stelle) { log.LogInformation( - "„{Titel}“: kein Abspann — {Proben} Probe(n), Schwelle {Schwelle:0.#}, " - + "letzte Probe {Letzte:0.#}.", - name, proben.Count, Abspannkante.Schwelle(proben, regeln), + "„{Titel}“: kein Abspann — {Grund}. {Proben} Probe(n), " + + "Schwelle {Schwelle:0.#}, letzte Probe {Letzte:0.#}.", + name, grund, proben.Count, Abspannkante.Schwelle(proben, regeln), proben.Count > 0 ? proben[^1].Helligkeit : double.NaN); return false; } @@ -741,8 +748,10 @@ public sealed class Markenerkennung( private async Task VermerkenAsync( FlaroContext db, Guid filmId, Markenart art, CancellationToken ct) { - var alt = await db.Markensuchen - .FirstOrDefaultAsync(x => x.MovieId == filmId && x.Art == art, ct); + var alt = db.Markensuchen.Local + .FirstOrDefault(x => x.MovieId == filmId && x.Art == art) + ?? await db.Markensuchen + .FirstOrDefaultAsync(x => x.MovieId == filmId && x.Art == art, ct); if (alt is not null) { alt.GelaufenAm = clock.GetUtcNow(); return; } @@ -759,8 +768,20 @@ public sealed class Markenerkennung( private async Task VermerkenAsync( FlaroContext db, Guid serieId, int staffel, Markenart art, int folgen, CancellationToken ct) { - var alt = await db.Markensuchen - .FirstOrDefaultAsync(x => x.SeriesId == serieId && x.Staffel == staffel && x.Art == art, ct); + // **Erst im Änderungsspeicher nachsehen, dann in der Datenbank.** + // + // Eine Abfrage sieht nicht, was derselbe Lauf eben erst hinzugefügt + // und noch nicht gespeichert hat. Seit der Bildweg dazukam, wird + // für dieselbe Staffel zweimal vermerkt — einmal nach dem + // Tonabgleich, einmal vor dem Bild —, und die zweite Abfrage fand + // nichts, legte eine zweite Zeile an und brach beim Speichern am + // eindeutigen Schlüssel ab. Die Serie verlor dabei alles, was in + // diesem Lauf gefunden worden war. + var alt = db.Markensuchen.Local + .FirstOrDefault(x => x.SeriesId == serieId && x.Staffel == staffel && x.Art == art) + ?? await db.Markensuchen + .FirstOrDefaultAsync( + x => x.SeriesId == serieId && x.Staffel == staffel && x.Art == art, ct); if (alt is not null) { diff --git a/server/src/Flaro.Core/Media/Abspannkante.cs b/server/src/Flaro.Core/Media/Abspannkante.cs index 6e1f6f9..4e8faf2 100644 --- a/server/src/Flaro.Core/Media/Abspannkante.cs +++ b/server/src/Flaro.Core/Media/Abspannkante.cs @@ -46,14 +46,26 @@ public readonly record struct Abspannregeln /// public double Anteil { get; init; } - /// Wie viele helle Proben am Stück eine Strecke nicht brechen. + /// Wie viele Sekunden hell eine dunkle Strecke nicht brechen. /// /// + /// /// Eine Schrifttafel blitzt hell auf, ein Logo steht zwei Sekunden im /// Bild. Ohne Nachsicht zerfiele ein Abspann in fünf kurze Strecken, /// von denen keine lang genug wäre. + /// + /// + /// In Sekunden, nicht in Proben — dieselbe Lehre wie bei + /// , hier eine Zeile höher übersehen. Als + /// Probenzahl hing die Nachsicht daran, wie dicht die Datei ihre + /// Schlüsselbilder setzt: gemessen an „(T)Raumschiff Surprise" kommen + /// 1318 Proben auf zwanzig Minuten, also eine alle 0,9 Sekunden — + /// zwei Proben waren dort 1,8 Sekunden Nachsicht. Bei einer Datei mit + /// einem Schlüsselbild alle zehn Sekunden waren es zwanzig. Dasselbe + /// Zugeständnis, zehnfach verschieden. + /// /// - public int Lücke { get; init; } + public double Nachsicht { get; init; } /// Wie viele Sekunden hell hinter dem Abspann erlaubt sind. /// @@ -73,7 +85,7 @@ public readonly record struct Abspannregeln { Dunkelschwelle = 35, Anteil = 0.55, - Lücke = 2, + Nachsicht = 10, Schlusstafel = 20, Mindestlänge = 45, Höchstlänge = 12 * 60, @@ -137,9 +149,25 @@ public static class Abspannkante return Math.Max(regeln.Dunkelschwelle, grundton * regeln.Anteil); } - public static double? Suchen(IReadOnlyList proben, Abspannregeln regeln) + public static double? Suchen(IReadOnlyList proben, Abspannregeln regeln) => + Suchen(proben, regeln, out _); + + /// + /// Woran es lag — für das Protokoll. „Kein Abspann gefunden" ist keine + /// Auskunft: ob das Ende zu hell war, die Strecke zu kurz oder zu lang, + /// sind drei verschiedene Ursachen mit drei verschiedenen Antworten, + /// und ohne diese Zeile beginnt jede Nachfrage wieder beim Raten. + /// + public static double? Suchen( + IReadOnlyList proben, Abspannregeln regeln, out string grund) { - if (proben.Count < 2) return null; + grund = "gefunden"; + if (proben.Count < 2) + { + grund = "zu wenige Proben"; + return null; + } + var schwelle = Schwelle(proben, regeln); // **Von hinten.** Der Abspann endet am Dateiende; alles, was davor @@ -150,33 +178,44 @@ public static class Abspannkante // Sender, Studio, Förderer —, oft hell und ein paar Sekunden lang. // Darauf zu bestehen, dass die letzte Probe dunkel ist, hiess: ein // einziges weisses Schlussbild macht den ganzen Abspann unsichtbar. - // Dieselbe Nachsicht wie innerhalb der Strecke, also `Lücke`. var letzte = proben.Count - 1; var ende = proben[letzte].Sekunde; while (letzte > 0 && proben[letzte].Helligkeit >= schwelle) { - if (ende - proben[letzte].Sekunde >= regeln.Schlusstafel) return null; + if (ende - proben[letzte].Sekunde >= regeln.Schlusstafel) + { + grund = "das Ende ist zu lange hell"; + return null; + } letzte--; } - if (proben[letzte].Helligkeit >= schwelle) return null; + if (proben[letzte].Helligkeit >= schwelle) + { + grund = "das Ende ist hell"; + return null; + } + // **Rückwärts, und helle Stellen in Sekunden verzeihen.** Gezählt + // wurden hier früher Proben, und was das bedeutet, hängt allein an + // der Schlüsselbilddichte der Datei — siehe `Nachsicht`. var anfang = letzte; - var hellAmStück = 0; - for (var i = letzte - 1; i >= 0; i--) { - if (proben[i].Helligkeit < schwelle) - { - anfang = i; - hellAmStück = 0; - continue; - } - - if (++hellAmStück > regeln.Lücke) break; + if (proben[i].Helligkeit < schwelle) { anfang = i; continue; } + if (proben[anfang].Sekunde - proben[i].Sekunde > regeln.Nachsicht) break; } var länge = proben[letzte].Sekunde - proben[anfang].Sekunde; - if (länge < regeln.Mindestlänge || länge > regeln.Höchstlänge) return null; + if (länge < regeln.Mindestlänge) + { + grund = $"die dunkle Strecke ist mit {länge:0} s zu kurz"; + return null; + } + if (länge > regeln.Höchstlänge) + { + grund = $"die dunkle Strecke ist mit {länge:0} s zu lang"; + return null; + } // **Die erste dunkle Probe ist nicht der Anfang.** Zwischen ihr und // der letzten hellen davor liegt die Kante irgendwo; wo genau, sagt diff --git a/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs b/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs index 21407a5..a5b1a17 100644 --- a/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs +++ b/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs @@ -72,13 +72,20 @@ public class AbspannkanteTests } /// Eine helle Schrifttafel mittendrin bricht die Strecke nicht. + /// + /// + /// Eine Probe, also zehn Sekunden. Hier standen zwei, und das + /// war unter der alten Zählweise gedacht: nachgesehen wurde, wie viele + /// Proben am Stück hell sind, nicht wie lange. Zwanzig Sekunden + /// helles Bild sind keine Schrifttafel mehr, sondern eine Szene — dass + /// die Suche dort abbricht, ist richtig und steht in + /// AbspannNachsichtTests. + /// [Test] public void Eine_kurze_helle_Tafel_bricht_nichts() { var proben = Reihe(0, 600, abspannAb: 420); - // Zwei Proben, also zwanzig Sekunden, sind hell. proben[50] = proben[50] with { Helligkeit = 140 }; - proben[51] = proben[51] with { Helligkeit = 140 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420)); } @@ -310,3 +317,94 @@ public class AbspannSchlusstafelTests Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } } + +/// Dieselbe Szene, zweierlei Schlüsselbilddichte. +/// +/// +/// +/// Der Fehler, für den diese Prüfungen stehen. Die Nachsicht +/// gegenüber hellen Stellen mitten im Abspann war in Proben +/// angegeben, nicht in Sekunden. Was das bedeutet, hängt allein daran, +/// wie dicht eine Datei ihre Schlüsselbilder setzt — und das schwankt um +/// eine Zehnerpotenz. +/// +/// +/// Gemessen am Bestand: „(T)Raumschiff Surprise" liefert 1318 Proben auf +/// zwanzig Minuten, also eine alle 0,9 Sekunden. Zwei Proben Nachsicht +/// waren dort 1,8 Sekunden; bei einer Datei mit einem Schlüsselbild alle +/// zehn Sekunden waren dieselben zwei Proben zwanzig Sekunden. Ein +/// fünfsekündiges Logo mitten im Abspann zerriss die Strecke deshalb nur +/// bei der dicht abgetasteten Datei — und genau die fiel durch. +/// +/// +public class AbspannNachsichtTests +{ + private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; + + /// Abspann ab 2340 s, darin fünf Sekunden helles Logo. + private static List Fenster(double takt) + { + var proben = new List(); + for (var t = 1500.0; t <= 2700; t += takt) + { + var imLogo = t >= 2500 && t < 2505; + proben.Add(new Bildprobe(t, t < 2340 ? 126 : imLogo ? 180 : 32)); + } + return proben; + } + + /// Dicht abgetastet — der Fall, der im Bestand durchfiel. + [Test] + public void Ein_Logo_zerreisst_den_Abspann_nicht_bei_dichten_Proben() + { + Assert.That(Abspannkante.Suchen(Fenster(takt: 0.9), Regeln), + Is.EqualTo(2340.0).Within(2)); + } + + /// Dünn abgetastet — derselbe Film, dieselbe Antwort. + [Test] + public void Und_auch_nicht_bei_duennen_Proben() + { + Assert.That(Abspannkante.Suchen(Fenster(takt: 10), Regeln), + Is.EqualTo(2340.0).Within(11)); + } + + /// Eine lange helle Strecke bricht weiterhin ab. + /// + /// + /// Sonst liefe die Suche rückwärts durch jede Nachtszene und meldete + /// den halben Film als Abspann. + /// + [Test] + public void Eine_lange_helle_Strecke_bricht_die_Suche() + { + var proben = new List(); + for (var t = 1500.0; t <= 2700; t += 1) + { + // Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell. + var hell = t < 2340 || (t >= 2400 && t < 2460); + proben.Add(new Bildprobe(t, hell ? 126 : 32)); + } + + // Gefunden wird nur das Stück hinter der hellen Minute. + Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2460.0).Within(2)); + } + + /// Und der Grund steht für das Protokoll bereit. + [Test] + public void Der_Grund_wird_benannt() + { + var hell = new List(); + for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 126)); + + Abspannkante.Suchen(hell, Regeln, out var grund); + Assert.That(grund, Does.Contain("hell")); + + var kurz = new List(); + for (var t = 1500.0; t <= 2700; t += 5) + kurz.Add(new Bildprobe(t, t < 2680 ? 126 : 32)); + + Abspannkante.Suchen(kurz, Regeln, out var zuKurz); + Assert.That(zuKurz, Does.Contain("kurz")); + } +}