feat(api): expose the album endpoints
Eight routes under /albums covering the listing, the detail, the tracklist and the owner-only writes, wired to the same validation filter and ErrorOr mapping the playlists use.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Musify.Api.DataTransferObjects.Albums;
|
||||||
|
|
||||||
|
public record CreateAlbumRequest(
|
||||||
|
string Title,
|
||||||
|
string? Description,
|
||||||
|
int? ReleaseYear);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Musify.Api.DataTransferObjects.Albums;
|
||||||
|
|
||||||
|
public record UpdateAlbumRequest(
|
||||||
|
string NewTitle,
|
||||||
|
string? NewDescription,
|
||||||
|
int? NewReleaseYear);
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
using Mediator;
|
||||||
|
using Musify.Api.Authentication;
|
||||||
|
using Musify.Api.DataTransferObjects.Albums;
|
||||||
|
using Musify.Api.Extensions;
|
||||||
|
using Musify.Api.Filters;
|
||||||
|
using Musify.Application.Albums;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Shared;
|
||||||
|
using Musify.Application.Tracks.Responses;
|
||||||
|
|
||||||
|
namespace Musify.Api.Endpoints;
|
||||||
|
|
||||||
|
public static class AlbumEndpoints
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapAlbumEndpoints(this IEndpointRouteBuilder app)
|
||||||
|
{
|
||||||
|
var group = app.MapGroup("/albums")
|
||||||
|
.WithTags("Albums");
|
||||||
|
|
||||||
|
group.MapGet("/", GetAlbums)
|
||||||
|
.WithName("GetAlbums")
|
||||||
|
.WithSummary("Get Paginated Albums.")
|
||||||
|
.Produces<PaginatedResponse<AlbumApplicationResponse>>();
|
||||||
|
|
||||||
|
group.MapGet("/{id}", GetAlbumById)
|
||||||
|
.WithName("GetAlbumById")
|
||||||
|
.WithSummary("Get An Album By Id.")
|
||||||
|
.Produces<AlbumApplicationResponse>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapGet("/users/{userId}", GetAlbumsByUserId)
|
||||||
|
.WithName("GetAlbumsByUserId")
|
||||||
|
.WithSummary("Get Paginated Albums For A User.")
|
||||||
|
.Produces<PaginatedResponse<AlbumApplicationResponse>>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapGet("/{albumId}/tracks", GetAlbumTracks)
|
||||||
|
.WithName("GetAlbumTracks")
|
||||||
|
.WithSummary("Get The Tracks Of An Album Ordered By Track Number.")
|
||||||
|
.Produces<PaginatedResponse<TrackApplicationResponse>>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPost("/", CreateAlbum)
|
||||||
|
.WithName("CreateAlbum")
|
||||||
|
.WithSummary("Create A New Album.")
|
||||||
|
.AddEndpointFilter<ValidationFilter<CreateAlbumRequest>>()
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces<AlbumApplicationResponse>(StatusCodes.Status201Created)
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPut("/{albumId}", UpdateAlbum)
|
||||||
|
.WithName("UpdateAlbum")
|
||||||
|
.WithSummary("Update An Album.")
|
||||||
|
.AddEndpointFilter<ValidationFilter<UpdateAlbumRequest>>()
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces<AlbumApplicationResponse>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapDelete("/{albumId}", DeleteAlbum)
|
||||||
|
.WithName("DeleteAlbum")
|
||||||
|
.WithSummary("Delete An Album.")
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPost("/{albumId}/tracks/{trackId}", AddTrackToAlbum)
|
||||||
|
.WithName("AddTrackToAlbum")
|
||||||
|
.WithSummary("Add One Of Your Uploaded Tracks To An Album.")
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
|
group.MapDelete("/{albumId}/tracks/{trackId}", RemoveTrackFromAlbum)
|
||||||
|
.WithName("RemoveTrackFromAlbum")
|
||||||
|
.WithSummary("Remove A Track From An Album.")
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetAlbums(
|
||||||
|
IMediator mediator,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
string? title,
|
||||||
|
int pageNumber = 1,
|
||||||
|
int pageSize = 10)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetAlbumsQuery(title, pageNumber, pageSize), cancellationToken);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetAlbumById(
|
||||||
|
IMediator mediator,
|
||||||
|
Guid id,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetAlbumByIdQuery(id), cancellationToken);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetAlbumsByUserId(
|
||||||
|
IMediator mediator,
|
||||||
|
long userId,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
int pageNumber = 1,
|
||||||
|
int pageSize = 10)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetAlbumsByUserIdQuery(userId, pageNumber, pageSize), cancellationToken);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetAlbumTracks(
|
||||||
|
IMediator mediator,
|
||||||
|
Guid albumId,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
int pageNumber = 1,
|
||||||
|
int pageSize = 50)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetAlbumTracksQuery(albumId, pageNumber, pageSize), cancellationToken);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> CreateAlbum(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
CreateAlbumRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(
|
||||||
|
new CreateAlbumCommand(currentUser.RequiredId, request.Title, request.Description, request.ReleaseYear),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return result.ToCreatedResult(album => $"/albums/{album.Id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> UpdateAlbum(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
Guid albumId,
|
||||||
|
UpdateAlbumRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(
|
||||||
|
new UpdateAlbumCommand(currentUser.RequiredId, albumId, request.NewTitle, request.NewDescription, request.NewReleaseYear),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> DeleteAlbum(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
Guid albumId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new DeleteAlbumCommand(currentUser.RequiredId, albumId), cancellationToken);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> AddTrackToAlbum(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
Guid albumId,
|
||||||
|
Guid trackId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(
|
||||||
|
new AddTrackToAlbumCommand(currentUser.RequiredId, albumId, trackId),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> RemoveTrackFromAlbum(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
Guid albumId,
|
||||||
|
Guid trackId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(
|
||||||
|
new RemoveTrackFromAlbumCommand(currentUser.RequiredId, albumId, trackId),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ app.UseAuthentication();
|
|||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app
|
app
|
||||||
|
.MapAlbumEndpoints()
|
||||||
.MapPlayListEndpoints()
|
.MapPlayListEndpoints()
|
||||||
.MapTrackEndpoints()
|
.MapTrackEndpoints()
|
||||||
.MapUserEndpoints()
|
.MapUserEndpoints()
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Musify.Api.Validators.Albums;
|
||||||
|
|
||||||
|
public static class AlbumReleaseYear
|
||||||
|
{
|
||||||
|
public const int Earliest = 1877;
|
||||||
|
|
||||||
|
public static int Latest => DateTime.UtcNow.Year + 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Musify.Api.DataTransferObjects.Albums;
|
||||||
|
|
||||||
|
namespace Musify.Api.Validators.Albums;
|
||||||
|
|
||||||
|
public sealed class CreateAlbumRequestValidator : AbstractValidator<CreateAlbumRequest>
|
||||||
|
{
|
||||||
|
public CreateAlbumRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Title)
|
||||||
|
.NotEmpty()
|
||||||
|
.MaximumLength(200);
|
||||||
|
|
||||||
|
RuleFor(x => x.Description)
|
||||||
|
.MaximumLength(256);
|
||||||
|
|
||||||
|
RuleFor(x => x.ReleaseYear)
|
||||||
|
.InclusiveBetween(AlbumReleaseYear.Earliest, AlbumReleaseYear.Latest)
|
||||||
|
.When(x => x.ReleaseYear.HasValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Musify.Api.DataTransferObjects.Albums;
|
||||||
|
|
||||||
|
namespace Musify.Api.Validators.Albums;
|
||||||
|
|
||||||
|
public sealed class UpdateAlbumRequestValidator : AbstractValidator<UpdateAlbumRequest>
|
||||||
|
{
|
||||||
|
public UpdateAlbumRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.NewTitle)
|
||||||
|
.NotEmpty()
|
||||||
|
.MaximumLength(200);
|
||||||
|
|
||||||
|
RuleFor(x => x.NewDescription)
|
||||||
|
.MaximumLength(256);
|
||||||
|
|
||||||
|
RuleFor(x => x.NewReleaseYear)
|
||||||
|
.InclusiveBetween(AlbumReleaseYear.Earliest, AlbumReleaseYear.Latest)
|
||||||
|
.When(x => x.NewReleaseYear.HasValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user