diff --git a/backend/Musify.Api/DataTransferObjects/Albums/CreateAlbumRequest.cs b/backend/Musify.Api/DataTransferObjects/Albums/CreateAlbumRequest.cs new file mode 100644 index 0000000..6698c9c --- /dev/null +++ b/backend/Musify.Api/DataTransferObjects/Albums/CreateAlbumRequest.cs @@ -0,0 +1,6 @@ +namespace Musify.Api.DataTransferObjects.Albums; + +public record CreateAlbumRequest( + string Title, + string? Description, + int? ReleaseYear); diff --git a/backend/Musify.Api/DataTransferObjects/Albums/UpdateAlbumRequest.cs b/backend/Musify.Api/DataTransferObjects/Albums/UpdateAlbumRequest.cs new file mode 100644 index 0000000..a27e8e7 --- /dev/null +++ b/backend/Musify.Api/DataTransferObjects/Albums/UpdateAlbumRequest.cs @@ -0,0 +1,6 @@ +namespace Musify.Api.DataTransferObjects.Albums; + +public record UpdateAlbumRequest( + string NewTitle, + string? NewDescription, + int? NewReleaseYear); diff --git a/backend/Musify.Api/Endpoints/AlbumEndpoints.cs b/backend/Musify.Api/Endpoints/AlbumEndpoints.cs new file mode 100644 index 0000000..7de5d9b --- /dev/null +++ b/backend/Musify.Api/Endpoints/AlbumEndpoints.cs @@ -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>(); + + group.MapGet("/{id}", GetAlbumById) + .WithName("GetAlbumById") + .WithSummary("Get An Album By Id.") + .Produces() + .Produces(StatusCodes.Status404NotFound); + + group.MapGet("/users/{userId}", GetAlbumsByUserId) + .WithName("GetAlbumsByUserId") + .WithSummary("Get Paginated Albums For A User.") + .Produces>() + .Produces(StatusCodes.Status404NotFound); + + group.MapGet("/{albumId}/tracks", GetAlbumTracks) + .WithName("GetAlbumTracks") + .WithSummary("Get The Tracks Of An Album Ordered By Track Number.") + .Produces>() + .Produces(StatusCodes.Status404NotFound); + + group.MapPost("/", CreateAlbum) + .WithName("CreateAlbum") + .WithSummary("Create A New Album.") + .AddEndpointFilter>() + .RequireAuthorization() + .Produces(StatusCodes.Status201Created) + .ProducesValidationProblem() + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound); + + group.MapPut("/{albumId}", UpdateAlbum) + .WithName("UpdateAlbum") + .WithSummary("Update An Album.") + .AddEndpointFilter>() + .RequireAuthorization() + .Produces() + .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 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 GetAlbumById( + IMediator mediator, + Guid id, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetAlbumByIdQuery(id), cancellationToken); + return result.ToHttpResult(); + } + + private static async Task 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 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 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 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 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 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 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(); + } +} diff --git a/backend/Musify.Api/Program.cs b/backend/Musify.Api/Program.cs index bc5341d..bb161d2 100644 --- a/backend/Musify.Api/Program.cs +++ b/backend/Musify.Api/Program.cs @@ -38,6 +38,7 @@ app.UseAuthentication(); app.UseAuthorization(); app + .MapAlbumEndpoints() .MapPlayListEndpoints() .MapTrackEndpoints() .MapUserEndpoints() diff --git a/backend/Musify.Api/Validators/Albums/AlbumReleaseYear.cs b/backend/Musify.Api/Validators/Albums/AlbumReleaseYear.cs new file mode 100644 index 0000000..222c8be --- /dev/null +++ b/backend/Musify.Api/Validators/Albums/AlbumReleaseYear.cs @@ -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; +} diff --git a/backend/Musify.Api/Validators/Albums/CreateAlbumRequestValidator.cs b/backend/Musify.Api/Validators/Albums/CreateAlbumRequestValidator.cs new file mode 100644 index 0000000..c2d2cd8 --- /dev/null +++ b/backend/Musify.Api/Validators/Albums/CreateAlbumRequestValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using Musify.Api.DataTransferObjects.Albums; + +namespace Musify.Api.Validators.Albums; + +public sealed class CreateAlbumRequestValidator : AbstractValidator +{ + 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); + } +} diff --git a/backend/Musify.Api/Validators/Albums/UpdateAlbumRequestValidator.cs b/backend/Musify.Api/Validators/Albums/UpdateAlbumRequestValidator.cs new file mode 100644 index 0000000..122a906 --- /dev/null +++ b/backend/Musify.Api/Validators/Albums/UpdateAlbumRequestValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using Musify.Api.DataTransferObjects.Albums; + +namespace Musify.Api.Validators.Albums; + +public sealed class UpdateAlbumRequestValidator : AbstractValidator +{ + 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); + } +}