using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Flaro.Core.Library;
using Microsoft.EntityFrameworkCore;
namespace Flaro.Api.Tests;
/// Vorspann und Abspann auf dem Weg zu den Spielern.
///
///
/// Ohne Tonabgleich. Ob er findet, was er finden soll, steht in
/// TonabgleichTests — 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.
///
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 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())!;
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
for (var versuch = 0; versuch < 100; versuch++)
{
var stand = await client.GetFromJsonAsync($"/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>("/api/v1/series");
var serie = serien!.Items[0].Id;
var folgen = await client.GetFromJsonAsync>(
$"/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($"/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");
});
}
/// Auch in der Folgenliste — dort holt die App sie beim Blättern.
[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>(
$"/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");
});
}
/// Ohne erkannte Marken bleibt die Liste leer — kein Knopf, kein Fehler.
[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($"/api/v1/episodes/{folge}");
Assert.That(einzeln!.Markers, Is.Empty);
}
/// Den Lauf darf nur ein Verwalter anstossen.
[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>("/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));
});
}
/// Eine abgesuchte Staffel wird nicht noch einmal abgetastet.
///
///
/// Der teuerste Fehler, den es hier gab. 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.
///
[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>("/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 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");
}
}
/// Ein Film trägt seine Abspannmarke genauso.
[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>("/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($"/api/v1/movies/{film}");
var liste = await client.GetFromJsonAsync>("/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");
});
}
/// Eine falsche Marke lässt sich wieder wegnehmen.
///
///
/// 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.
///
[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>("/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($"/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");
});
}
/// Wie weit ein Lauf greift — drei Umfänge, drei Antworten.
///
///
///
/// „Schon durchsucht" und „hat eine Marke" sind zwei verschiedene
/// Dinge. Ein Film, bei dem die Suche einmal nichts fand, trägt
/// einen Vermerk und keine Marke — und war damit für jeden weiteren
/// Lauf erledigt, obwohl er der interessanteste Fall ist.
///
///
/// Geprüft wird am Vermerk: fasst der Lauf den Film an, schreibt er
/// GelaufenAm neu. Das ist die einzige Spur, die auch ohne
/// ffmpeg und ohne echten Abspann entsteht.
///
///
[TestCase("Neue", false, TestName = "Neue überspringt, was schon durchsucht ist")]
[TestCase("Luecken", true, TestName = "Luecken nimmt sich den Film ohne Marke vor")]
[TestCase("Alles", true, TestName = "Alles nimmt sich alles vor")]
public async Task Der_Umfang_entscheidet_bei_einem_Film_ohne_Marke(
string umfang, bool erwartetAngefasst)
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Movies");
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
var film = filme!.Items[0].Id;
// Gesucht wurde schon, gefunden nichts: Vermerk ja, Marke nein.
var vorher = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
await server.InDerDatenbank(async db =>
{
// **Ohne Laufzeit kommt der Lauf nicht bis zum Vermerk.** Die
// Prüfdatei ist leer; die Laufzeit steht sonst aus ffprobe.
var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film);
datei.Duration = TimeSpan.FromMinutes(120);
db.Markensuchen.Add(new Markensuche
{
MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher,
});
await db.SaveChangesAsync();
});
(await client.PostAsync($"/api/v1/movies/{film}/marken?umfang={umfang}", null))
.EnsureSuccessStatusCode();
var gelaufen = vorher;
for (var versuch = 0; versuch < 40; versuch++)
{
await server.InDerDatenbank(async db =>
gelaufen = await db.Markensuchen.AsNoTracking()
.Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync());
if (gelaufen != vorher) break;
await Task.Delay(50);
}
Assert.That(gelaufen != vorher, Is.EqualTo(erwartetAngefasst),
erwartetAngefasst
? "dieser Umfang muss den Film noch einmal anfassen"
: "dieser Umfang muss ihn überspringen");
}
/// Ein Film mit Marke bleibt bei „nur Lücken" unangetastet.
[Test]
public async Task Luecken_laesst_einen_Film_mit_Marke_in_Ruhe()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Movies");
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
var film = filme!.Items[0].Id;
var vorher = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
await server.InDerDatenbank(async db =>
{
var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film);
datei.Duration = TimeSpan.FromMinutes(120);
db.Marken.Add(new Marke
{
MovieId = film, Art = Markenart.Abspann,
VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = vorher,
});
db.Markensuchen.Add(new Markensuche
{
MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher,
});
await db.SaveChangesAsync();
});
(await client.PostAsync($"/api/v1/movies/{film}/marken?umfang=Luecken", null))
.EnsureSuccessStatusCode();
await Task.Delay(400);
var gelaufen = DateTimeOffset.MinValue;
await server.InDerDatenbank(async db =>
gelaufen = await db.Markensuchen.AsNoTracking()
.Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync());
Assert.That(gelaufen, Is.EqualTo(vorher),
"wo schon eine Marke steht, ist nichts zu füllen");
}
}