using Flaro.Core.Metadata; using Flaro.Data; using Flaro.Metadata; using Microsoft.EntityFrameworkCore; namespace Flaro.Api.Endpoints; /// Plakat, Hintergrund und Schriftzug von Hand wählen. /// /// /// /// Wofür. Es gab genau einen Bild-Endpunkt, und der lieferte nur aus. /// Wählte die Metadatenquelle ein schlechtes Plakat, blieb es — und die /// einzige Abhilfe war, den ganzen Titel einem anderen Eintrag zuzuordnen. /// /// /// Die Kandidaten kosten nichts. TMDB liefert sie beim Einzelabruf /// ohnehin mit; bisher wurde alles ausser dem Schriftzug weggeworfen. /// /// public static class ArtworkEndpoints { /// Höchstens so gross darf ein eigenes Bild sein. /// /// /// Zehn Megabyte reichen für jedes Plakat. Ohne Grenze lädt irgendwann /// jemand ein Foto hoch, und der Cache eines NAS ist nicht dafür da. /// private const long HoechstensBytes = 10 * 1024 * 1024; public static void MapArtwork(this IEndpointRouteBuilder app) { var gruppe = app.MapGroup("/api/v1") .RequireAuthorization(FlaroPolicies.Administrator); gruppe.MapGet("/{art:regex(^movies|series$)}/{id:guid}/artwork", async ( string art, Guid id, FlaroContext db, TmdbClient tmdb, CancellationToken ct) => { var serie = art == "series"; var kennung = serie ? await db.Series.Where(s => s.Id == id).Select(s => s.TmdbId).FirstOrDefaultAsync(ct) : await db.Movies.Where(m => m.Id == id).Select(m => m.TmdbId).FirstOrDefaultAsync(ct); var aktuell = serie ? await db.Series.Where(s => s.Id == id) .Select(s => new Bildstand(s.PosterImage, s.BackdropImage, s.LogoImage, s.PosterPinned, s.BackdropPinned, s.LogoPinned)).FirstOrDefaultAsync(ct) : await db.Movies.Where(m => m.Id == id) .Select(m => new Bildstand(m.PosterImage, m.BackdropImage, m.LogoImage, m.PosterPinned, m.BackdropPinned, m.LogoPinned)).FirstOrDefaultAsync(ct); if (aktuell is null) return Results.NotFound(); // Ohne Zuordnung gibt es nichts zu wählen — dann ist die // Zuordnung das Problem, nicht das Bild. var kandidaten = kennung is { } k ? await tmdb.BilderAsync(k, serie, ct) : []; return Results.Ok(new Bildauswahl(aktuell, kandidaten)); }); gruppe.MapPut("/{art:regex(^movies|series$)}/{id:guid}/artwork", async ( string art, Guid id, BildWahlRequest anfrage, FlaroContext db, IImageStore ablage, CancellationToken ct) => { if (anfrage.Kind is not ("poster" or "backdrop" or "logo")) return Einwand.Bitte("Erwartet: poster, backdrop oder logo."); // Das Bild wandert in den Cache, bevor irgendetwas gesetzt wird — // sonst zeigt der Eintrag auf etwas, das es hier nicht gibt. var schluessel = string.IsNullOrWhiteSpace(anfrage.Url) ? null : await ablage.StoreAsync(new RemoteImage(anfrage.Url), ct); if (anfrage.Url is { Length: > 0 } && schluessel is null) return Results.Problem("Das Bild liess sich nicht laden.", statusCode: 502); var gesetzt = art == "series" ? await Setzen(db.Series.Where(s => s.Id == id), anfrage.Kind, schluessel, ct) : await Setzen(db.Movies.Where(m => m.Id == id), anfrage.Kind, schluessel, ct); return gesetzt ? Results.NoContent() : Results.NotFound(); }); gruppe.MapPost("/{art:regex(^movies|series$)}/{id:guid}/artwork", async ( string art, Guid id, string kind, HttpRequest anfrage, FlaroContext db, IImageStore ablage, CancellationToken ct) => { if (kind is not ("poster" or "backdrop" or "logo")) return Einwand.Bitte("Erwartet: poster, backdrop oder logo."); if (!anfrage.HasFormContentType) return Einwand.Bitte("Kein Formular."); var form = await anfrage.ReadFormAsync(ct); var datei = form.Files.FirstOrDefault(); if (datei is null || datei.Length == 0) return Einwand.Bitte("Keine Datei dabei."); if (datei.Length > HoechstensBytes) return Einwand.Bitte("Höchstens 10 MB."); await using var strom = datei.OpenReadStream(); var schluessel = await ablage.StoreStreamAsync(strom, datei.ContentType, ct); if (schluessel is null) return Results.Problem("Das Bild liess sich nicht ablegen.", statusCode: 502); var gesetzt = art == "series" ? await Setzen(db.Series.Where(s => s.Id == id), kind, schluessel, ct) : await Setzen(db.Movies.Where(m => m.Id == id), kind, schluessel, ct); return gesetzt ? Results.Ok(new { image = schluessel }) : Results.NotFound(); }).DisableAntiforgery(); } /// Setzt ein Bild und merkt sich, dass es von Hand kam. private static async Task Setzen( IQueryable abfrage, string art, string? schluessel, CancellationToken ct) where T : class, IBebildert { var betroffen = art switch { "poster" => await abfrage.ExecuteUpdateAsync(s => s .SetProperty(x => x.PosterImage, schluessel) .SetProperty(x => x.PosterPinned, schluessel != null), ct), "backdrop" => await abfrage.ExecuteUpdateAsync(s => s .SetProperty(x => x.BackdropImage, schluessel) .SetProperty(x => x.BackdropPinned, schluessel != null), ct), _ => await abfrage.ExecuteUpdateAsync(s => s .SetProperty(x => x.LogoImage, schluessel) .SetProperty(x => x.LogoPinned, schluessel != null), ct), }; return betroffen > 0; } }