Vorspann und Abspann — erkannt, abgelegt, überspringbar
Beides mit derselben Rechnung, nur in einem anderen Fenster: vorn die ersten fünfzehn Minuten, hinten die letzten zehn. Was `Tonabgleich` findet, hängt allein daran, welchen Ausschnitt man ihm gibt — eine zweite Erkennung für den Abspann wäre dieselbe Suche ein zweites Mal. Je Staffel verglichen, nicht je Serie: Serien wechseln ihren Vorspann zwischen Staffeln, und über alle hinweg fände man nichts oder das Falsche. Gefundenes über fünf Minuten wird verworfen — zwei Folgen, die dieselbe Datei tragen, teilen vierzig, und darüber stünde sonst ein Überspringen-Knopf. Der Lauf geht auf Zuruf, nicht bei jedem Scan: er liest von jeder Folge ein Stück Ton und dauert Minuten. Der Knopf dafür steht auf der Serienseite, bei den Verwalterknöpfen. Die Marken gehen im Folgenvertrag mit — als „intro" und „credits", die Wörter, die die Apps von Jellyfin her ohnehin kennen. In der App zeichnet sie der vorhandene Überspringen-Knopf, der bisher nie etwas zu tun hatte; im Web ist er neu, rechtsbündig über der Zeitleiste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
acba683c94
commit
8cf2118723
@@ -0,0 +1,171 @@
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user