using System.Diagnostics; using System.Net; using System.Net.Http.Json; using Halo.Api.Endpoints; using Halo.Core.Library; using Halo.Core.Metadata; using Halo.Media; using Microsoft.Extensions.DependencyInjection; namespace Halo.Api.Tests; /// Eine falsche Zuordnung richtigstellen — und sie festschreiben. public class MatchCorrectionTests { private string _bestand = ""; private string _filmOrdner = ""; private HaloServer? _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(), "halo-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 HaloServer(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 bibliotheken = await client.GetFromJsonAsync>("/api/v1/libraries"); (await client.PostAsync($"/api/v1/libraries/{bibliotheken!.Single().Id}/scan", null)) .EnsureSuccessStatusCode(); for (var i = 0; i < 150; i++) { var stand = await client.GetFromJsonAsync( $"/api/v1/libraries/{bibliotheken.Single().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)); } }