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;
}
}