diff --git a/server/src/Flaro.Api/Endpoints/MatchEndpoints.cs b/server/src/Flaro.Api/Endpoints/MatchEndpoints.cs index b95b44c..78432b3 100644 --- a/server/src/Flaro.Api/Endpoints/MatchEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/MatchEndpoints.cs @@ -149,7 +149,7 @@ public static class MatchEndpoints app.MapPut("/api/v1/movies/{id:guid}/match", async ( Guid id, ApplyMatchRequest anfrage, FlaroContext db, - IMetadataProvider anbieter, MatchApplier anwender) => + IMetadataProvider anbieter, MatchApplier anwender, Zusammenlegung zusammen) => { var film = await db.Movies.FirstOrDefaultAsync(m => m.Id == id); if (film is null) return Results.NotFound(); @@ -159,6 +159,33 @@ public static class MatchEndpoints return Results.Problem($"TMDB kennt den Film {anfrage.TmdbId} nicht.", statusCode: StatusCodes.Status404NotFound); + // **Hat die Kennung schon jemand, ist es derselbe Film.** + // + // Ein Film kommt je Mediathek genau einmal vor — das erzwingt ein + // Eindeutigkeitsindex. Die Kennung blind zu setzen liess ihn + // zuschlagen, und der Benutzer bekam „An error occurred while + // processing your request." zu sehen: eine englische + // Allgemeinmeldung für einen Fall, der gar kein Fehler ist. + // + // Zwei Dateien, die denselben Film meinen, gehören zusammengelegt + // — genau das tut der Scan am Ende jedes Laufs auch. Der + // Sehfortschritt wird dabei gefaltet, nichts geht verloren. + var schonDa = await db.Movies + .Where(m => m.LibraryId == film.LibraryId && m.TmdbId == anfrage.TmdbId && m.Id != id) + .Select(m => m.Id) + .FirstOrDefaultAsync(); + + if (schonDa != Guid.Empty) + { + await anwender.ApplyAsync( + (await db.Movies.FirstAsync(m => m.Id == schonDa)), gefunden); + await zusammen.FilmVerschmelzenAsync(sieger: schonDa, verlierer: id); + + var sieger = await db.Movies.AsNoTracking().FirstAsync(m => m.Id == schonDa); + return Results.Ok(new MatchState( + sieger.TmdbId, sieger.ImdbId, sieger.MatchLocked, sieger.Title, sieger.Year)); + } + await anwender.ApplyAsync(film, gefunden); return Results.Ok(new MatchState(film.TmdbId, film.ImdbId, film.MatchLocked, film.Title, film.Year)); }).RequireAuthorization(FlaroPolicies.Administrator); @@ -180,7 +207,7 @@ public static class MatchEndpoints app.MapPut("/api/v1/series/{id:guid}/match", async ( Guid id, ApplyMatchRequest anfrage, FlaroContext db, - IMetadataProvider anbieter, MatchApplier anwender) => + IMetadataProvider anbieter, MatchApplier anwender, Zusammenlegung zusammen) => { var serie = await db.Series.FirstOrDefaultAsync(s => s.Id == id); if (serie is null) return Results.NotFound(); @@ -190,6 +217,23 @@ public static class MatchEndpoints return Results.Problem($"TMDB kennt die Serie {anfrage.TmdbId} nicht.", statusCode: StatusCodes.Status404NotFound); + // Siehe bei den Filmen: dieselbe Kennung heisst dieselbe Serie, + // und zwei Ordner derselben Serie gehören zusammengelegt. + var schonDa = await db.Series + .Where(x => x.LibraryId == serie.LibraryId && x.TmdbId == anfrage.TmdbId && x.Id != id) + .Select(x => x.Id) + .FirstOrDefaultAsync(); + + if (schonDa != Guid.Empty) + { + await anwender.ApplyAsync(await db.Series.FirstAsync(x => x.Id == schonDa), gefunden); + await zusammen.SerieVerschmelzenAsync(sieger: schonDa, verlierer: id); + + var sieger = await db.Series.AsNoTracking().FirstAsync(x => x.Id == schonDa); + return Results.Ok(new MatchState( + sieger.TmdbId, sieger.ImdbId, sieger.MatchLocked, sieger.Title, sieger.Year)); + } + await anwender.ApplyAsync(serie, gefunden); return Results.Ok(new MatchState(serie.TmdbId, serie.ImdbId, serie.MatchLocked, serie.Title, serie.Year)); }).RequireAuthorization(FlaroPolicies.Administrator); diff --git a/server/src/Flaro.Api/Program.cs b/server/src/Flaro.Api/Program.cs index cd1198c..5b424dc 100644 --- a/server/src/Flaro.Api/Program.cs +++ b/server/src/Flaro.Api/Program.cs @@ -156,7 +156,33 @@ using (var bereich = app.Services.CreateScope()) await db.Database.MigrateAsync(); } -app.UseExceptionHandler(); +// **Auch das Letzte, was durchrutscht, spricht Deutsch.** +// +// Die Vorgabe von ASP.NET antwortet mit „An error occurred while processing +// your request." — englisch, ohne Grund, ohne nächsten Schritt. Genau das +// stand im Zuordnungsdialog, als eine Kennung doppelt vergeben werden +// sollte. Hier bekommt jede Ausnahme, die es bis nach oben schafft, einen +// deutschen Satz; die Einzelheiten gehen ins Tagesprotokoll, wo man sie +// sucht. +app.UseExceptionHandler(zweig => zweig.Run(async zusammenhang => +{ + var fehler = zusammenhang.Features + .Get()?.Error; + + if (fehler is not null) + { + zusammenhang.RequestServices.GetRequiredService().Schreiben( + $"{zusammenhang.Request.Method} {zusammenhang.Request.Path}: " + + Fehlertext.FuersProtokoll(fehler), + Tagebuch.Art.Fehler); + } + + zusammenhang.Response.StatusCode = StatusCodes.Status500InternalServerError; + await Results.Problem( + fehler is null ? "Etwas ist schiefgegangen." : Fehlertext.FuerMenschen(fehler), + statusCode: StatusCodes.Status500InternalServerError) + .ExecuteAsync(zusammenhang); +})); app.UseStatusCodePages(); app.UseRateLimiter(); diff --git a/server/tests/Flaro.Api.Tests/MetadataTests.cs b/server/tests/Flaro.Api.Tests/MetadataTests.cs index 899e93c..c5c62bb 100644 --- a/server/tests/Flaro.Api.Tests/MetadataTests.cs +++ b/server/tests/Flaro.Api.Tests/MetadataTests.cs @@ -354,6 +354,53 @@ public class MetadataTests "Gesucht wird mit 2016 aus dem Dateinamen, nicht mit 2022 aus dem Treffer."); } + /// Zwei Dateien auf denselben Film zu legen ist kein Fehler. + /// + /// + /// Aus dem echten Bestand. Im Zuordnungsdialog stand plötzlich „An + /// error occurred while processing your request." — englisch, ohne Grund. + /// Der gewählte Film war in derselben Mediathek schon vergeben, der + /// Eindeutigkeitsindex schlug zu, und die Ausnahme ging ungefiltert nach + /// oben. + /// + /// Es ist aber gar kein Fehler: zwei Dateien, die denselben Film meinen, + /// gehören zusammengelegt — genau das tut der Scan am Ende jedes Laufs. + /// + [Test] + public async Task Zwei_Dateien_auf_denselben_Film_werden_zusammengelegt() + { + Datei("Breaking Dawn Teil 1 (2011).mkv"); + Datei("Breaking Dawn (2011).mkv"); + + // Beim Scan findet keiner etwas — so bleiben es zwei Einträge. + var anbieter = new VorgegebenerAnbieter { Film = null }; + + using var server = MitAnbieter(anbieter); + var (client, bibliothek) = await Einrichten(server, "Movies"); + await Scannen(client, bibliothek); + + var filme = await client.GetFromJsonAsync>("/api/v1/movies"); + Assert.That(filme!.Total, Is.EqualTo(2), "Zwei Dateien, zwei Einträge ohne Zuordnung."); + + anbieter.Film = new MovieMetadata(50619, "Breaking Dawn – Teil 1", 2011, null, null, null) + { + Language = "de-DE", + }; + + var erste = await client.PutAsJsonAsync( + $"/api/v1/movies/{filme.Items[0].Id}/match", new ApplyMatchRequest(50619)); + Assert.That(erste.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + var zweite = await client.PutAsJsonAsync( + $"/api/v1/movies/{filme.Items[1].Id}/match", new ApplyMatchRequest(50619)); + Assert.That(zweite.StatusCode, Is.EqualTo(HttpStatusCode.OK), + "Die zweite Zuordnung darf nicht scheitern."); + + var danach = await client.GetFromJsonAsync>("/api/v1/movies"); + Assert.That(danach!.Total, Is.EqualTo(1), "Ein Film."); + Assert.That(danach.Items[0].Files, Has.Count.EqualTo(2), "Mit beiden Dateien."); + } + [Test] public async Task Ein_Film_ohne_Treffer_behaelt_seinen_Dateinamen() {