using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Flaro.Core.Library;
using Flaro.Core.Metadata;
using Flaro.Media;
using Microsoft.Extensions.DependencyInjection;
namespace Flaro.Api.Tests;
/// Eine falsche Zuordnung richtigstellen — und sie festschreiben.
public class MatchCorrectionTests
{
private string _bestand = "";
private string _filmOrdner = "";
private FlaroServer? _server;
private static string? Werkzeug(string name) =>
(Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator)
.Select(o => o.Length == 0 ? null : Path.Combine(o, name))
.FirstOrDefault(p => p is not null && File.Exists(p));
[SetUp]
public void Anlegen()
{
if (Werkzeug("ffmpeg") is null) Assert.Ignore("ffmpeg ist auf diesem Rechner nicht installiert.");
_bestand = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-korrektur-" + Guid.CreateVersion7().ToString("N"))).FullName;
_filmOrdner = Directory.CreateDirectory(Path.Combine(_bestand, "Dark (2017)")).FullName;
var start = new ProcessStartInfo(Werkzeug("ffmpeg")!) { RedirectStandardError = true, UseShellExecute = false };
foreach (var a in new[]
{
"-y", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=duration=1:size=160x120:rate=10",
"-c:v", "libx264", "-preset", "ultrafast", Path.Combine(_filmOrdner, "Dark (2017).mkv"),
}) start.ArgumentList.Add(a);
using var p = Process.Start(start)!;
p.StandardError.ReadToEnd();
p.WaitForExit();
}
[TearDown]
public void Aufraeumen()
{
_server?.Dispose(); _server = null;
try { if (_bestand.Length > 0) Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
}
/// Ein Anbieter, der zwei verschiedene Filme kennt.
private sealed class ZweiTreffer : IMetadataProvider
{
public static readonly MovieMetadata Falsch =
new(1, "Dark Shadows", 1966, "Eine Fernsehserie von 1966.", null, null, "tt0059978");
public static readonly MovieMetadata Richtig =
new(70523, "Dark", 2017, "Nach dem Verschwinden eines Kindes.", null, null, "tt5753856");
/// Ein Treffer mit anderem Jahr als im Dateinamen.
public static readonly MovieMetadata AnderesJahr =
new(613, "Der Untergang", 2004, "Berlin, April 1945.", null, null, "tt0363163");
public Task IsConfiguredAsync(CancellationToken ct = default) => Task.FromResult(true);
// Die Suche liefert den beliebteren zuerst — genau wie TMDB.
public Task FindMovieAsync(string t, int? j, CancellationToken ct = default) =>
Task.FromResult(Falsch);
public Task> SearchMoviesAsync(string t, int? j, CancellationToken ct = default) =>
Task.FromResult>([Falsch, Richtig]);
public Task GetMovieAsync(int id, CancellationToken ct = default) =>
Task.FromResult(
id == Richtig.ProviderId ? Richtig
: id == Falsch.ProviderId ? Falsch
: id == AnderesJahr.ProviderId ? AnderesJahr
: null);
public Task FindSeriesAsync(string t, int? j, CancellationToken ct = default) =>
Task.FromResult(null);
public Task> SearchSeriesAsync(string t, int? j, CancellationToken ct = default) =>
Task.FromResult>([]);
public Task GetSeriesAsync(int id, CancellationToken ct = default) =>
Task.FromResult(null);
public Task FindEpisodeAsync(int s, int st, int f, CancellationToken ct = default) =>
Task.FromResult(null);
}
private async Task<(HttpClient Client, MovieResponse Film)> Eingelesen(bool beiwerkSchreiben)
{
_server = new FlaroServer(dienste =>
{
dienste.AddScoped();
dienste.AddSingleton(
new SidecarWriter(new SidecarOptions(beiwerkSchreiben),
Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance));
});
var admin = await _server.SetUpAndSignIn();
var client = _server.AlsAngemeldeter(admin.Token);
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
new CreateLibraryRequest("Filme", "Movies", _bestand));
angelegt.EnsureSuccessStatusCode();
var bibliothek = (await angelegt.Content.ReadFromJsonAsync())!;
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
for (var i = 0; i < 150; i++)
{
var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek.Id}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var filme = await client.GetFromJsonAsync>("/api/v1/movies");
return (client, filme!.Items.Single());
}
[Test]
public async Task Der_Scan_ordnet_zuerst_falsch_zu()
{
var (_, film) = await Eingelesen(beiwerkSchreiben: false);
Assert.That(film.Title, Is.EqualTo("Dark Shadows"), "So sieht der Fehler aus, den es zu beheben gilt");
}
[Test]
public async Task Die_Vorschlagsliste_enthaelt_den_richtigen()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: false);
var vorschläge = await client.GetFromJsonAsync>($"/api/v1/movies/{film.Id}/matches");
Assert.That(vorschläge, Has.Count.EqualTo(2));
Assert.That(vorschläge!.Select(v => v.Title), Does.Contain("Dark"));
}
[Test]
public async Task Eine_Korrektur_greift_sofort()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: false);
var antwort = await client.PutAsJsonAsync($"/api/v1/movies/{film.Id}/match", new ApplyMatchRequest(70523));
antwort.EnsureSuccessStatusCode();
var stand = await antwort.Content.ReadFromJsonAsync();
Assert.That(stand!.Title, Is.EqualTo("Dark"));
Assert.That(stand.TmdbId, Is.EqualTo(70523));
Assert.That(stand.ImdbId, Is.EqualTo("tt5753856"));
Assert.That(stand.Locked, Is.True);
var danach = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(danach!.Items.Single().Title, Is.EqualTo("Dark"));
}
/// Die Korrektur überlebt einen Neuaufbau der Datenbank.
///
///
/// Darum geht es bei der NFO-Datei. In der Datenbank allein wäre
/// die Entscheidung mit dem nächsten Umzug oder Neuaufbau verloren.
///
[Test]
public async Task Eine_Korrektur_landet_in_der_NFO_Datei()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: true);
(await client.PutAsJsonAsync($"/api/v1/movies/{film.Id}/match", new ApplyMatchRequest(70523)))
.EnsureSuccessStatusCode();
var nfo = Path.Combine(_filmOrdner, "movie.nfo");
Assert.That(File.Exists(nfo), Is.True, "Die NFO muss neben dem Film liegen");
var inhalt = await File.ReadAllTextAsync(nfo);
var kennungen = Nfo.ReadIds(inhalt);
Assert.That(kennungen.Tmdb, Is.EqualTo(70523));
Assert.That(kennungen.Imdb, Is.EqualTo("tt5753856"));
Assert.That(Nfo.ReadLocked(inhalt), Is.True, "Damit kein Lauf die Entscheidung überstimmt");
Assert.That(inhalt, Does.Contain("Dark"));
}
[Test]
public async Task Ohne_Schreiberlaubnis_bleibt_der_Ordner_unberuehrt()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: false);
(await client.PutAsJsonAsync($"/api/v1/movies/{film.Id}/match", new ApplyMatchRequest(70523)))
.EnsureSuccessStatusCode();
Assert.That(Directory.GetFiles(_filmOrdner).Select(Path.GetFileName),
Is.EqualTo(new[] { "Dark (2017).mkv" }));
}
[Test]
public async Task Eine_Kennung_die_es_nicht_gibt_wird_abgelehnt()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: false);
var antwort = await client.PutAsJsonAsync($"/api/v1/movies/{film.Id}/match", new ApplyMatchRequest(999999));
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
/// Eine Korrektur muss den nächsten Scan überstehen.
///
///
/// Live gefunden, und der Fehler war unangenehm. Nach der Korrektur
/// stand im Ordner „Dark (2017)", in der Datenbank aber 2004. Der nächste
/// Scan suchte nach (Dark, 2017), fand nichts, legte den Film neu an und
/// löschte den korrigierten als verwaist — die Zuordnung überlebte nur,
/// weil sie in der NFO stand. Zugeordnet wird deshalb über
/// ParsedYear, das sich nie ändert.
///
[Test]
public async Task Eine_Korrektur_ueberlebt_den_naechsten_Scan()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: false);
(await client.PutAsJsonAsync($"/api/v1/movies/{film.Id}/match", new ApplyMatchRequest(613)))
.EnsureSuccessStatusCode();
var bibliothek = (await client.GetFromJsonAsync>("/api/v1/libraries"))!.Single();
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null))
.EnsureSuccessStatusCode();
for (var i = 0; i < 150; i++)
{
var stand = await client.GetFromJsonAsync(
$"/api/v1/libraries/{bibliothek.Id}/scan");
if (stand!.State == "finished") break;
await Task.Delay(50);
}
var danach = await client.GetFromJsonAsync>("/api/v1/movies");
Assert.That(danach!.Total, Is.EqualTo(1), "Kein zweiter Eintrag für denselben Film");
var geblieben = danach.Items.Single();
Assert.That(geblieben.Id, Is.EqualTo(film.Id), "Derselbe Eintrag, nicht ein neuer");
Assert.That(geblieben.Title, Is.EqualTo("Der Untergang"));
Assert.That(geblieben.Year, Is.EqualTo(2004), "Und nicht wieder 2017 aus dem Dateinamen");
}
[Test]
public async Task Nur_Administratoren_duerfen_korrigieren()
{
var (client, film) = await Eingelesen(beiwerkSchreiben: false);
await client.PostAsJsonAsync("/api/v1/users",
new CreateUserRequest("gast", "auch-ein-langes-passwort", false));
var gast = await _server!.SignIn("gast", "auch-ein-langes-passwort", "iPad", "g-1");
var antwort = await _server.AlsAngemeldeter(gast.Token)
.PutAsJsonAsync($"/api/v1/movies/{film.Id}/match", new ApplyMatchRequest(70523));
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
}
}