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:
2026-07-15 16:09:40 +02:00
parent a10426b3f1
commit b4b08aa3fd
5 changed files with 80 additions and 3 deletions
@@ -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);
}