Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
@@ -13,263 +13,264 @@ using Musify.Application.Pictures.Responses;
using Musify.Application.Shared;
using Musify.Application.Tracks.Responses;
namespace Musify.Api.Endpoints;
public static class AlbumEndpoints
namespace Musify.Api.Endpoints
{
private const int MaxRecentAlbumsLimit = 50;
public static IEndpointRouteBuilder MapAlbumEndpoints(this IEndpointRouteBuilder app)
public static class AlbumEndpoints
{
var group = app.MapGroup("/albums")
.WithTags("Albums");
private const int MaxRecentAlbumsLimit = 50;
group.MapGet("/", GetAlbums)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbums")
.WithSummary("Get paginated albums, optionally filtered by title")
.Produces<PaginatedResponse<AlbumApplicationResponse>>();
group.MapGet("/{id:guid}", GetAlbumById)
.WithName("GetAlbumById")
.WithSummary("Get an album by id")
.Produces<AlbumApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId:long}", GetAlbumsByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbumsByUserId")
.WithSummary("Get paginated albums for a user")
.Produces<PaginatedResponse<AlbumApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/recent", GetRecentlyListenedAlbums)
.WithName("GetRecentlyListenedAlbums")
.WithSummary("Get the albums the current user listened to most recently")
.RequireAuthorization()
.Produces<IReadOnlyList<AlbumApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/{id:guid}/tracks", GetAlbumTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbumTracks")
.WithSummary("Get the tracks of an album ordered by track number")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.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<ValidationFilter<PictureUploadRequest>>()
.RequireAuthorization()
.Produces<PictureUploadResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.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("/{id:guid}", UpdateAlbum)
.WithName("UpdateAlbum")
.WithSummary("Update an album")
.AddEndpointFilter<ValidationFilter<UpdateAlbumRequest>>()
.RequireAuthorization()
.Produces<AlbumApplicationResponse>()
.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<ValidationFilter<AddTracksRequest>>()
.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<IResult> 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<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,
[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<IResult> GetRecentlyListenedAlbums(
IMediator mediator,
CurrentUser currentUser,
CancellationToken cancellationToken,
int limit = 12)
{
if (limit is < 1 or > MaxRecentAlbumsLimit)
public static IEndpointRouteBuilder MapAlbumEndpoints(this IEndpointRouteBuilder app)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
[nameof(limit)] = [$"'{nameof(limit)}' must be between 1 and {MaxRecentAlbumsLimit}."]
});
var group = app.MapGroup("/albums")
.WithTags("Albums");
group.MapGet("/", GetAlbums)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbums")
.WithSummary("Get paginated albums, optionally filtered by title")
.Produces<PaginatedResponse<AlbumApplicationResponse>>();
group.MapGet("/{id:guid}", GetAlbumById)
.WithName("GetAlbumById")
.WithSummary("Get an album by id")
.Produces<AlbumApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId:long}", GetAlbumsByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbumsByUserId")
.WithSummary("Get paginated albums for a user")
.Produces<PaginatedResponse<AlbumApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/recent", GetRecentlyListenedAlbums)
.WithName("GetRecentlyListenedAlbums")
.WithSummary("Get the albums the current user listened to most recently")
.RequireAuthorization()
.Produces<IReadOnlyList<AlbumApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/{id:guid}/tracks", GetAlbumTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbumTracks")
.WithSummary("Get the tracks of an album ordered by track number")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.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<ValidationFilter<PictureUploadRequest>>()
.RequireAuthorization()
.Produces<PictureUploadResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.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("/{id:guid}", UpdateAlbum)
.WithName("UpdateAlbum")
.WithSummary("Update an album")
.AddEndpointFilter<ValidationFilter<UpdateAlbumRequest>>()
.RequireAuthorization()
.Produces<AlbumApplicationResponse>()
.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<ValidationFilter<AddTracksRequest>>()
.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;
}
var result = await mediator.Send(new GetRecentlyListenedAlbumsQuery(currentUser.RequiredId, limit), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> 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<IResult> 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<IResult> GetAlbumById(
IMediator mediator,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetAlbumByIdQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static Task<IResult> 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<IResult> 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<IResult> 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);
private static async Task<IResult> GetRecentlyListenedAlbums(
IMediator mediator,
CurrentUser currentUser,
CancellationToken cancellationToken,
int limit = 12)
{
if (limit is < 1 or > MaxRecentAlbumsLimit)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
[nameof(limit)] = [$"'{nameof(limit)}' must be between 1 and {MaxRecentAlbumsLimit}."]
});
}
return result.ToHttpResult();
}
var result = await mediator.Send(new GetRecentlyListenedAlbumsQuery(currentUser.RequiredId, limit), cancellationToken);
return Results.Ok(result);
}
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, request.PictureIntentId),
cancellationToken);
private static async Task<IResult> 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();
}
return result.ToCreatedResult(album => $"/albums/{album.Id}");
}
private static Task<IResult> 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<IResult> 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, request.PictureIntentId),
cancellationToken);
private static async Task<IResult> 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();
}
return result.ToHttpResult();
}
private static async Task<IResult> 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<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, request.PictureIntentId),
cancellationToken);
private static async Task<IResult> 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.ToCreatedResult(album => $"/albums/{album.Id}");
}
return result.ToHttpResult();
}
private static async Task<IResult> 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, request.PictureIntentId),
cancellationToken);
private static async Task<IResult> 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();
}
return result.ToHttpResult();
private static async Task<IResult> 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<IResult> 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<IResult> 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();
}
}
}
@@ -2,28 +2,29 @@ using Mediator;
using Musify.Application.Configuration;
using Musify.Application.Configuration.Responses;
namespace Musify.Api.Endpoints;
public static class ConfigEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapConfigEndpoints(this IEndpointRouteBuilder app)
public static class ConfigEndpoints
{
var group = app.MapGroup("/config")
.WithTags("Config");
public static IEndpointRouteBuilder MapConfigEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/config")
.WithTags("Config");
group.MapGet("/playback", GetPlaybackPublicConfig)
.WithName("GetPlaybackPublicConfig")
.WithSummary("Get the public playback configuration (whether anonymous listening is allowed). Works anonymously")
.Produces<PlaybackPublicConfigResponse>();
group.MapGet("/playback", GetPlaybackPublicConfig)
.WithName("GetPlaybackPublicConfig")
.WithSummary("Get the public playback configuration (whether anonymous listening is allowed). Works anonymously")
.Produces<PlaybackPublicConfigResponse>();
return app;
}
return app;
}
private static async Task<IResult> GetPlaybackPublicConfig(
IMediator mediator,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlaybackPublicConfigQuery(), cancellationToken);
return Results.Ok(result);
private static async Task<IResult> GetPlaybackPublicConfig(
IMediator mediator,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlaybackPublicConfigQuery(), cancellationToken);
return Results.Ok(result);
}
}
}
@@ -3,29 +3,30 @@ using Musify.Application.Configuration;
using Musify.Application.Contracts;
using Musify.Application.Pictures;
namespace Musify.Api.Endpoints;
internal static class CoverEndpoint
namespace Musify.Api.Endpoints
{
public static async Task<IResult> Stream(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
CoverOwner owner,
Guid id,
string size,
CancellationToken cancellationToken)
internal static class CoverEndpoint
{
var result = await mediator.Send(new GetCoverQuery(owner, id, PictureSizeParser.Parse(size)), cancellationToken);
if (result.IsError)
return Results.NotFound();
public static async Task<IResult> Stream(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
CoverOwner owner,
Guid id,
string size,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetCoverQuery(owner, id, PictureSizeParser.Parse(size)), cancellationToken);
if (result.IsError)
return Results.NotFound();
var location = result.Value;
var stream = await storageService.GetFileAsync(location.Bucket, location.Key, cancellationToken);
if (stream == null)
return Results.NotFound();
var location = result.Value;
var stream = await storageService.GetFileAsync(location.Bucket, location.Key, cancellationToken);
if (stream == null)
return Results.NotFound();
response.Headers.CacheControl = "public, max-age=31536000, immutable";
return Results.Stream(stream, location.ContentType);
response.Headers.CacheControl = "public, max-age=31536000, immutable";
return Results.Stream(stream, location.ContentType);
}
}
}
@@ -2,41 +2,42 @@ using Mediator;
using Musify.Application.Genres;
using Musify.Application.Genres.Responses;
namespace Musify.Api.Endpoints;
public static class GenreEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapGenreEndpoints(this IEndpointRouteBuilder app)
public static class GenreEndpoints
{
var group = app.MapGroup("/genres")
.WithTags("Genres");
public static IEndpointRouteBuilder MapGenreEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/genres")
.WithTags("Genres");
group.MapGet("/", GetGenres)
.WithName("GetGenres")
.WithSummary("Get the genres that have at least one track, most populated first. Works anonymously")
.Produces<IReadOnlyList<GenreResponse>>();
group.MapGet("/", GetGenres)
.WithName("GetGenres")
.WithSummary("Get the genres that have at least one track, most populated first. Works anonymously")
.Produces<IReadOnlyList<GenreResponse>>();
group.MapGet("/available", GetAvailableGenres)
.WithName("GetAvailableGenres")
.WithSummary("Get every genre a track can be tagged with, including the genres it cannot be combined with. Works anonymously")
.Produces<IReadOnlyList<AvailableGenreResponse>>();
group.MapGet("/available", GetAvailableGenres)
.WithName("GetAvailableGenres")
.WithSummary("Get every genre a track can be tagged with, including the genres it cannot be combined with. Works anonymously")
.Produces<IReadOnlyList<AvailableGenreResponse>>();
return app;
}
return app;
}
private static async Task<IResult> GetAvailableGenres(
IMediator mediator,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetAvailableGenresQuery(), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetAvailableGenres(
IMediator mediator,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetAvailableGenresQuery(), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetGenres(
IMediator mediator,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetGenresQuery(), cancellationToken);
return Results.Ok(result);
private static async Task<IResult> GetGenres(
IMediator mediator,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetGenresQuery(), cancellationToken);
return Results.Ok(result);
}
}
}
@@ -8,53 +8,54 @@ using Musify.Application.Likes;
using Musify.Application.Shared;
using Musify.Application.Tracks.Responses;
namespace Musify.Api.Endpoints;
public static class LikeEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapLikeEndpoints(this IEndpointRouteBuilder app)
public static class LikeEndpoints
{
var group = app.MapGroup("/likes")
.WithTags("Likes")
.RequireAuthorization();
public static IEndpointRouteBuilder MapLikeEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/likes")
.WithTags("Likes")
.RequireAuthorization();
group.MapGet("/", GetLikedTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetLikedTracks")
.WithSummary("Get the current user's liked tracks")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/", GetLikedTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetLikedTracks")
.WithSummary("Get the current user's liked tracks")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapPost("/toggle", ToggleTrackLike)
.WithName("ToggleTrackLike")
.WithSummary("Like or unlike a track by id")
.Produces<bool>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/toggle", ToggleTrackLike)
.WithName("ToggleTrackLike")
.WithSummary("Like or unlike a track by id")
.Produces<bool>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
return app;
}
return app;
}
private static async Task<IResult> GetLikedTracks(
IMediator mediator,
CurrentUser currentUser,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetLikedTracksQuery(currentUser.RequiredId, page.PageNumber, page.PageSize), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetLikedTracks(
IMediator mediator,
CurrentUser currentUser,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetLikedTracksQuery(currentUser.RequiredId, page.PageNumber, page.PageSize), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> ToggleTrackLike(
IMediator mediator,
CurrentUser currentUser,
ToggleLikeRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new ToggleTrackLikeCommand(currentUser.RequiredId, request.TrackId),
cancellationToken);
private static async Task<IResult> ToggleTrackLike(
IMediator mediator,
CurrentUser currentUser,
ToggleLikeRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new ToggleTrackLikeCommand(currentUser.RequiredId, request.TrackId),
cancellationToken);
return result.ToHttpResult();
return result.ToHttpResult();
}
}
}
@@ -4,48 +4,49 @@ using Musify.Api.Extensions;
using Musify.Application.Mixes;
using Musify.Application.Mixes.Responses;
namespace Musify.Api.Endpoints;
public static class MixEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapMixEndpoints(this IEndpointRouteBuilder app)
public static class MixEndpoints
{
var group = app.MapGroup("/mixes")
.WithTags("Mixes")
.RequireAuthorization();
public static IEndpointRouteBuilder MapMixEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/mixes")
.WithTags("Mixes")
.RequireAuthorization();
group.MapGet("/", GetMixes)
.WithName("GetMixes")
.WithSummary("Get the server-generated mixes of the current user")
.Produces<IReadOnlyList<MixApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/", GetMixes)
.WithName("GetMixes")
.WithSummary("Get the server-generated mixes of the current user")
.Produces<IReadOnlyList<MixApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/{id:guid}", GetMixById)
.WithName("GetMixById")
.WithSummary("Get a mix with all of its songs")
.Produces<MixApplicationResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:guid}", GetMixById)
.WithName("GetMixById")
.WithSummary("Get a mix with all of its songs")
.Produces<MixApplicationResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
return app;
}
return app;
}
private static async Task<IResult> GetMixes(
IMediator mediator,
CurrentUser currentUser,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMixesByUserIdQuery(currentUser.RequiredId), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetMixes(
IMediator mediator,
CurrentUser currentUser,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMixesByUserIdQuery(currentUser.RequiredId), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetMixById(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMixByIdQuery(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
private static async Task<IResult> GetMixById(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMixByIdQuery(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
}
}
}
@@ -13,223 +13,224 @@ using Musify.Application.PlayLists.Responses;
using Musify.Application.Shared;
using Musify.Application.Tracks.Responses;
namespace Musify.Api.Endpoints;
public static class PlayListEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapPlayListEndpoints(this IEndpointRouteBuilder app)
public static class PlayListEndpoints
{
var group = app.MapGroup("/playlists")
.WithTags("PlayLists");
public static IEndpointRouteBuilder MapPlayListEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/playlists")
.WithTags("PlayLists");
group.MapGet("/{id:guid}", GetPlayListById)
.WithName("GetPlayListById")
.WithSummary("Get a playlist by id")
.Produces<PlayListApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:guid}", GetPlayListById)
.WithName("GetPlayListById")
.WithSummary("Get a playlist by id")
.Produces<PlayListApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId:long}", GetPlayListsByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetPlayListsByUserId")
.WithSummary("Get paginated playlists for a user. Private playlists are only returned to their owner")
.Produces<PaginatedResponse<PlayListApplicationResponse>>();
group.MapGet("/users/{userId:long}", GetPlayListsByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetPlayListsByUserId")
.WithSummary("Get paginated playlists for a user. Private playlists are only returned to their owner")
.Produces<PaginatedResponse<PlayListApplicationResponse>>();
group.MapGet("/{id:guid}/tracks", GetPlayListTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetPlayListTracks")
.WithSummary("Get paginated tracks of a playlist")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:guid}/tracks", GetPlayListTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetPlayListTracks")
.WithSummary("Get paginated tracks of a playlist")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:guid}/cover", GetPlayListCover)
.WithName("GetPlayListCover")
.WithSummary("Get a playlist cover image")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:guid}/cover", GetPlayListCover)
.WithName("GetPlayListCover")
.WithSummary("Get a playlist cover image")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/", CreatePlayList)
.WithName("CreatePlayList")
.WithSummary("Create a new playlist")
.AddEndpointFilter<ValidationFilter<CreatePlayListRequest>>()
.RequireAuthorization()
.Produces<PlayListApplicationResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/", CreatePlayList)
.WithName("CreatePlayList")
.WithSummary("Create a new playlist")
.AddEndpointFilter<ValidationFilter<CreatePlayListRequest>>()
.RequireAuthorization()
.Produces<PlayListApplicationResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPut("/{id:guid}", UpdatePlayList)
.WithName("UpdatePlayList")
.WithSummary("Update an existing playlist")
.AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>()
.RequireAuthorization()
.Produces<PlayListApplicationResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapPut("/{id:guid}", UpdatePlayList)
.WithName("UpdatePlayList")
.WithSummary("Update an existing playlist")
.AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>()
.RequireAuthorization()
.Produces<PlayListApplicationResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{id:guid}", DeletePlayList)
.WithName("DeletePlayList")
.WithSummary("Delete a playlist")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{id:guid}", DeletePlayList)
.WithName("DeletePlayList")
.WithSummary("Delete a playlist")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/upload-picture", RequestPlayListPictureUpload)
.WithName("RequestPlayListPictureUpload")
.WithSummary("Request a pre-signed URL to upload a playlist picture")
.AddEndpointFilter<ValidationFilter<PictureUploadRequest>>()
.RequireAuthorization()
.Produces<PictureUploadResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/upload-picture", RequestPlayListPictureUpload)
.WithName("RequestPlayListPictureUpload")
.WithSummary("Request a pre-signed URL to upload a playlist picture")
.AddEndpointFilter<ValidationFilter<PictureUploadRequest>>()
.RequireAuthorization()
.Produces<PictureUploadResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{id:guid}/tracks", AddPlayListTracks)
.WithName("AddPlayListTracks")
.WithSummary("Add tracks to a playlist, skipping the ones it already has")
.AddEndpointFilter<ValidationFilter<AddTracksRequest>>()
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapPost("/{id:guid}/tracks", AddPlayListTracks)
.WithName("AddPlayListTracks")
.WithSummary("Add tracks to a playlist, skipping the ones it already has")
.AddEndpointFilter<ValidationFilter<AddTracksRequest>>()
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapDelete("/{id:guid}/tracks/{trackId:guid}", RemoveTrackFromPlayList)
.WithName("RemoveTrackFromPlayList")
.WithSummary("Remove a track from a playlist")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{id:guid}/tracks/{trackId:guid}", RemoveTrackFromPlayList)
.WithName("RemoveTrackFromPlayList")
.WithSummary("Remove a track from a playlist")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
return app;
}
return app;
}
private static async Task<IResult> GetPlayListById(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListByIdQuery(id, currentUser.Id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListById(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListByIdQuery(id, currentUser.Id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListsByUserId(
IMediator mediator,
CurrentUser currentUser,
long userId,
string? name,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListsByUserIdQuery(userId, name, page.PageNumber, page.PageSize, currentUser.Id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetPlayListsByUserId(
IMediator mediator,
CurrentUser currentUser,
long userId,
string? name,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListsByUserIdQuery(userId, name, page.PageNumber, page.PageSize, currentUser.Id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetPlayListTracks(
IMediator mediator,
CurrentUser currentUser,
Guid id,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListTracksQuery(id, page.PageNumber, page.PageSize, currentUser.Id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListTracks(
IMediator mediator,
CurrentUser currentUser,
Guid id,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListTracksQuery(id, page.PageNumber, page.PageSize, currentUser.Id), cancellationToken);
return result.ToHttpResult();
}
private static Task<IResult> GetPlayListCover(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
Guid id,
CancellationToken cancellationToken,
string size = "medium")
{
return CoverEndpoint.Stream(mediator, storageService, response, CoverOwner.PlayList, id, size, cancellationToken);
}
private static Task<IResult> GetPlayListCover(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
Guid id,
CancellationToken cancellationToken,
string size = "medium")
{
return CoverEndpoint.Stream(mediator, storageService, response, CoverOwner.PlayList, id, size, cancellationToken);
}
private static async Task<IResult> CreatePlayList(
IMediator mediator,
CurrentUser currentUser,
CreatePlayListRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId, request.Visibility),
cancellationToken);
private static async Task<IResult> CreatePlayList(
IMediator mediator,
CurrentUser currentUser,
CreatePlayListRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId, request.Visibility),
cancellationToken);
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
}
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
}
private static async Task<IResult> UpdatePlayList(
IMediator mediator,
CurrentUser currentUser,
Guid id,
UpdatePlayListRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new UpdatePlayListCommand(currentUser.RequiredId, id, request.Name, request.Description, request.PictureIntentId, request.Visibility),
cancellationToken);
private static async Task<IResult> UpdatePlayList(
IMediator mediator,
CurrentUser currentUser,
Guid id,
UpdatePlayListRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new UpdatePlayListCommand(currentUser.RequiredId, id, request.Name, request.Description, request.PictureIntentId, request.Visibility),
cancellationToken);
return result.ToHttpResult();
}
return result.ToHttpResult();
}
private static async Task<IResult> DeletePlayList(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new DeletePlayListCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> DeletePlayList(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new DeletePlayListCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> RequestPlayListPictureUpload(
IMediator mediator,
CurrentUser currentUser,
PictureUploadRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RequestPlayListPictureUploadCommand(currentUser.RequiredId, request.FileType, request.ContentType, request.ExpectedSizeBytes),
cancellationToken);
private static async Task<IResult> RequestPlayListPictureUpload(
IMediator mediator,
CurrentUser currentUser,
PictureUploadRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RequestPlayListPictureUploadCommand(currentUser.RequiredId, request.FileType, request.ContentType, request.ExpectedSizeBytes),
cancellationToken);
return result.ToHttpResult();
}
return result.ToHttpResult();
}
private static async Task<IResult> AddPlayListTracks(
IMediator mediator,
CurrentUser currentUser,
Guid id,
AddTracksRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new AddTracksToPlayListCommand(currentUser.RequiredId, id, request.TrackIds),
cancellationToken);
private static async Task<IResult> AddPlayListTracks(
IMediator mediator,
CurrentUser currentUser,
Guid id,
AddTracksRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new AddTracksToPlayListCommand(currentUser.RequiredId, id, request.TrackIds),
cancellationToken);
return result.ToHttpResult();
}
return result.ToHttpResult();
}
private static async Task<IResult> RemoveTrackFromPlayList(
IMediator mediator,
CurrentUser currentUser,
Guid id,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RemoveTrackFromPlayListCommand(currentUser.RequiredId, id, trackId),
cancellationToken);
private static async Task<IResult> RemoveTrackFromPlayList(
IMediator mediator,
CurrentUser currentUser,
Guid id,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RemoveTrackFromPlayListCommand(currentUser.RequiredId, id, trackId),
cancellationToken);
return result.ToHttpResult();
return result.ToHttpResult();
}
}
}
@@ -10,196 +10,197 @@ using Musify.Application.Shared;
using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses;
namespace Musify.Api.Endpoints;
public static class TrackEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapTrackEndpoints(this IEndpointRouteBuilder app)
public static class TrackEndpoints
{
var group = app.MapGroup("/tracks")
.WithTags("Tracks");
public static IEndpointRouteBuilder MapTrackEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/tracks")
.WithTags("Tracks");
group.MapGet("/", GetTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetTracks")
.WithSummary("Get paginated tracks, optionally filtered by name and genre")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.ProducesValidationProblem();
group.MapGet("/", GetTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetTracks")
.WithSummary("Get paginated tracks, optionally filtered by name and genre")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.ProducesValidationProblem();
group.MapGet("/{id:guid}", GetTrackById)
.WithName("GetTrackById")
.WithSummary("Get a track by id")
.Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:guid}", GetTrackById)
.WithName("GetTrackById")
.WithSummary("Get a track by id")
.Produces<TrackApplicationResponse>()
.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}/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<TrackStreamResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.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<TrackStreamResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPut("/listens/{listenId:guid}/progress", RecordListeningProgress)
.AddEndpointFilter<ValidationFilter<RecordListeningProgressRequest>>()
.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.MapPut("/listens/{listenId:guid}/progress", RecordListeningProgress)
.AddEndpointFilter<ValidationFilter<RecordListeningProgressRequest>>()
.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<ValidationFilter<PageQuery>>()
.WithName("GetTracksByUserId")
.WithSummary("Get paginated tracks for a user")
.Produces<PaginatedResponse<TrackApplicationResponse>>();
group.MapGet("/users/{userId:long}", GetTracksByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetTracksByUserId")
.WithSummary("Get paginated tracks for a user")
.Produces<PaginatedResponse<TrackApplicationResponse>>();
group.MapPost("/", CreateTrack)
.WithName("CreateTrack")
.WithSummary("Create a new track")
.AddEndpointFilter<ValidationFilter<CreateTrackRequest>>()
.RequireAuthorization()
.Produces<TrackApplicationResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapPost("/", CreateTrack)
.WithName("CreateTrack")
.WithSummary("Create a new track")
.AddEndpointFilter<ValidationFilter<CreateTrackRequest>>()
.RequireAuthorization()
.Produces<TrackApplicationResponse>(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("/{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.MapPost("/upload-urls", RequestTrackUploadUrls)
.WithName("RequestTrackUploadUrls")
.WithSummary("Request pre-signed URLs to upload track picture and audio")
.AddEndpointFilter<ValidationFilter<RequestTrackUploadUrlsRequest>>()
.RequireAuthorization()
.Produces<TrackUploadUrlsResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapPost("/upload-urls", RequestTrackUploadUrls)
.WithName("RequestTrackUploadUrls")
.WithSummary("Request pre-signed URLs to upload track picture and audio")
.AddEndpointFilter<ValidationFilter<RequestTrackUploadUrlsRequest>>()
.RequireAuthorization()
.Produces<TrackUploadUrlsResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
return app;
}
return app;
}
private static async Task<IResult> 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<IResult> 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<IResult> GetTrackById(
IMediator mediator,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetTrackByIdQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetTrackById(
IMediator mediator,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetTrackByIdQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static Task<IResult> 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 Task<IResult> 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<IResult> 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<IResult> 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<IResult> 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<IResult> 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<IResult> GetTracksByUserId(
IMediator mediator,
long userId,
string? name,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetTracksByUserIdQuery(userId, name, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetTracksByUserId(
IMediator mediator,
long userId,
string? name,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetTracksByUserIdQuery(userId, name, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> 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);
private static async Task<IResult> 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}");
}
return result.ToCreatedResult(track => $"/tracks/{track.Id}");
}
private static async Task<IResult> 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<IResult> 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<IResult> 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),
cancellationToken);
private static async Task<IResult> 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),
cancellationToken);
return result.ToHttpResult();
return result.ToHttpResult();
}
}
}
@@ -9,204 +9,205 @@ using Musify.Application.Tracks.Responses;
using Musify.Application.Users;
using Musify.Application.Users.Responses;
namespace Musify.Api.Endpoints;
public static class UserEndpoints
namespace Musify.Api.Endpoints
{
public static IEndpointRouteBuilder MapUserEndpoints(this IEndpointRouteBuilder app)
public static class UserEndpoints
{
var group = app.MapGroup("/users")
.WithTags("Users");
public static IEndpointRouteBuilder MapUserEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/users")
.WithTags("Users");
group.MapGet("/", GetUsers)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUsers")
.WithSummary("Get paginated users")
.Produces<PaginatedResponse<UserSummaryResponse>>();
group.MapGet("/", GetUsers)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUsers")
.WithSummary("Get paginated users")
.Produces<PaginatedResponse<UserSummaryResponse>>();
group.MapGet("/{id:long}", GetUserById)
.WithName("GetUserById")
.WithSummary("Get a user by id")
.Produces<UserApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}", GetUserById)
.WithName("GetUserById")
.WithSummary("Get a user by id")
.Produces<UserApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/listening-history", GetListeningHistory)
.WithName("GetListeningHistory")
.WithSummary("Get a user's listening history")
.Produces<IEnumerable<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/listening-history", GetListeningHistory)
.WithName("GetListeningHistory")
.WithSummary("Get a user's listening history")
.Produces<IEnumerable<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/listening-stats", GetListeningStats)
.WithName("GetListeningStats")
.WithSummary("Get a user's listening stats for the current week and streak")
.RequireAuthorization()
.Produces<ListeningStatsResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden);
group.MapGet("/{id:long}/listening-stats", GetListeningStats)
.WithName("GetListeningStats")
.WithSummary("Get a user's listening stats for the current week and streak")
.RequireAuthorization()
.Produces<ListeningStatsResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden);
group.MapGet("/{id:long}/last-listened-track", GetLastTrackListenedByUserId)
.WithName("GetLastTrackListenedByUserId")
.WithSummary("Get the last track a user listened to")
.Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status204NoContent);
group.MapGet("/{id:long}/last-listened-track", GetLastTrackListenedByUserId)
.WithName("GetLastTrackListenedByUserId")
.WithSummary("Get the last track a user listened to")
.Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status204NoContent);
group.MapGet("/{id:long}/profile", GetUserProfile)
.WithName("GetUserProfile")
.WithSummary("Get a user's public profile, including follower counts")
.Produces<UserProfileResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/profile", GetUserProfile)
.WithName("GetUserProfile")
.WithSummary("Get a user's public profile, including follower counts")
.Produces<UserProfileResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/followers", GetFollowers)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUserFollowers")
.WithSummary("Get the users following a user. Visible to the user and to mutual followers")
.Produces<PaginatedResponse<UserSummaryResponse>>()
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/followers", GetFollowers)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUserFollowers")
.WithSummary("Get the users following a user. Visible to the user and to mutual followers")
.Produces<PaginatedResponse<UserSummaryResponse>>()
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/following", GetFollowing)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUserFollowing")
.WithSummary("Get the users a user follows")
.Produces<PaginatedResponse<UserSummaryResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/following", GetFollowing)
.AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUserFollowing")
.WithSummary("Get the users a user follows")
.Produces<PaginatedResponse<UserSummaryResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id:long}/is-following", IsFollowing)
.WithName("IsFollowingUser")
.WithSummary("Check whether the current user follows another user")
.RequireAuthorization()
.Produces<bool>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/{id:long}/is-following", IsFollowing)
.WithName("IsFollowingUser")
.WithSummary("Check whether the current user follows another user")
.RequireAuthorization()
.Produces<bool>()
.Produces(StatusCodes.Status401Unauthorized);
group.MapPost("/{id:long}/follow", FollowUser)
.WithName("FollowUser")
.WithSummary("Follow a user")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.ProducesValidationProblem();
group.MapPost("/{id:long}/follow", FollowUser)
.WithName("FollowUser")
.WithSummary("Follow a user")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.ProducesValidationProblem();
group.MapDelete("/{id:long}/follow", UnfollowUser)
.WithName("UnfollowUser")
.WithSummary("Unfollow a user")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized);
group.MapDelete("/{id:long}/follow", UnfollowUser)
.WithName("UnfollowUser")
.WithSummary("Unfollow a user")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized);
return app;
}
return app;
}
private static async Task<IResult> GetUsers(
IMediator mediator,
CurrentUser currentUser,
string? usernameSearch,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUsersQuery(page.PageNumber, page.PageSize, usernameSearch, currentUser.Id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetUsers(
IMediator mediator,
CurrentUser currentUser,
string? usernameSearch,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUsersQuery(page.PageNumber, page.PageSize, usernameSearch, currentUser.Id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetUserById(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserByIdQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetUserById(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserByIdQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetListeningHistory(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetListeningHistoryQuery(id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetListeningHistory(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetListeningHistoryQuery(id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetListeningStats(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
if (currentUser.RequiredId != id)
return Results.Problem(statusCode: StatusCodes.Status403Forbidden);
private static async Task<IResult> GetListeningStats(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
if (currentUser.RequiredId != id)
return Results.Problem(statusCode: StatusCodes.Status403Forbidden);
var result = await mediator.Send(new GetListeningStatsQuery(id), cancellationToken);
return Results.Ok(result);
}
var result = await mediator.Send(new GetListeningStatsQuery(id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetLastTrackListenedByUserId(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
var track = await mediator.Send(new GetLastTrackListenedByUserIdQuery(id), cancellationToken);
return track == null ? Results.NoContent() : Results.Ok(track);
}
private static async Task<IResult> GetLastTrackListenedByUserId(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
var track = await mediator.Send(new GetLastTrackListenedByUserIdQuery(id), cancellationToken);
return track == null ? Results.NoContent() : Results.Ok(track);
}
private static async Task<IResult> GetUserProfile(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserProfileQuery(id, currentUser.Id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetUserProfile(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserProfileQuery(id, currentUser.Id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetFollowers(
IMediator mediator,
CurrentUser currentUser,
long id,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserFollowersQuery(currentUser.Id, id, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetFollowers(
IMediator mediator,
CurrentUser currentUser,
long id,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserFollowersQuery(currentUser.Id, id, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetFollowing(
IMediator mediator,
CurrentUser currentUser,
long id,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserFollowingQuery(currentUser.Id, id, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetFollowing(
IMediator mediator,
CurrentUser currentUser,
long id,
[AsParameters] PageQuery page,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetUserFollowingQuery(currentUser.Id, id, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> IsFollowing(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new IsFollowingUserQuery(currentUser.RequiredId, id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> IsFollowing(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new IsFollowingUserQuery(currentUser.RequiredId, id), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> FollowUser(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new FollowUserCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> FollowUser(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new FollowUserCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> UnfollowUser(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new UnfollowUserCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
private static async Task<IResult> UnfollowUser(
IMediator mediator,
CurrentUser currentUser,
long id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new UnfollowUserCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult();
}
}
}