Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
5 changed files with 80 additions and 3 deletions
Showing only changes of commit b4b08aa3fd - Show all commits
@@ -3,6 +3,7 @@ using Musify.Application.PlayLists;
using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Responses;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Contracts;
using Musify.Api.Authentication; using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.PlayLists; using Musify.Api.DataTransferObjects.PlayLists;
using Musify.Api.Extensions; using Musify.Api.Extensions;
@@ -39,6 +40,12 @@ public static class PlayListEndpoints
.Produces<PaginatedResponse<TrackApplicationResponse>>() .Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound); .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) group.MapPost("/", CreatePlayList)
.WithName("CreatePlayList") .WithName("CreatePlayList")
.WithSummary("Create A New PlayList.") .WithSummary("Create A New PlayList.")
@@ -139,14 +146,36 @@ public static class PlayListEndpoints
return result.ToHttpResult(); 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( private static async Task<IResult> CreatePlayList(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
CreatePlayListRequest request, CreatePlayListRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var description = string.IsNullOrWhiteSpace(request.Description)
? "No description was provided."
: request.Description;
var result = await mediator.Send( 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); cancellationToken);
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}"); return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
@@ -12,7 +12,6 @@ public sealed class CreatePlayListRequestValidator : AbstractValidator<CreatePla
.MaximumLength(50); .MaximumLength(50);
RuleFor(x => x.Description) RuleFor(x => x.Description)
.NotEmpty()
.MaximumLength(256); .MaximumLength(256);
} }
} }
@@ -13,7 +13,6 @@ public sealed class UpdatePlayListRequestValidator : AbstractValidator<UpdatePla
.When(x => x.NewName is not null); .When(x => x.NewName is not null);
RuleFor(x => x.NewDescription) RuleFor(x => x.NewDescription)
.NotEmpty()
.MaximumLength(256) .MaximumLength(256)
.When(x => x.NewDescription is not null); .When(x => x.NewDescription is not null);
} }
@@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" /> <PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
<PackageReference Include="MimeMapping" />
<PackageReference Include="X.PagedList" /> <PackageReference Include="X.PagedList" />
<PackageReference Include="X.PagedList.EF" /> <PackageReference Include="X.PagedList.EF" />
</ItemGroup> </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);
}
}