using Flaro.Data; using Microsoft.EntityFrameworkCore; namespace Flaro.Api.Endpoints; /// Neue Funde prüfen, veröffentlichen oder auf einen Zeitpunkt legen. public static class FreigabeEndpoints { public static void MapFreigabe(this IEndpointRouteBuilder app) { var gruppe = app.MapGroup("/api/v1/review") .RequireAuthorization(FlaroPolicies.Administrator); // **Was noch wartet, und was schon geplant ist.** // // Beides in einer Liste: ein geplanter Titel ist nicht erledigt, er // hat nur schon einen Termin. Ihn verschwinden zu lassen hiesse, dass // man ihn nur findet, wenn man sich den Termin gemerkt hat. gruppe.MapGet("/", async (FlaroContext db, TimeProvider uhr, CancellationToken ct) => { var jetzt = uhr.GetUtcNow(); var filme = await db.Movies.AsNoTracking() .Where(m => m.PublishAt == null || m.PublishAt > jetzt) .OrderBy(m => m.SortTitleValue) .Select(m => new Pruefeintrag( "movie", m.Id, m.Title, m.Year, m.PosterImage, m.PublishAt, db.MediaFiles.Where(f => f.MovieId == m.Id).Select(f => f.Path).FirstOrDefault(), m.TmdbId != null, 1, new List { m.Id })) .ToListAsync(ct); // **Folgen werden nach Serie gebündelt.** Zwanzig neue Folgen // derselben Serie sind eine Entscheidung, nicht zwanzig. var offeneFolgen = await db.Episodes.AsNoTracking() .Where(f => f.PublishAt == null || f.PublishAt > jetzt) .Select(f => new { f.Id, f.SeriesId, f.SeasonNumber, f.EpisodeNumber, f.PublishAt, Pfad = db.MediaFiles.Where(d => d.EpisodeId == f.Id) .Select(d => d.Path).FirstOrDefault(), }) .ToListAsync(ct); var serienIds = offeneFolgen.Select(f => f.SeriesId).Distinct().ToList(); var serien = await db.Series.AsNoTracking() .Where(s => serienIds.Contains(s.Id)) .Select(s => new { s.Id, s.Title, s.Year, s.PosterImage, Zugeordnet = s.TmdbId != null }) .ToDictionaryAsync(s => s.Id, ct); var gebuendelt = offeneFolgen .GroupBy(f => f.SeriesId) .Select(g => { var serie = serien.GetValueOrDefault(g.Key); var termine = g.Select(f => f.PublishAt).Distinct().ToList(); return new Pruefeintrag( "series", g.Key, serie?.Title ?? "Unbekannte Serie", serie?.Year, serie?.PosterImage, // Nur wenn alle denselben Termin tragen — sonst wäre es // eine Behauptung über Folgen, die anders liegen. termine.Count == 1 ? termine[0] : null, Pfad(g.Select(f => f.Pfad).FirstOrDefault()), serie?.Zugeordnet ?? false, g.Count(), g.OrderBy(f => f.SeasonNumber).ThenBy(f => f.EpisodeNumber) .Select(f => f.Id).ToList()); }) .OrderBy(e => e.Title) .ToList(); return Results.Ok(filme.Concat(gebuendelt).ToList()); // Bei einer Serie sagt der Ordner mehr als die erste Folgendatei. static string? Pfad(string? datei) => datei is null ? null : System.IO.Path.GetDirectoryName(datei); }); // Veröffentlichen oder planen. Ohne Zeitpunkt heisst: jetzt. gruppe.MapPost("/publish", async ( FreigabeRequest anfrage, FlaroContext db, TimeProvider uhr, CancellationToken ct) => { var wann = anfrage.PublishAt ?? uhr.GetUtcNow(); // Alles Wartende einer einzelnen Mediathek — der Knopf, der dort // steht, wo man gerade gescannt hat. if (anfrage.LibraryId is { } mediathek) { var jetztDort = uhr.GetUtcNow(); var dortFilme = await db.Movies .Where(m => m.LibraryId == mediathek && (m.PublishAt == null || m.PublishAt > jetztDort)) .ExecuteUpdateAsync(s => s.SetProperty(m => m.PublishAt, wann), ct); var dortFolgen = await db.Episodes .Where(f => db.Series.Any(s => s.Id == f.SeriesId && s.LibraryId == mediathek) && (f.PublishAt == null || f.PublishAt > jetztDort)) .ExecuteUpdateAsync(s => s.SetProperty(f => f.PublishAt, wann), ct); return Results.Ok(new { published = dortFilme + dortFolgen }); } // **Alles auf einmal, wenn keine Kennungen dabei sind.** Bei // vierhundert neuen Titeln wäre einzeln Klicken keine Bedienung. if (anfrage.Ids is null or { Count: 0 }) { // **Alles, was noch nicht sichtbar ist** — Wartendes und // Geplantes. Nur das Wartende zu fassen hiesse: ein auf // nächste Woche gelegter Titel bliebe liegen, obwohl er in // derselben Liste stand und „Alle veröffentlichen" gedrückt // wurde. var jetzt = uhr.GetUtcNow(); var alleFilme = await db.Movies .Where(m => m.PublishAt == null || m.PublishAt > jetzt) .ExecuteUpdateAsync(s => s.SetProperty(m => m.PublishAt, wann), ct); var alleFolgen = await db.Episodes .Where(f => f.PublishAt == null || f.PublishAt > jetzt) .ExecuteUpdateAsync(s => s.SetProperty(f => f.PublishAt, wann), ct); return Results.Ok(new { published = alleFilme + alleFolgen }); } var ids = anfrage.Ids; var filme = await db.Movies.Where(m => ids.Contains(m.Id)) .ExecuteUpdateAsync(s => s.SetProperty(m => m.PublishAt, wann), ct); var folgen = await db.Episodes.Where(f => ids.Contains(f.Id)) .ExecuteUpdateAsync(s => s.SetProperty(f => f.PublishAt, wann), ct); return Results.Ok(new { published = filme + folgen }); }); // Zurückziehen — wieder in die Prüfung. gruppe.MapPost("/withhold", async ( FreigabeRequest anfrage, FlaroContext db, CancellationToken ct) => { if (anfrage.Ids is null or { Count: 0 }) return Results.BadRequest(new { fehler = "Ohne Kennungen gibt es nichts zurückzuziehen." }); var ids = anfrage.Ids; var filme = await db.Movies.Where(m => ids.Contains(m.Id)) .ExecuteUpdateAsync(s => s.SetProperty(m => m.PublishAt, (DateTimeOffset?)null), ct); var folgen = await db.Episodes.Where(f => ids.Contains(f.Id)) .ExecuteUpdateAsync(s => s.SetProperty(f => f.PublishAt, (DateTimeOffset?)null), ct); return Results.Ok(new { withheld = filme + folgen }); }); } }