Files
TeleWave/backend/src/TeleWave.Api/Endpoints/MetadataEndpoints.cs
T

196 lines
6.7 KiB
C#

using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Api.Common;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Metadata;
using TeleWave.Application.Metadata.ApplyShowMetadata;
using TeleWave.Application.Metadata.ClearShowMetadata;
using TeleWave.Application.Metadata.GetProviders;
using TeleWave.Application.Metadata.RefreshEpisodes;
using TeleWave.Application.Metadata.SearchShows;
using TeleWave.Application.Metadata.SetShowPoster;
using TeleWave.Application.Metadata.UpdateShowMetadata;
using TeleWave.Infrastructure.Identity;
namespace TeleWave.Api.Endpoints;
public static class MetadataEndpoints
{
private const long MaxPosterBytes = 10L * 1024 * 1024; // 10 МБ
private static readonly IReadOnlySet<string> PosterExtensions = new HashSet<string>(
StringComparer.OrdinalIgnoreCase
)
{
".jpg",
".jpeg",
".png",
".webp",
};
public static IEndpointRouteBuilder MapMetadataEndpoints(this IEndpointRouteBuilder app)
{
var admin = app.MapGroup("/api/admin/metadata")
.WithTags("Admin.Metadata")
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
admin.MapGet("/providers", GetProviders).Produces<IReadOnlyList<string>>();
admin.MapGet("/search", Search).Produces<IReadOnlyList<MetadataCandidate>>();
admin.MapPost("/shows/{showId:guid}/apply", Apply).Produces(StatusCodes.Status204NoContent);
admin.MapPut("/shows/{showId:guid}", Update).Produces(StatusCodes.Status204NoContent);
admin.MapDelete("/shows/{showId:guid}", Clear).Produces(StatusCodes.Status204NoContent);
admin.MapPut("/shows/{showId:guid}/poster", UploadPoster).Produces(StatusCodes.Status204NoContent);
admin.MapPost("/shows/{showId:guid}/refresh-episodes", RefreshEpisodes).Produces<int>();
// Постеры/кадры отдаём публично (просто картинки, id не угадать) — чтобы работал <img src>.
app.MapGet("/api/metadata/shows/{showId:guid}/poster", ServePoster)
.WithTags("Metadata")
.Produces(StatusCodes.Status200OK);
app.MapGet("/api/metadata/episodes/{episodeId:guid}/still", ServeStill)
.WithTags("Metadata")
.Produces(StatusCodes.Status200OK);
return app;
}
private static async Task<IResult> GetProviders(ISender sender, CancellationToken cancellationToken)
{
var providers = await sender.Send(new GetMetadataProvidersQuery(), cancellationToken);
return Results.Ok(providers);
}
private static async Task<IResult> Search(
string provider,
string query,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new SearchShowMetadataQuery(provider, query), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> Apply(
Guid showId,
ApplyMetadataBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new ApplyShowMetadataCommand(showId, body.Provider, body.ExternalId),
cancellationToken
);
return result.ToHttpResult();
}
private static async Task<IResult> Update(
Guid showId,
UpdateMetadataBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new UpdateShowMetadataCommand(showId, body.Description, body.Year),
cancellationToken
);
return result.ToHttpResult();
}
private static async Task<IResult> Clear(
Guid showId,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new ClearShowMetadataCommand(showId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> UploadPoster(
Guid showId,
string fileName,
HttpRequest request,
IMetadataImageStore imageStore,
ISender sender,
CancellationToken cancellationToken
)
{
var ext = Path.GetExtension(fileName).ToLowerInvariant();
if (
request.ContentLength is > MaxPosterBytes or 0 or null
|| !PosterExtensions.Contains(ext)
)
return MetadataErrors.InvalidPoster.ToProblem();
var relative = await imageStore.SaveShowPosterAsync(showId, ext, request.Body, cancellationToken);
var result = await sender.Send(new SetShowPosterCommand(showId, relative), cancellationToken);
if (!result.IsSuccess)
imageStore.DeleteShowImages(showId);
return result.ToHttpResult();
}
private static async Task<IResult> RefreshEpisodes(
Guid showId,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new RefreshShowEpisodesMetadataCommand(showId),
cancellationToken
);
return result.ToHttpResult();
}
private static async Task<IResult> ServePoster(
Guid showId,
IAppDbContext dbContext,
IMetadataImageStore imageStore,
CancellationToken cancellationToken
)
{
var path = await dbContext.Shows.AsNoTracking()
.Where(s => s.Id == showId)
.Select(s => s.PosterPath)
.FirstOrDefaultAsync(cancellationToken);
return ServeImage(path, imageStore);
}
private static async Task<IResult> ServeStill(
Guid episodeId,
IAppDbContext dbContext,
IMetadataImageStore imageStore,
CancellationToken cancellationToken
)
{
var path = await dbContext.Shows.AsNoTracking()
.SelectMany(s => s.Episodes)
.Where(e => e.Id == episodeId)
.Select(e => e.StillPath)
.FirstOrDefaultAsync(cancellationToken);
return ServeImage(path, imageStore);
}
private static IResult ServeImage(string? relativePath, IMetadataImageStore imageStore)
{
if (string.IsNullOrEmpty(relativePath))
return Results.NotFound();
var abs = imageStore.ResolveAbsolutePath(relativePath);
return abs is null ? Results.NotFound() : Results.File(abs, ContentTypeFor(Path.GetExtension(abs)));
}
private static string ContentTypeFor(string extension) =>
extension.ToLowerInvariant() switch
{
".png" => "image/png",
".webp" => "image/webp",
_ => "image/jpeg",
};
}
public sealed record ApplyMetadataBody(string Provider, string ExternalId);
public sealed record UpdateMetadataBody(string? Description, int? Year);