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, und wurde wieder abgetastet: jede Tonspur jeder Folge noch einmal von der Platte, für immer, ohne Ertrag. Bei sechstausend Folgen ist das kein Schönheitsfehler. Jetzt steht auch „gesucht und nichts gefunden" in der Datenbank, je Staffel und Art. Ausgelöst wird ein zweiter Lauf von der Folgenzahl: kommt eine Folge dazu, gibt es etwas Neues zu vergleichen, sonst nicht. Ein Zeitstempel allein täte das nicht — eine Staffel, die sich nie ändert, hat nie einen Grund für einen zweiten Lauf. Dazu der Knopf, der ausdrücklich doch noch einmal rechnet: die Uhr auf der Serienseite sucht ab jetzt immer neu. Von selbst läuft die Suche nach jedem Scan und überspringt dort das Abgesuchte; wer hier drückt, will genau das Gegenteil. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
220 lines
8.6 KiB
C#
220 lines
8.6 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");
|
|
}
|
|
}
|
|
}
|