using Mediator; using Microsoft.AspNetCore.Mvc; using Musify.Api.Authentication; using Musify.Api.DataTransferObjects.Tracks; using Musify.Api.Extensions; using Musify.Api.Filters; using Musify.Api.Models; using Musify.Application.Coauthors; using Musify.Application.Contracts; using Musify.Application.Pictures; using Musify.Application.Shared; using Musify.Application.Tracks; using Musify.Application.Tracks.Responses; using Musify.Domain.ValueObjects; namespace Musify.Api.Endpoints { public static class TrackEndpoints { public static IEndpointRouteBuilder MapTrackEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/tracks") .WithTags("Tracks"); group.MapGet("/", GetTracks) .AddEndpointFilter>() .WithName("GetTracks") .WithSummary("Get paginated tracks, optionally filtered by name and genre") .Produces>() .ProducesValidationProblem(); group.MapGet("/{id:guid}", GetTrackById) .WithName("GetTrackById") .WithSummary("Get a track by id") .Produces() .Produces(StatusCodes.Status404NotFound); group.MapGet("/{id:guid}/cover", GetTrackCover) .WithName("GetTrackCover") .WithSummary("Get a track cover image") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapGet("/{id:guid}/stream", GetTrackStream) .WithName("GetTrackStream") .WithSummary("Get a streaming manifest URL and ticket for a track. Works anonymously when the playback configuration allows it") .Produces() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound); group.MapPut("/listens/{listenId:guid}/progress", RecordListeningProgress) .AddEndpointFilter>() .WithName("RecordListeningProgress") .WithSummary("Report the total seconds actually played for a listen started by the stream endpoint") .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound); group.MapGet("/users/{userId:long}", GetTracksByUserId) .AddEndpointFilter>() .WithName("GetTracksByUserId") .WithSummary("Get paginated tracks for a user, optionally filtered by name, audio processing status and published state. Each track lists the albums it belongs to. Only the owner sees unpublished tracks") .Produces>(); group.MapGet("/users/{userId:long}/status-counts", GetTrackStatusCounts) .WithName("GetTrackStatusCounts") .WithSummary("Get how many of your tracks are in each audio processing status") .RequireAuthorization() .Produces() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden); group.MapPost("/", CreateTrack) .WithName("CreateTrack") .WithSummary("Create a new track, unpublished, from upload intents. The picture is optional") .AddEndpointFilter>() .RequireAuthorization() .Produces(StatusCodes.Status201Created) .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status409Conflict); group.MapDelete("/{id:guid}", DeleteTrack) .WithName("DeleteTrack") .WithSummary("Delete a track") .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status409Conflict); group.MapDelete("/", DeleteTracks) .WithName("DeleteTracks") .WithSummary("Delete several of your tracks at once. Tracks that are missing or still processing are returned as failed") .AddEndpointFilter>() .RequireAuthorization() .Produces() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden); group.MapPut("/{id:guid}/published", SetTrackPublished) .WithName("SetTrackPublished") .WithSummary("Publish or unpublish one of your tracks. Only tracks with completed audio processing can be published") .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); group.MapPut("/published", SetTracksPublished) .WithName("SetTracksPublished") .WithSummary("Publish or unpublish several of your tracks at once. Missing tracks and tracks that cannot be published yet are returned as skipped") .AddEndpointFilter>() .RequireAuthorization() .Produces() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden); group.MapPost("/{id:guid}/coauthors", InviteCoauthors) .WithName("InviteCoauthors") .WithSummary("Invite users to coauthor one of your tracks. Returns every invitation of the track with its status. A rejected invitation is reset to pending") .AddEndpointFilter>() .RequireAuthorization() .Produces>() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status409Conflict); group.MapDelete("/{id:guid}/coauthors/{userId:long}", RemoveCoauthor) .WithName("RemoveCoauthor") .WithSummary("Remove a coauthor or pending invitation from a track. Allowed to the track owner and to the coauthor themself") .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound); group.MapPost("/upload-urls", RequestTrackUploadUrls) .WithName("RequestTrackUploadUrls") .WithSummary("Request pre-signed URLs to upload the audio of one or many tracks and an optional picture shared by all of them") .AddEndpointFilter>() .RequireAuthorization() .Produces() .ProducesValidationProblem() .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status409Conflict); return app; } private static async Task GetTracks( IMediator mediator, string? name, GenreParameter? genre, [AsParameters] PageQuery page, CancellationToken cancellationToken) { var result = await mediator.Send(new GetTracksQuery(name, page.PageNumber, page.PageSize, genre?.Value), cancellationToken); return Results.Ok(result); } private static async Task GetTrackById( IMediator mediator, CurrentUser currentUser, Guid id, CancellationToken cancellationToken) { var result = await mediator.Send(new GetTrackByIdQuery(id, currentUser.Id), cancellationToken); return result.ToHttpResult(); } private static Task GetTrackCover( IMediator mediator, IStorageService storageService, HttpResponse response, Guid id, CancellationToken cancellationToken, string size = "medium") { return CoverEndpoint.Stream(mediator, storageService, response, CoverOwner.Track, id, size, cancellationToken); } private static async Task GetTrackStream( IMediator mediator, CurrentUser currentUser, Guid id, CancellationToken cancellationToken) { var result = await mediator.Send(new StartListeningCommand(id, currentUser.Id), cancellationToken); return result.ToHttpResult(); } private static async Task RecordListeningProgress( IMediator mediator, CurrentUser currentUser, Guid listenId, RecordListeningProgressRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new RecordListeningProgressCommand(currentUser.RequiredId, listenId, request.PlayedSeconds), cancellationToken); return result.ToHttpResult(); } private static async Task GetTracksByUserId( IMediator mediator, CurrentUser currentUser, long userId, string? name, [AsParameters] PageQuery page, CancellationToken cancellationToken, ProcessingStatus[]? status = null, bool? published = null, bool? includeCoauthored = null) { var result = await mediator.Send( new GetTracksByUserIdQuery(userId, name, page.PageNumber, page.PageSize, status, currentUser.Id, published, includeCoauthored), cancellationToken); return result.ToHttpResult(); } private static async Task InviteCoauthors( IMediator mediator, CurrentUser currentUser, Guid id, InviteCoauthorsRequest request, CancellationToken cancellationToken) { var result = await mediator.Send(new InviteCoauthorsCommand(currentUser.RequiredId, id, request.UserIds), cancellationToken); return result.ToHttpResult(); } private static async Task RemoveCoauthor( IMediator mediator, CurrentUser currentUser, Guid id, long userId, CancellationToken cancellationToken) { var result = await mediator.Send(new RemoveCoauthorCommand(currentUser.RequiredId, id, userId), cancellationToken); return result.ToHttpResult(); } private static async Task SetTrackPublished( IMediator mediator, CurrentUser currentUser, Guid id, SetTrackPublishedRequest request, CancellationToken cancellationToken) { var result = await mediator.Send(new SetTrackPublishedCommand(currentUser.RequiredId, id, request.IsPublished), cancellationToken); return result.ToHttpResult(); } private static async Task SetTracksPublished( IMediator mediator, CurrentUser currentUser, SetTracksPublishedRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new SetTracksPublishedCommand(currentUser.RequiredId, request.TrackIds, request.IsPublished), cancellationToken); return result.ToHttpResult(); } private static async Task GetTrackStatusCounts( IMediator mediator, CurrentUser currentUser, long userId, CancellationToken cancellationToken) { if (currentUser.RequiredId != userId) return Results.Problem("You can only read the status counts of your own tracks.", statusCode: StatusCodes.Status403Forbidden); var result = await mediator.Send(new GetTrackStatusCountsQuery(userId), cancellationToken); return Results.Ok(result); } private static async Task DeleteTracks( IMediator mediator, CurrentUser currentUser, [FromBody] DeleteTracksRequest request, CancellationToken cancellationToken) { var result = await mediator.Send(new DeleteTracksCommand(currentUser.RequiredId, request.TrackIds), cancellationToken); return result.ToHttpResult(); } private static async Task CreateTrack( IMediator mediator, CurrentUser currentUser, CreateTrackRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId, request.Tags, request.IsExplicit), cancellationToken); return result.ToCreatedResult(track => $"/tracks/{track.Id}"); } private static async Task DeleteTrack( IMediator mediator, CurrentUser currentUser, Guid id, CancellationToken cancellationToken) { var result = await mediator.Send(new DeleteTrackCommand(currentUser.RequiredId, id), cancellationToken); return result.ToHttpResult(); } private static async Task RequestTrackUploadUrls( IMediator mediator, CurrentUser currentUser, RequestTrackUploadUrlsRequest request, CancellationToken cancellationToken) { var result = await mediator.Send( new RequestTrackUploadUrlsCommand( currentUser.RequiredId, request.PictureFileType, request.PictureContentType, request.AudioFileType, request.AudioContentType, request.ExpectedPictureSizeBytes, request.ExpectedAudioSizeBytes, request.Audios?.Select(audio => new AudioUploadSpec(audio.FileType, audio.ContentType, audio.ExpectedSizeBytes)).ToList()), cancellationToken); return result.ToHttpResult(); } } }