Implement image management enhancements: add functionality to relocate legacy images during startup, update image handling in show metadata, and refactor related API endpoints to utilize the new image storage system. Adjust database schema to support image references and update UI components for improved image selection and display.

This commit is contained in:
Leonid Pershin
2026-07-25 11:43:17 +03:00
parent 149cd153b9
commit a970eae7d2
30 changed files with 1231 additions and 172 deletions
@@ -2,6 +2,8 @@ using LiteCqrs;
using Microsoft.EntityFrameworkCore;
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;
@@ -10,6 +12,7 @@ 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;
@@ -40,12 +43,13 @@ public static class MetadataEndpoints
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<int>();
// Постеры/кадры отдаём публично (просто картинки, id не угадать) — чтобы работал <img src>.
app.MapGet("/api/metadata/shows/{showId:guid}/poster", ServePoster)
.WithTags("Metadata")
.Produces(StatusCodes.Status200OK);
// Кадры серий отдаём публично (просто картинки, id не угадать) — чтобы работал <img src>.
// Постеры шоу теперь в общем реестре и отдаются по /api/images/{id}.
app.MapGet("/api/metadata/episodes/{episodeId:guid}/still", ServeStill)
.WithTags("Metadata")
.Produces(StatusCodes.Status200OK);
@@ -112,7 +116,7 @@ public static class MetadataEndpoints
Guid showId,
string fileName,
HttpRequest request,
IMetadataImageStore imageStore,
IImageStore imageStore,
ISender sender,
CancellationToken cancellationToken
)
@@ -124,10 +128,44 @@ public static class MetadataEndpoints
)
return MetadataErrors.InvalidPoster.ToProblem();
var relative = await imageStore.SaveShowPosterAsync(showId, ext, request.Body, cancellationToken);
var result = await sender.Send(new SetShowPosterCommand(showId, relative), cancellationToken);
// Регистрируем постер в общем реестре (категория 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)
imageStore.DeleteShowImages(showId);
await sender.Send(new DeleteImageCommand(created.Value), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> SetPosterImage(
Guid showId,
SetPosterImageBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new SetShowPosterCommand(showId, body.ImageId),
cancellationToken
);
return result.ToHttpResult();
}
@@ -144,20 +182,6 @@ public static class MetadataEndpoints
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,
@@ -193,3 +217,5 @@ public static class MetadataEndpoints
public sealed record ApplyMetadataBody(string Provider, string ExternalId);
public sealed record UpdateMetadataBody(string? Description, int? Year);
public sealed record SetPosterImageBody(Guid? ImageId);
+1
View File
@@ -93,6 +93,7 @@ var app = builder.Build();
// Авто-применение миграций и идемпотентный сидинг (роли + админ из env) на старте.
await app.Services.ApplyMigrationsAsync();
await app.Services.RelocateLegacyImagesAsync();
await app.Services.SeedDataAsync();
app.UseForwardedHeaders();