196 lines
6.7 KiB
C#
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);
|