diff --git a/backend/Musify.Api/Endpoints/PlayListEndpoints.cs b/backend/Musify.Api/Endpoints/PlayListEndpoints.cs index 134b7a9..37abeb3 100644 --- a/backend/Musify.Api/Endpoints/PlayListEndpoints.cs +++ b/backend/Musify.Api/Endpoints/PlayListEndpoints.cs @@ -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>() .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 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 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}"); diff --git a/backend/Musify.Api/Validators/PlayLists/CreatePlayListRequestValidator.cs b/backend/Musify.Api/Validators/PlayLists/CreatePlayListRequestValidator.cs index 0664e82..b0003b7 100644 --- a/backend/Musify.Api/Validators/PlayLists/CreatePlayListRequestValidator.cs +++ b/backend/Musify.Api/Validators/PlayLists/CreatePlayListRequestValidator.cs @@ -12,7 +12,6 @@ public sealed class CreatePlayListRequestValidator : AbstractValidator x.Description) - .NotEmpty() .MaximumLength(256); } } diff --git a/backend/Musify.Api/Validators/PlayLists/UpdatePlayListRequestValidator.cs b/backend/Musify.Api/Validators/PlayLists/UpdatePlayListRequestValidator.cs index 7d35d9b..c18fd81 100644 --- a/backend/Musify.Api/Validators/PlayLists/UpdatePlayListRequestValidator.cs +++ b/backend/Musify.Api/Validators/PlayLists/UpdatePlayListRequestValidator.cs @@ -13,7 +13,6 @@ public sealed class UpdatePlayListRequestValidator : AbstractValidator x.NewName is not null); RuleFor(x => x.NewDescription) - .NotEmpty() .MaximumLength(256) .When(x => x.NewDescription is not null); } diff --git a/backend/Musify.Application/Musify.Application.csproj b/backend/Musify.Application/Musify.Application.csproj index e6266ce..ce975d0 100644 --- a/backend/Musify.Application/Musify.Application.csproj +++ b/backend/Musify.Application/Musify.Application.csproj @@ -13,6 +13,7 @@ + diff --git a/backend/Musify.Application/PlayLists/GetPlayListCover.cs b/backend/Musify.Application/PlayLists/GetPlayListCover.cs new file mode 100644 index 0000000..ea3d87b --- /dev/null +++ b/backend/Musify.Application/PlayLists/GetPlayListCover.cs @@ -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>; + + public record PlayListCoverLocation(string Bucket, string Key, string ContentType); + + public class GetPlayListCoverQueryHandler( + IDatabase database, + ApplicationStorageConfiguration storageConfiguration, + PlayListConfiguration playListConfiguration) + : IQueryHandler> + { + public async ValueTask> 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 builder, string? name) + => string.IsNullOrEmpty(name) ? null : builder(name); + } +}