using LiteCqrs; using TeleWave.Api.Common; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Images.DeleteImage; using TeleWave.Application.Images.UploadImage; using TeleWave.Application.Metadata; using TeleWave.Application.Metadata.ApplyShowMetadata; using TeleWave.Application.Metadata.ClearShowMetadata; using TeleWave.Application.Metadata.FindMissingEpisodes; 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.Domain.Images; using TeleWave.Infrastructure.Identity; namespace TeleWave.Api.Endpoints; public static class MetadataEndpoints { private const long MaxPosterBytes = 10L * 1024 * 1024; // 10 МБ private static readonly IReadOnlySet PosterExtensions = new HashSet( 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>(); admin.MapGet("/search", Search).Produces>(); 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 .MapPut("/shows/{showId:guid}/poster-image", SetPosterImage) .Produces(StatusCodes.Status204NoContent); admin.MapPost("/shows/{showId:guid}/refresh-episodes", RefreshEpisodes).Produces(); admin .MapGet("/shows/{showId:guid}/missing-episodes", FindMissing) .Produces(); // Постеры шоу и кадры серий теперь в общем реестре и отдаются по /api/images/{id}. return app; } private static async Task GetProviders( ISender sender, CancellationToken cancellationToken ) { var providers = await sender.Send(new GetMetadataProvidersQuery(), cancellationToken); return Results.Ok(providers); } private static async Task 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 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 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 Clear( Guid showId, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new ClearShowMetadataCommand(showId), cancellationToken); return result.ToHttpResult(); } private static async Task UploadPoster( Guid showId, string fileName, HttpRequest request, IImageStore 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(); // Регистрируем постер в общем реестре (категория ShowPoster) и привязываем к шоу. var created = await sender.Send( new UploadImageCommand(ImageCategory.ShowPoster, ext, fileName), cancellationToken ); if (!created.IsSuccess) return created.ToHttpResult(); try { await imageStore.SaveAsync(created.Value, ext, request.Body, cancellationToken); } catch { await sender.Send(new DeleteImageCommand(created.Value), cancellationToken); throw; } var result = await sender.Send( new SetShowPosterCommand(showId, created.Value), cancellationToken ); if (!result.IsSuccess) await sender.Send(new DeleteImageCommand(created.Value), cancellationToken); return result.ToHttpResult(); } private static async Task SetPosterImage( Guid showId, SetPosterImageBody body, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send( new SetShowPosterCommand(showId, body.ImageId), cancellationToken ); return result.ToHttpResult(); } private static async Task RefreshEpisodes( Guid showId, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send( new RefreshShowEpisodesMetadataCommand(showId), cancellationToken ); return result.ToHttpResult(); } private static async Task FindMissing( Guid showId, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new FindMissingEpisodesQuery(showId), cancellationToken); return result.ToHttpResult(); } } public sealed record ApplyMetadataBody(string Provider, string ExternalId); public sealed record UpdateMetadataBody(string? Description, int? Year); public sealed record SetPosterImageBody(Guid? ImageId);