Die Untertitel kamen vier Sekunden zu spät. Nicht an ihnen lag es: der
Server meldete dem Spieler einen Anfang, der vor dem lag, an dem der
Strom wirklich begann, und genau diese Differenz landet eins zu eins im
Versatz.
Die Schlüsselbildsuche las mit `-read_intervals` bis eine halbe Sekunde
hinter den Wunsch. ffprobe bricht am Ende eines Fensters auf Paketebene
ab — das letzte Schlüsselbild darin kam regelmässig gar nicht mehr
heraus, und die Suche gab das davor zurück. Bei Bildgruppen von vier
Sekunden sind das vier Sekunden.
Nachgemessen gegen die Stelle, an der ffmpeg tatsächlich anfängt, an
einer Datei mit Schlüsselbildern alle vier Sekunden:
Wunsch Suche (0,5 s) ffmpeg Abweichung
52,0 48 52,0 −4,0
73,3 68 72,0 −4,0
100,2 96 100,0 −4,0
Mit zehn Sekunden Nachlauf lag dieselbe Messung bei allen sieben Werten
auf 0,000. Was über den Wunsch hinausgelesen wird, verwirft die
Filterung ohnehin.
Die Prüfung vergleicht nicht gegen eine erwartete Zahl, sondern gegen
ffmpeg selbst — mit dem alten Wert fällt sie um.
Dazu der Filmteil der Abspannerkennung: `Abspannkante` als reine
Rechnung mit elf Prüfungen, `Bildabtaster` über ffmpeg, Marken auch an
Filmen, Knöpfe zum Suchen und Verwerfen auf der Filmseite. An einer
erzeugten Datei trifft die Kante auf vier Sekunden genau.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 lines
12 KiB
C#
300 lines
12 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using Flaro.Api.Endpoints;
|
|
using Flaro.Core.Library;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Flaro.Api.Tests;
|
|
|
|
/// <summary>Vorspann und Abspann auf dem Weg zu den Spielern.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Ohne Tonabgleich.</b> Ob er findet, was er finden soll, steht in
|
|
/// <c>TonabgleichTests</c> — mit erfundenen Abdrücken und ohne Mediendateien.
|
|
/// Hier geht es um das Stück danach: dass eine gefundene Marke auch bei dem
|
|
/// ankommt, der den Knopf zeichnen soll.
|
|
/// </remarks>
|
|
public class MarkenTests
|
|
{
|
|
private string _bestand = "";
|
|
|
|
[SetUp]
|
|
public void Anlegen() => _bestand = Directory.CreateDirectory(
|
|
Path.Combine(Path.GetTempPath(), "flaro-medien-" + Guid.CreateVersion7().ToString("N"))).FullName;
|
|
|
|
[TearDown]
|
|
public void Aufraeumen()
|
|
{
|
|
try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
|
|
}
|
|
|
|
private void Datei(string relativ)
|
|
{
|
|
var voll = Path.Combine(_bestand, relativ);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
|
|
File.WriteAllBytes(voll, new byte[2048]);
|
|
}
|
|
|
|
private async Task<HttpClient> Eingelesen(FlaroServer server, string art)
|
|
{
|
|
var admin = await server.SetUpAndSignIn();
|
|
var client = server.AlsAngemeldeter(admin.Token);
|
|
|
|
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
|
|
new CreateLibraryRequest("Prüfbestand", art, _bestand));
|
|
angelegt.EnsureSuccessStatusCode();
|
|
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
|
|
|
|
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
|
|
for (var versuch = 0; versuch < 100; versuch++)
|
|
{
|
|
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek.Id}/scan");
|
|
if (stand!.State is "finished") return client;
|
|
if (stand.State is "failed") Assert.Fail($"Der Scan ist gescheitert: {stand.Error}");
|
|
await Task.Delay(50);
|
|
}
|
|
Assert.Fail("Der Scan wurde in fünf Sekunden nicht fertig.");
|
|
return null!;
|
|
}
|
|
|
|
private static async Task<(Guid Serie, Guid Folge)> MitEinerFolge(HttpClient client)
|
|
{
|
|
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
|
var serie = serien!.Items[0].Id;
|
|
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
|
|
$"/api/v1/series/{serie}/episodes");
|
|
return (serie, folgen!.Items[0].Id);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Eine_Marke_kommt_bei_der_Folge_an()
|
|
{
|
|
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Shows");
|
|
var (_, folge) = await MitEinerFolge(client);
|
|
|
|
await server.InDerDatenbank(async db =>
|
|
{
|
|
db.Marken.Add(new Marke
|
|
{
|
|
EpisodeId = folge, Art = Markenart.Intro,
|
|
VonSekunden = 30, BisSekunden = 65, ErkanntAm = DateTimeOffset.UtcNow,
|
|
});
|
|
db.Marken.Add(new Marke
|
|
{
|
|
EpisodeId = folge, Art = Markenart.Abspann,
|
|
VonSekunden = 2500, BisSekunden = 2600, ErkanntAm = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
});
|
|
|
|
var einzeln = await client.GetFromJsonAsync<EpisodeResponse>($"/api/v1/episodes/{folge}");
|
|
var intro = einzeln!.Markers!.Single(m => m.Kind == "intro");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(einzeln.Markers, Has.Count.EqualTo(2));
|
|
Assert.That(intro.StartSeconds, Is.EqualTo(30));
|
|
Assert.That(intro.EndSeconds, Is.EqualTo(65));
|
|
Assert.That(einzeln.Markers!.Any(m => m.Kind == "credits"), Is.True,
|
|
"der Abspann heisst gegenüber den Apps credits");
|
|
});
|
|
}
|
|
|
|
/// <summary>Auch in der Folgenliste — dort holt die App sie beim Blättern.</summary>
|
|
[Test]
|
|
public async Task Die_Folgenliste_bringt_die_Marken_mit()
|
|
{
|
|
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
|
|
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Shows");
|
|
var (serie, folge) = await MitEinerFolge(client);
|
|
|
|
await server.InDerDatenbank(async db =>
|
|
{
|
|
db.Marken.Add(new Marke
|
|
{
|
|
EpisodeId = folge, Art = Markenart.Intro,
|
|
VonSekunden = 10, BisSekunden = 40, ErkanntAm = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
});
|
|
|
|
var liste = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
|
|
$"/api/v1/series/{serie}/episodes");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(liste!.Items.Single(f => f.Id == folge).Markers, Has.Count.EqualTo(1));
|
|
Assert.That(liste.Items.Single(f => f.Id != folge).Markers, Is.Empty,
|
|
"eine Folge ohne Marke bekommt eine leere Liste, nicht die der anderen");
|
|
});
|
|
}
|
|
|
|
/// <summary>Ohne erkannte Marken bleibt die Liste leer — kein Knopf, kein Fehler.</summary>
|
|
[Test]
|
|
public async Task Ohne_Erkennung_ist_die_Liste_leer()
|
|
{
|
|
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Shows");
|
|
var (_, folge) = await MitEinerFolge(client);
|
|
|
|
var einzeln = await client.GetFromJsonAsync<EpisodeResponse>($"/api/v1/episodes/{folge}");
|
|
Assert.That(einzeln!.Markers, Is.Empty);
|
|
}
|
|
|
|
/// <summary>Den Lauf darf nur ein Verwalter anstossen.</summary>
|
|
[Test]
|
|
public async Task Die_Suche_laesst_sich_anstossen()
|
|
{
|
|
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Shows");
|
|
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
|
|
|
var antwort = await client.PostAsync($"/api/v1/series/{serien!.Items[0].Id}/marken", null);
|
|
var unbekannt = await client.PostAsync($"/api/v1/series/{Guid.NewGuid()}/marken", null);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Accepted));
|
|
Assert.That(unbekannt.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
|
});
|
|
}
|
|
|
|
/// <summary>Eine abgesuchte Staffel wird nicht noch einmal abgetastet.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Der teuerste Fehler, den es hier gab.</b> Gemerkt wurde nur, was
|
|
/// gefunden wurde. Eine Staffel ohne Abspann — es gibt genug davon —
|
|
/// sah damit bei jedem Scan aus wie eine, in der noch niemand
|
|
/// nachgesehen hat: jede Tonspur jeder Folge noch einmal von der
|
|
/// Platte, für immer, ohne Ertrag.
|
|
///
|
|
/// Geprüft wird hier der Vermerk, nicht das Abtasten selbst — ohne
|
|
/// fpcalc und ohne echten Ton gibt es nichts zu finden, und genau
|
|
/// dieser Fall ist der, um den es geht.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ein_Lauf_ohne_Fund_merkt_sich_das()
|
|
{
|
|
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
|
|
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Shows");
|
|
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
|
var serie = serien!.Items[0].Id;
|
|
|
|
(await client.PostAsync($"/api/v1/series/{serie}/marken", null)).EnsureSuccessStatusCode();
|
|
|
|
// Der Lauf hängt im Hintergrund; ohne fpcalc ist er sofort durch.
|
|
List<Markensuche> vermerke = [];
|
|
for (var versuch = 0; versuch < 40; versuch++)
|
|
{
|
|
await server.InDerDatenbank(async db =>
|
|
vermerke = await db.Markensuchen.AsNoTracking()
|
|
.Where(x => x.SeriesId == serie).ToListAsync());
|
|
if (vermerke.Count > 0) break;
|
|
await Task.Delay(50);
|
|
}
|
|
|
|
// Ohne fpcalc gibt es keinen Vermerk — dann ist nichts gelaufen und
|
|
// auch nichts zu merken. Mit fpcalc müssen beide Arten dastehen.
|
|
Assert.That(vermerke.Count, Is.AnyOf(0, 2),
|
|
"entweder gar nicht gelaufen oder beide Arten vermerkt");
|
|
if (vermerke.Count == 2)
|
|
{
|
|
Assert.That(vermerke.Select(v => v.Folgen), Is.All.EqualTo(2),
|
|
"die Folgenzahl löst den nächsten Lauf aus");
|
|
}
|
|
}
|
|
|
|
/// <summary>Ein Film trägt seine Abspannmarke genauso.</summary>
|
|
[Test]
|
|
public async Task Ein_Film_bekommt_seine_Marke()
|
|
{
|
|
Datei("Matrix (1999)/Matrix (1999).mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Movies");
|
|
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
|
var film = filme!.Items[0].Id;
|
|
|
|
await server.InDerDatenbank(async db =>
|
|
{
|
|
db.Marken.Add(new Marke
|
|
{
|
|
MovieId = film, Art = Markenart.Abspann,
|
|
VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
});
|
|
|
|
var einzeln = await client.GetFromJsonAsync<MovieResponse>($"/api/v1/movies/{film}");
|
|
var liste = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(einzeln!.Markers!.Single().Kind, Is.EqualTo("credits"));
|
|
Assert.That(einzeln.Markers!.Single().StartSeconds, Is.EqualTo(6900));
|
|
Assert.That(liste!.Items[0].Markers, Has.Count.EqualTo(1), "auch in der Liste");
|
|
});
|
|
}
|
|
|
|
/// <summary>Eine falsche Marke lässt sich wieder wegnehmen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Ohne diesen Weg wäre die Erkennung eine Blackbox, die manchmal recht
|
|
/// hat: ein Fehlgriff bliebe stehen, und niemand könnte etwas dagegen
|
|
/// tun ausser die Datenbank anzufassen.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Eine_falsche_Filmmarke_laesst_sich_verwerfen()
|
|
{
|
|
Datei("Matrix (1999)/Matrix (1999).mkv");
|
|
|
|
using var server = new FlaroServer();
|
|
var client = await Eingelesen(server, "Movies");
|
|
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
|
var film = filme!.Items[0].Id;
|
|
|
|
await server.InDerDatenbank(async db =>
|
|
{
|
|
db.Marken.Add(new Marke
|
|
{
|
|
MovieId = film, Art = Markenart.Abspann,
|
|
VonSekunden = 60, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow,
|
|
});
|
|
db.Markensuchen.Add(new Markensuche
|
|
{
|
|
MovieId = film, Art = Markenart.Abspann, Folgen = 1,
|
|
GelaufenAm = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
});
|
|
|
|
var weg = await client.DeleteAsync($"/api/v1/movies/{film}/marken");
|
|
var danach = await client.GetFromJsonAsync<MovieResponse>($"/api/v1/movies/{film}");
|
|
|
|
var vermerke = 0;
|
|
await server.InDerDatenbank(async db =>
|
|
vermerke = await db.Markensuchen.CountAsync(x => x.MovieId == film));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
|
Assert.That(danach!.Markers, Is.Empty);
|
|
Assert.That(vermerke, Is.EqualTo(1),
|
|
"der Vermerk bleibt, sonst legte der nächste Scan dieselbe Marke wieder an");
|
|
});
|
|
}
|
|
}
|