feat(backend): add playlist cover endpoint and relax description validation
Adds GET /playlists/{id}/cover (mirrors the track cover endpoint) so
uploaded playlist pictures can actually be served. Also drops the
NotEmpty rule on playlist description, which rejected the blank
description the frontend treats as optional; CreatePlayList now falls
back to the domain's default text instead.
This commit is contained in:
@@ -3,6 +3,7 @@ using Musify.Application.PlayLists;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Api.Authentication;
|
||||
using Musify.Api.DataTransferObjects.PlayLists;
|
||||
using Musify.Api.Extensions;
|
||||
@@ -39,6 +40,12 @@ public static class PlayListEndpoints
|
||||
.Produces<PaginatedResponse<TrackApplicationResponse>>()
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapGet("/{id}/cover", GetPlayListCover)
|
||||
.WithName("GetPlayListCover")
|
||||
.WithSummary("Get A PlayList Cover Image.")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/", CreatePlayList)
|
||||
.WithName("CreatePlayList")
|
||||
.WithSummary("Create A New PlayList.")
|
||||
@@ -139,14 +146,36 @@ public static class PlayListEndpoints
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetPlayListCover(
|
||||
IMediator mediator,
|
||||
IStorageService storageService,
|
||||
HttpResponse response,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken,
|
||||
string size = "medium")
|
||||
{
|
||||
var result = await mediator.Send(new GetPlayListCoverQuery(id, size), cancellationToken);
|
||||
if (result.IsError)
|
||||
return Results.NotFound();
|
||||
|
||||
var location = result.Value;
|
||||
var stream = await storageService.GetFileAsync(location.Bucket, location.Key, cancellationToken);
|
||||
response.Headers.CacheControl = "public, max-age=31536000, immutable";
|
||||
return Results.Stream(stream, location.ContentType);
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreatePlayList(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
CreatePlayListRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var description = string.IsNullOrWhiteSpace(request.Description)
|
||||
? "No description was provided."
|
||||
: request.Description;
|
||||
|
||||
var result = await mediator.Send(
|
||||
new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId),
|
||||
new CreatePlayListCommand(currentUser.RequiredId, request.Name, description, request.PictureIntentId),
|
||||
cancellationToken);
|
||||
|
||||
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
|
||||
|
||||
@@ -12,7 +12,6 @@ public sealed class CreatePlayListRequestValidator : AbstractValidator<CreatePla
|
||||
.MaximumLength(50);
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.NotEmpty()
|
||||
.MaximumLength(256);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ public sealed class UpdatePlayListRequestValidator : AbstractValidator<UpdatePla
|
||||
.When(x => x.NewName is not null);
|
||||
|
||||
RuleFor(x => x.NewDescription)
|
||||
.NotEmpty()
|
||||
.MaximumLength(256)
|
||||
.When(x => x.NewDescription is not null);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
|
||||
<PackageReference Include="MimeMapping" />
|
||||
<PackageReference Include="X.PagedList" />
|
||||
<PackageReference Include="X.PagedList.EF" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MimeMapping;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public record GetPlayListCoverQuery(Guid PlayListId, string Size)
|
||||
: IQuery<ErrorOr<PlayListCoverLocation>>;
|
||||
|
||||
public record PlayListCoverLocation(string Bucket, string Key, string ContentType);
|
||||
|
||||
public class GetPlayListCoverQueryHandler(
|
||||
IDatabase database,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
PlayListConfiguration playListConfiguration)
|
||||
: IQueryHandler<GetPlayListCoverQuery, ErrorOr<PlayListCoverLocation>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PlayListCoverLocation>> Handle(GetPlayListCoverQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists.AsNoTracking()
|
||||
.Where(p => p.Id == request.PlayListId)
|
||||
.Select(p => new { p.SmallPictureName, p.MediumPictureName, p.LargePictureName })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (playList == null)
|
||||
return Error.NotFound();
|
||||
|
||||
var routes = playListConfiguration.Routes;
|
||||
var (name, key, presetName) = request.Size.ToLowerInvariant() switch
|
||||
{
|
||||
"small" => (playList.SmallPictureName, BuildKey(routes.BuildSmallPicturePath, playList.SmallPictureName), routes.PresetSmallPicture),
|
||||
"large" => (playList.LargePictureName, BuildKey(routes.BuildLargePicturePath, playList.LargePictureName), routes.PresetLargePicture),
|
||||
_ => (playList.MediumPictureName, BuildKey(routes.BuildMediumPicturePath, playList.MediumPictureName), routes.PresetMediumPicture)
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(key) || name == presetName)
|
||||
return Error.NotFound();
|
||||
|
||||
var contentType = MimeUtility.GetMimeMapping(name);
|
||||
return new PlayListCoverLocation(storageConfiguration.Bucket, key, contentType);
|
||||
}
|
||||
|
||||
private static string? BuildKey(Func<string, string> builder, string? name)
|
||||
=> string.IsNullOrEmpty(name) ? null : builder(name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user