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"));
+ }
+}