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