using Mediator; using Musify.Api.Authentication; using Musify.Api.DataTransferObjects; using Musify.Api.DataTransferObjects.Albums; using Musify.Api.Extensions; using Musify.Api.Filters; using Musify.Api.Models; using Musify.Application.Albums; using Musify.Application.Albums.Responses; using Musify.Application.Contracts; using Musify.Application.Pictures; using Musify.Application.Pictures.Responses; using Musify.Application.Shared; using Musify.Application.Tracks.Responses; namespace Musify.Api.Endpoints { public static class AlbumEndpoints { private const int MaxRecentAlbumsLimit = 50; public static IEndpointRouteBuilder MapAlbumEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/albums") .WithTags("Albums"); group.MapGet("/", GetAlbums) .AddEndpointFilter>() .WithName("GetAlbums") .WithSummary("Get paginated albums, optionally filtered by title") .Produces>(); group.MapGet("/{id:guid}", GetAlbumById) .WithName("GetAlbumById") .WithSummary("Get an album by id") .Produces() .Produces(StatusCodes.Status404NotFound); group.MapGet("/users/{userId:long}", GetAlbumsByUserId) .AddEndpointFilter>() .WithName("GetAlbumsByUserId") .WithSummary("Get paginated albums for a user") .Produces>() .Produces(StatusCodes.Status404NotFound); group.MapGet("/recent", GetRecentlyListenedAlbums) .WithName("GetRecentlyListenedAlbums") .WithSummary("Get the albums the current user listened to most recently") .RequireAuthorization() .Produces>() .Produces(StatusCodes.Status401Unauthorized); group.MapGet("/{id:guid}/tracks", GetAlbumTracks) .AddEndpointFilter>() .WithName("GetAlbumTracks") .WithSummary("Get the tracks of an album ordered by track number") .Produces>() .Produces(StatusCodes.Status404NotFound); group.MapGet("/{id:guid}/tracks/ids", GetAlbumTrackIds) .WithName("GetAlbumTrackIds") .WithSummary("Get the ids of every track in one of your albums, ordered by track number") .RequireAuthorization() .Produces() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); group.MapGet("/{id:guid}/cover", GetAlbumCover) .WithName("GetAlbumCover") .WithSummary("Get an album cover image") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapPost("/upload-picture", RequestAlbumPictureUpload) .WithName("RequestAlbumPictureUpload") .WithSummary("Request a pre-signed URL to upload an album picture") .AddEndpointFilter>() .RequireAuthorization() .Produces() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .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("/{id:guid}", UpdateAlbum) .WithName("UpdateAlbum") .WithSummary("Update an album") .AddEndpointFilter>() .RequireAuthorization() .Produces() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); group.MapPut("/{id:guid}/picture", UpdateAlbumPicture) .WithName("UpdateAlbumPicture") .WithSummary("Replace an album picture with an uploaded image") .AddEndpointFilter>() .RequireAuthorization() .Produces() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); group.MapDelete("/{id:guid}", DeleteAlbum) .WithName("DeleteAlbum") .WithSummary("Delete an album") .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); group.MapPost("/{id:guid}/tracks", AddAlbumTracks) .WithName("AddAlbumTracks") .WithSummary("Add your uploaded tracks to an album, skipping the ones it already has") .AddEndpointFilter>() .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status409Conflict); group.MapDelete("/{id:guid}/tracks/{trackId:guid}", RemoveTrackFromAlbum) .WithName("RemoveTrackFromAlbum") .WithSummary("Remove a track from an album") .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); return app; } private static async Task GetAlbums( IMediator mediator, string? title, [AsParameters] PageQuery page, CancellationToken cancellationToken) { var result = await mediator.Send(new GetAlbumsQuery(title, page.PageNumber, page.PageSize), cancellationToken); return Results.Ok(result); } 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, [AsParameters] PageQuery page, CancellationToken cancellationToken) { var result = await mediator.Send(new GetAlbumsByUserIdQuery(userId, page.PageNumber, page.PageSize), cancellationToken); return result.ToHttpResult(); } private static async Task GetRecentlyListenedAlbums( IMediator mediator, CurrentUser currentUser, CancellationToken cancellationToken, int limit = 12) { if (limit is < 1 or > MaxRecentAlbumsLimit) { return Results.ValidationProblem(new Dictionary { [nameof(limit)] = [$"'{nameof(limit)}' must be between 1 and {MaxRecentAlbumsLimit}."] }); } var result = await mediator.Send(new GetRecentlyListenedAlbumsQuery(currentUser.RequiredId, limit), cancellationToken); return Results.Ok(result); } private static async Task GetAlbumTracks( IMediator mediator, Guid id, [AsParameters] PageQuery page, CancellationToken cancellationToken) { var result = await mediator.Send(new GetAlbumTracksQuery(id, page.PageNumber, page.PageSize), cancellationToken); return result.ToHttpResult(); } private static async Task GetAlbumTrackIds( IMediator mediator, CurrentUser currentUser, Guid id, CancellationToken cancellationToken) { var result = await mediator.Send(new GetAlbumTrackIdsQuery(currentUser.RequiredId, id), cancellationToken); return result.ToHttpResult(); } private static Task GetAlbumCover( IMediator mediator, IStorageService storageService, HttpResponse response, Guid id, CancellationToken cancellationToken, string size = "medium") { return CoverEndpoint.Stream(mediator, storageService, response, CoverOwner.Album, id, size, cancellationToken); } private static async Task RequestAlbumPictureUpload( IMediator mediator, CurrentUser currentUser, PictureUploadRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new RequestAlbumPictureUploadCommand(currentUser.RequiredId, request.FileType, request.ContentType, request.ExpectedSizeBytes), 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, request.PictureIntentId), cancellationToken); return result.ToCreatedResult(album => $"/albums/{album.Id}"); } private static async Task UpdateAlbum( IMediator mediator, CurrentUser currentUser, Guid id, UpdateAlbumRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new UpdateAlbumCommand(currentUser.RequiredId, id, request.Title, request.Description, request.ReleaseYear), cancellationToken); return result.ToHttpResult(); } private static async Task UpdateAlbumPicture( IMediator mediator, CurrentUser currentUser, Guid id, UpdateAlbumPictureRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new UpdateAlbumPictureCommand(currentUser.RequiredId, id, request.PictureIntentId), cancellationToken); return result.ToHttpResult(); } private static async Task DeleteAlbum( IMediator mediator, CurrentUser currentUser, Guid id, CancellationToken cancellationToken) { var result = await mediator.Send(new DeleteAlbumCommand(currentUser.RequiredId, id), cancellationToken); return result.ToHttpResult(); } private static async Task AddAlbumTracks( IMediator mediator, CurrentUser currentUser, Guid id, AddTracksRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new AddTracksToAlbumCommand(currentUser.RequiredId, id, request.TrackIds), cancellationToken); return result.ToHttpResult(); } private static async Task RemoveTrackFromAlbum( IMediator mediator, CurrentUser currentUser, Guid id, Guid trackId, CancellationToken cancellationToken) { var result = await mediator.Send( new RemoveTrackFromAlbumCommand(currentUser.RequiredId, id, trackId), cancellationToken); return result.ToHttpResult(); } } }