Unify API route parameters, return IResult everywhere, merge picture upload DTOs and centralize validation limits

Guid and long route constraints, id/trackId parameter names, Update*Request without the New prefix,
sentence-case operation summaries, shared security scheme id and a single Limits class.
This commit is contained in:
2026-09-24 12:36:48 +02:00
parent 405df082cc
commit fcf3093ad8
38 changed files with 740 additions and 829 deletions
@@ -8,27 +8,25 @@ namespace Musify.Api.Authentication;
public sealed class JwtBearerOptionsSetup(IOptions<AuthenticationConfiguration> options) public sealed class JwtBearerOptionsSetup(IOptions<AuthenticationConfiguration> options)
: IConfigureNamedOptions<JwtBearerOptions> : IConfigureNamedOptions<JwtBearerOptions>
{ {
private readonly AuthenticationConfiguration _configuration = options.Value; private readonly AuthenticationConfiguration configuration = options.Value;
public void Configure(string? name, JwtBearerOptions options) public void Configure(string? name, JwtBearerOptions options)
{ {
if (name != JwtBearerDefaults.AuthenticationScheme) if (name != JwtBearerDefaults.AuthenticationScheme)
{
return; return;
}
Configure(options); Configure(options);
} }
public void Configure(JwtBearerOptions options) public void Configure(JwtBearerOptions options)
{ {
options.MetadataAddress = _configuration.MetadataAddress; options.MetadataAddress = configuration.MetadataAddress;
options.RequireHttpsMetadata = _configuration.RequireHttpsMetadata; options.RequireHttpsMetadata = configuration.RequireHttpsMetadata;
options.Audience = _configuration.AudienceAddress; options.Audience = configuration.AudienceAddress;
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidIssuer = _configuration.IssuerAddress, ValidIssuer = configuration.IssuerAddress,
ValidAudience = _configuration.AudienceAddress, ValidAudience = configuration.AudienceAddress,
RoleClaimType = ClaimTypes.Role, RoleClaimType = ClaimTypes.Role,
NameClaimType = ClaimTypes.Name, NameClaimType = ClaimTypes.Name,
}; };
@@ -1,6 +0,0 @@
namespace Musify.Api.DataTransferObjects.Albums;
public record RequestAlbumPictureUploadRequest(
string FileType,
string ContentType,
long? ExpectedSizeBytes = null);
@@ -1,7 +1,7 @@
namespace Musify.Api.DataTransferObjects.Albums; namespace Musify.Api.DataTransferObjects.Albums;
public record UpdateAlbumRequest( public record UpdateAlbumRequest(
string NewTitle, string Title,
string? NewDescription, string? Description,
int? NewReleaseYear, int? ReleaseYear,
Guid? NewPictureIntentId); Guid? PictureIntentId);
@@ -0,0 +1,6 @@
namespace Musify.Api.DataTransferObjects;
public record PictureUploadRequest(
string FileType,
string ContentType,
long? ExpectedSizeBytes = null);
@@ -1,6 +0,0 @@
namespace Musify.Api.DataTransferObjects.PlayLists;
public record RequestPlayListPictureUploadRequest(
string FileType,
string ContentType,
long? ExpectedSizeBytes = null);
@@ -3,7 +3,7 @@ using Musify.Domain.ValueObjects;
namespace Musify.Api.DataTransferObjects.PlayLists; namespace Musify.Api.DataTransferObjects.PlayLists;
public record UpdatePlayListRequest( public record UpdatePlayListRequest(
string? NewName, string? Name,
string? NewDescription, string? Description,
Guid? NewPictureIntentId, Guid? PictureIntentId,
PlayListVisibility? NewVisibility = null); PlayListVisibility? Visibility = null);
@@ -4,12 +4,12 @@ using Musify.Api.DataTransferObjects;
using Musify.Api.DataTransferObjects.Albums; using Musify.Api.DataTransferObjects.Albums;
using Musify.Api.Extensions; using Musify.Api.Extensions;
using Musify.Api.Filters; using Musify.Api.Filters;
using Musify.Api.Models;
using Musify.Application.Albums; using Musify.Application.Albums;
using Musify.Application.Albums.Responses; using Musify.Application.Albums.Responses;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Pictures; using Musify.Application.Pictures;
using Musify.Application.Pictures.Responses; using Musify.Application.Pictures.Responses;
using Musify.Api.Models;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
@@ -27,46 +27,46 @@ public static class AlbumEndpoints
group.MapGet("/", GetAlbums) group.MapGet("/", GetAlbums)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbums") .WithName("GetAlbums")
.WithSummary("Get Paginated Albums, Optionally Filtered By Title.") .WithSummary("Get paginated albums, optionally filtered by title")
.Produces<PaginatedResponse<AlbumApplicationResponse>>(); .Produces<PaginatedResponse<AlbumApplicationResponse>>();
group.MapGet("/{id}", GetAlbumById) group.MapGet("/{id:guid}", GetAlbumById)
.WithName("GetAlbumById") .WithName("GetAlbumById")
.WithSummary("Get An Album By Id.") .WithSummary("Get an album by id")
.Produces<AlbumApplicationResponse>() .Produces<AlbumApplicationResponse>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId}", GetAlbumsByUserId) group.MapGet("/users/{userId:long}", GetAlbumsByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbumsByUserId") .WithName("GetAlbumsByUserId")
.WithSummary("Get Paginated Albums For A User.") .WithSummary("Get paginated albums for a user")
.Produces<PaginatedResponse<AlbumApplicationResponse>>() .Produces<PaginatedResponse<AlbumApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/recent", GetRecentlyListenedAlbums) group.MapGet("/recent", GetRecentlyListenedAlbums)
.WithName("GetRecentlyListenedAlbums") .WithName("GetRecentlyListenedAlbums")
.WithSummary("Get The Albums The Current User Listened To Most Recently.") .WithSummary("Get the albums the current user listened to most recently")
.RequireAuthorization() .RequireAuthorization()
.Produces<IReadOnlyList<AlbumApplicationResponse>>() .Produces<IReadOnlyList<AlbumApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized); .Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/{albumId}/tracks", GetAlbumTracks) group.MapGet("/{id:guid}/tracks", GetAlbumTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetAlbumTracks") .WithName("GetAlbumTracks")
.WithSummary("Get The Tracks Of An Album Ordered By Track Number.") .WithSummary("Get the tracks of an album ordered by track number")
.Produces<PaginatedResponse<TrackApplicationResponse>>() .Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/cover", GetAlbumCover) group.MapGet("/{id:guid}/cover", GetAlbumCover)
.WithName("GetAlbumCover") .WithName("GetAlbumCover")
.WithSummary("Get An Album Cover Image.") .WithSummary("Get an album cover image")
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPost("/upload-picture", RequestAlbumPictureUpload) group.MapPost("/upload-picture", RequestAlbumPictureUpload)
.WithName("RequestAlbumPictureUpload") .WithName("RequestAlbumPictureUpload")
.WithSummary("Request A Pre-Signed URL To Upload An Album Picture.") .WithSummary("Request a pre-signed URL to upload an album picture")
.AddEndpointFilter<ValidationFilter<RequestAlbumPictureUploadRequest>>() .AddEndpointFilter<ValidationFilter<PictureUploadRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<PictureUploadResponse>() .Produces<PictureUploadResponse>()
.ProducesValidationProblem() .ProducesValidationProblem()
@@ -75,7 +75,7 @@ public static class AlbumEndpoints
group.MapPost("/", CreateAlbum) group.MapPost("/", CreateAlbum)
.WithName("CreateAlbum") .WithName("CreateAlbum")
.WithSummary("Create A New Album.") .WithSummary("Create a new album")
.AddEndpointFilter<ValidationFilter<CreateAlbumRequest>>() .AddEndpointFilter<ValidationFilter<CreateAlbumRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<AlbumApplicationResponse>(StatusCodes.Status201Created) .Produces<AlbumApplicationResponse>(StatusCodes.Status201Created)
@@ -83,9 +83,9 @@ public static class AlbumEndpoints
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPut("/{albumId}", UpdateAlbum) group.MapPut("/{id:guid}", UpdateAlbum)
.WithName("UpdateAlbum") .WithName("UpdateAlbum")
.WithSummary("Update An Album.") .WithSummary("Update an album")
.AddEndpointFilter<ValidationFilter<UpdateAlbumRequest>>() .AddEndpointFilter<ValidationFilter<UpdateAlbumRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<AlbumApplicationResponse>() .Produces<AlbumApplicationResponse>()
@@ -94,18 +94,18 @@ public static class AlbumEndpoints
.Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{albumId}", DeleteAlbum) group.MapDelete("/{id:guid}", DeleteAlbum)
.WithName("DeleteAlbum") .WithName("DeleteAlbum")
.WithSummary("Delete An Album.") .WithSummary("Delete an album")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPost("/{id}/tracks", AddAlbumTracks) group.MapPost("/{id:guid}/tracks", AddAlbumTracks)
.WithName("AddAlbumTracks") .WithName("AddAlbumTracks")
.WithSummary("Add Your Uploaded Tracks To An Album, Skipping The Ones It Already Has.") .WithSummary("Add your uploaded tracks to an album, skipping the ones it already has")
.AddEndpointFilter<ValidationFilter<AddTracksRequest>>() .AddEndpointFilter<ValidationFilter<AddTracksRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
@@ -115,9 +115,9 @@ public static class AlbumEndpoints
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict); .Produces(StatusCodes.Status409Conflict);
group.MapDelete("/{albumId}/tracks/{trackId}", RemoveTrackFromAlbum) group.MapDelete("/{id:guid}/tracks/{trackId:guid}", RemoveTrackFromAlbum)
.WithName("RemoveTrackFromAlbum") .WithName("RemoveTrackFromAlbum")
.WithSummary("Remove A Track From An Album.") .WithSummary("Remove a track from an album")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
@@ -176,11 +176,11 @@ public static class AlbumEndpoints
private static async Task<IResult> GetAlbumTracks( private static async Task<IResult> GetAlbumTracks(
IMediator mediator, IMediator mediator,
Guid albumId, Guid id,
CancellationToken cancellationToken, CancellationToken cancellationToken,
[AsParameters] PageQuery page) [AsParameters] PageQuery page)
{ {
var result = await mediator.Send(new GetAlbumTracksQuery(albumId, page.PageNumber, page.PageSize), cancellationToken); var result = await mediator.Send(new GetAlbumTracksQuery(id, page.PageNumber, page.PageSize), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -198,7 +198,7 @@ public static class AlbumEndpoints
private static async Task<IResult> RequestAlbumPictureUpload( private static async Task<IResult> RequestAlbumPictureUpload(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
RequestAlbumPictureUploadRequest request, PictureUploadRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send( var result = await mediator.Send(
@@ -224,12 +224,12 @@ public static class AlbumEndpoints
private static async Task<IResult> UpdateAlbum( private static async Task<IResult> UpdateAlbum(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid albumId, Guid id,
UpdateAlbumRequest request, UpdateAlbumRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send( var result = await mediator.Send(
new UpdateAlbumCommand(currentUser.RequiredId, albumId, request.NewTitle, request.NewDescription, request.NewReleaseYear, request.NewPictureIntentId), new UpdateAlbumCommand(currentUser.RequiredId, id, request.Title, request.Description, request.ReleaseYear, request.PictureIntentId),
cancellationToken); cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
@@ -238,10 +238,10 @@ public static class AlbumEndpoints
private static async Task<IResult> DeleteAlbum( private static async Task<IResult> DeleteAlbum(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid albumId, Guid id,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send(new DeleteAlbumCommand(currentUser.RequiredId, albumId), cancellationToken); var result = await mediator.Send(new DeleteAlbumCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -262,12 +262,12 @@ public static class AlbumEndpoints
private static async Task<IResult> RemoveTrackFromAlbum( private static async Task<IResult> RemoveTrackFromAlbum(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid albumId, Guid id,
Guid trackId, Guid trackId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send( var result = await mediator.Send(
new RemoveTrackFromAlbumCommand(currentUser.RequiredId, albumId, trackId), new RemoveTrackFromAlbumCommand(currentUser.RequiredId, id, trackId),
cancellationToken); cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
@@ -13,16 +13,17 @@ public static class ConfigEndpoints
group.MapGet("/playback", GetPlaybackPublicConfig) group.MapGet("/playback", GetPlaybackPublicConfig)
.WithName("GetPlaybackPublicConfig") .WithName("GetPlaybackPublicConfig")
.WithSummary("Get The Public Playback Configuration (Whether Anonymous Listening Is Allowed). Works Anonymously.") .WithSummary("Get the public playback configuration (whether anonymous listening is allowed). Works anonymously")
.Produces<PlaybackPublicConfigResponse>(); .Produces<PlaybackPublicConfigResponse>();
return app; return app;
} }
private static async Task<PlaybackPublicConfigResponse> GetPlaybackPublicConfig( private static async Task<IResult> GetPlaybackPublicConfig(
IMediator mediator, IMediator mediator,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await mediator.Send(new GetPlaybackPublicConfigQuery(), cancellationToken); var result = await mediator.Send(new GetPlaybackPublicConfigQuery(), cancellationToken);
return Results.Ok(result);
} }
} }
@@ -13,28 +13,30 @@ public static class GenreEndpoints
group.MapGet("/", GetGenres) group.MapGet("/", GetGenres)
.WithName("GetGenres") .WithName("GetGenres")
.WithSummary("Get The Genres That Have At Least One Track, Most Populated First. Works Anonymously.") .WithSummary("Get the genres that have at least one track, most populated first. Works anonymously")
.Produces<IReadOnlyList<GenreResponse>>(); .Produces<IReadOnlyList<GenreResponse>>();
group.MapGet("/available", GetAvailableGenres) group.MapGet("/available", GetAvailableGenres)
.WithName("GetAvailableGenres") .WithName("GetAvailableGenres")
.WithSummary("Get Every Genre A Track Can Be Tagged With, Including The Genres It Cannot Be Combined With. Works Anonymously.") .WithSummary("Get every genre a track can be tagged with, including the genres it cannot be combined with. Works anonymously")
.Produces<IReadOnlyList<AvailableGenreResponse>>(); .Produces<IReadOnlyList<AvailableGenreResponse>>();
return app; return app;
} }
private static async Task<IReadOnlyList<AvailableGenreResponse>> GetAvailableGenres( private static async Task<IResult> GetAvailableGenres(
IMediator mediator, IMediator mediator,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await mediator.Send(new GetAvailableGenresQuery(), cancellationToken); var result = await mediator.Send(new GetAvailableGenresQuery(), cancellationToken);
return Results.Ok(result);
} }
private static async Task<IReadOnlyList<GenreResponse>> GetGenres( private static async Task<IResult> GetGenres(
IMediator mediator, IMediator mediator,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await mediator.Send(new GetGenresQuery(), cancellationToken); var result = await mediator.Send(new GetGenresQuery(), cancellationToken);
return Results.Ok(result);
} }
} }
@@ -2,11 +2,11 @@ using Mediator;
using Musify.Api.Authentication; using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.Likes; using Musify.Api.DataTransferObjects.Likes;
using Musify.Api.Extensions; using Musify.Api.Extensions;
using Musify.Application.Likes; using Musify.Api.Filters;
using Musify.Api.Models; using Musify.Api.Models;
using Musify.Application.Likes;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Api.Filters;
namespace Musify.Api.Endpoints; namespace Musify.Api.Endpoints;
@@ -21,13 +21,13 @@ public static class LikeEndpoints
group.MapGet("/", GetLikedTracks) group.MapGet("/", GetLikedTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetLikedTracks") .WithName("GetLikedTracks")
.WithSummary("Get The Current User'S Liked Tracks.") .WithSummary("Get the current user's liked tracks")
.Produces<PaginatedResponse<TrackApplicationResponse>>() .Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized); .Produces(StatusCodes.Status401Unauthorized);
group.MapPost("/toggle", ToggleTrackLike) group.MapPost("/toggle", ToggleTrackLike)
.WithName("ToggleTrackLike") .WithName("ToggleTrackLike")
.WithSummary("Like Or Unlike A Track By Id.") .WithSummary("Like or unlike a track by id")
.Produces<bool>() .Produces<bool>()
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
@@ -16,13 +16,13 @@ public static class MixEndpoints
group.MapGet("/", GetMixes) group.MapGet("/", GetMixes)
.WithName("GetMixes") .WithName("GetMixes")
.WithSummary("Get The Server-Generated Mixes Of The Current User.") .WithSummary("Get the server-generated mixes of the current user")
.Produces<IReadOnlyList<MixApplicationResponse>>() .Produces<IReadOnlyList<MixApplicationResponse>>()
.Produces(StatusCodes.Status401Unauthorized); .Produces(StatusCodes.Status401Unauthorized);
group.MapGet("/{id}", GetMixById) group.MapGet("/{id:guid}", GetMixById)
.WithName("GetMixById") .WithName("GetMixById")
.WithSummary("Get A Mix With All Of Its Songs.") .WithSummary("Get a mix with all of its songs")
.Produces<MixApplicationResponse>() .Produces<MixApplicationResponse>()
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
@@ -4,12 +4,12 @@ using Musify.Api.DataTransferObjects;
using Musify.Api.DataTransferObjects.PlayLists; using Musify.Api.DataTransferObjects.PlayLists;
using Musify.Api.Extensions; using Musify.Api.Extensions;
using Musify.Api.Filters; using Musify.Api.Filters;
using Musify.Api.Models;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Pictures; using Musify.Application.Pictures;
using Musify.Application.Pictures.Responses; using Musify.Application.Pictures.Responses;
using Musify.Application.PlayLists; using Musify.Application.PlayLists;
using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Responses;
using Musify.Api.Models;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
@@ -22,34 +22,34 @@ public static class PlayListEndpoints
var group = app.MapGroup("/playlists") var group = app.MapGroup("/playlists")
.WithTags("PlayLists"); .WithTags("PlayLists");
group.MapGet("/{id}", GetPlayListById) group.MapGet("/{id:guid}", GetPlayListById)
.WithName("GetPlayListById") .WithName("GetPlayListById")
.WithSummary("Get A PlayList By Id.") .WithSummary("Get a playlist by id")
.Produces<PlayListApplicationResponse>() .Produces<PlayListApplicationResponse>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId}", GetPlayListsByUserId) group.MapGet("/users/{userId:long}", GetPlayListsByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetPlayListsByUserId") .WithName("GetPlayListsByUserId")
.WithSummary("Get Paginated PlayLists For A User. Private PlayLists Are Only Returned To Their Owner.") .WithSummary("Get paginated playlists for a user. Private playlists are only returned to their owner")
.Produces<PaginatedResponse<PlayListApplicationResponse>>(); .Produces<PaginatedResponse<PlayListApplicationResponse>>();
group.MapGet("/{playlistId}/tracks", GetPlayListTracks) group.MapGet("/{id:guid}/tracks", GetPlayListTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetPlayListTracks") .WithName("GetPlayListTracks")
.WithSummary("Get Paginated Tracks Of A PlayList.") .WithSummary("Get paginated tracks of a playlist")
.Produces<PaginatedResponse<TrackApplicationResponse>>() .Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/cover", GetPlayListCover) group.MapGet("/{id:guid}/cover", GetPlayListCover)
.WithName("GetPlayListCover") .WithName("GetPlayListCover")
.WithSummary("Get A PlayList Cover Image.") .WithSummary("Get a playlist cover image")
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPost("/", CreatePlayList) group.MapPost("/", CreatePlayList)
.WithName("CreatePlayList") .WithName("CreatePlayList")
.WithSummary("Create A New PlayList.") .WithSummary("Create a new playlist")
.AddEndpointFilter<ValidationFilter<CreatePlayListRequest>>() .AddEndpointFilter<ValidationFilter<CreatePlayListRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<PlayListApplicationResponse>(StatusCodes.Status201Created) .Produces<PlayListApplicationResponse>(StatusCodes.Status201Created)
@@ -57,9 +57,9 @@ public static class PlayListEndpoints
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPut("/{playlistId}", UpdatePlayList) group.MapPut("/{id:guid}", UpdatePlayList)
.WithName("UpdatePlayList") .WithName("UpdatePlayList")
.WithSummary("Update An Existing PlayList.") .WithSummary("Update an existing playlist")
.AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>() .AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<PlayListApplicationResponse>() .Produces<PlayListApplicationResponse>()
@@ -68,9 +68,9 @@ public static class PlayListEndpoints
.Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{playlistId}", DeletePlayList) group.MapDelete("/{id:guid}", DeletePlayList)
.WithName("DeletePlayList") .WithName("DeletePlayList")
.WithSummary("Delete A PlayList.") .WithSummary("Delete a playlist")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
@@ -79,17 +79,17 @@ public static class PlayListEndpoints
group.MapPost("/upload-picture", RequestPlayListPictureUpload) group.MapPost("/upload-picture", RequestPlayListPictureUpload)
.WithName("RequestPlayListPictureUpload") .WithName("RequestPlayListPictureUpload")
.WithSummary("Request A Pre-Signed URL To Upload A PlayList Picture.") .WithSummary("Request a pre-signed URL to upload a playlist picture")
.AddEndpointFilter<ValidationFilter<RequestPlayListPictureUploadRequest>>() .AddEndpointFilter<ValidationFilter<PictureUploadRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<PictureUploadResponse>() .Produces<PictureUploadResponse>()
.ProducesValidationProblem() .ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPost("/{id}/tracks", AddPlayListTracks) group.MapPost("/{id:guid}/tracks", AddPlayListTracks)
.WithName("AddPlayListTracks") .WithName("AddPlayListTracks")
.WithSummary("Add Tracks To A PlayList, Skipping The Ones It Already Has.") .WithSummary("Add tracks to a playlist, skipping the ones it already has")
.AddEndpointFilter<ValidationFilter<AddTracksRequest>>() .AddEndpointFilter<ValidationFilter<AddTracksRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
@@ -99,9 +99,9 @@ public static class PlayListEndpoints
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict); .Produces(StatusCodes.Status409Conflict);
group.MapDelete("/{playlistId}/tracks/{trackId}", RemoveTrackFromPlayList) group.MapDelete("/{id:guid}/tracks/{trackId:guid}", RemoveTrackFromPlayList)
.WithName("RemoveTrackFromPlayList") .WithName("RemoveTrackFromPlayList")
.WithSummary("Remove A Track From A PlayList.") .WithSummary("Remove a track from a playlist")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
@@ -136,11 +136,11 @@ public static class PlayListEndpoints
private static async Task<IResult> GetPlayListTracks( private static async Task<IResult> GetPlayListTracks(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid playlistId, Guid id,
CancellationToken cancellationToken, CancellationToken cancellationToken,
[AsParameters] PageQuery page) [AsParameters] PageQuery page)
{ {
var result = await mediator.Send(new GetPlayListTracksQuery(playlistId, page.PageNumber, page.PageSize, currentUser.Id), cancellationToken); var result = await mediator.Send(new GetPlayListTracksQuery(id, page.PageNumber, page.PageSize, currentUser.Id), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -171,12 +171,12 @@ public static class PlayListEndpoints
private static async Task<IResult> UpdatePlayList( private static async Task<IResult> UpdatePlayList(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid playlistId, Guid id,
UpdatePlayListRequest request, UpdatePlayListRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send( var result = await mediator.Send(
new UpdatePlayListCommand(currentUser.RequiredId, playlistId, request.NewName, request.NewDescription, request.NewPictureIntentId, request.NewVisibility), new UpdatePlayListCommand(currentUser.RequiredId, id, request.Name, request.Description, request.PictureIntentId, request.Visibility),
cancellationToken); cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
@@ -185,17 +185,17 @@ public static class PlayListEndpoints
private static async Task<IResult> DeletePlayList( private static async Task<IResult> DeletePlayList(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid playlistId, Guid id,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send(new DeletePlayListCommand(currentUser.RequiredId, playlistId), cancellationToken); var result = await mediator.Send(new DeletePlayListCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
} }
private static async Task<IResult> RequestPlayListPictureUpload( private static async Task<IResult> RequestPlayListPictureUpload(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
RequestPlayListPictureUploadRequest request, PictureUploadRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send( var result = await mediator.Send(
@@ -222,12 +222,12 @@ public static class PlayListEndpoints
private static async Task<IResult> RemoveTrackFromPlayList( private static async Task<IResult> RemoveTrackFromPlayList(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid playlistId, Guid id,
Guid trackId, Guid trackId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send( var result = await mediator.Send(
new RemoveTrackFromPlayListCommand(currentUser.RequiredId, playlistId, trackId), new RemoveTrackFromPlayListCommand(currentUser.RequiredId, id, trackId),
cancellationToken); cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
@@ -3,9 +3,9 @@ using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.Tracks; using Musify.Api.DataTransferObjects.Tracks;
using Musify.Api.Extensions; using Musify.Api.Extensions;
using Musify.Api.Filters; using Musify.Api.Filters;
using Musify.Api.Models;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Pictures; using Musify.Application.Pictures;
using Musify.Api.Models;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks; using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
@@ -22,47 +22,47 @@ public static class TrackEndpoints
group.MapGet("/", GetTracks) group.MapGet("/", GetTracks)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetTracks") .WithName("GetTracks")
.WithSummary("Get Paginated Tracks, Optionally Filtered By Name And Genre.") .WithSummary("Get paginated tracks, optionally filtered by name and genre")
.Produces<PaginatedResponse<TrackApplicationResponse>>() .Produces<PaginatedResponse<TrackApplicationResponse>>()
.ProducesValidationProblem(); .ProducesValidationProblem();
group.MapGet("/{id}", GetTrackById) group.MapGet("/{id:guid}", GetTrackById)
.WithName("GetTrackById") .WithName("GetTrackById")
.WithSummary("Get A Track By Id.") .WithSummary("Get a track by id")
.Produces<TrackApplicationResponse>() .Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/cover", GetTrackCover) group.MapGet("/{id:guid}/cover", GetTrackCover)
.WithName("GetTrackCover") .WithName("GetTrackCover")
.WithSummary("Get A Track Cover Image.") .WithSummary("Get a track cover image")
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/stream", GetTrackStream) group.MapGet("/{id:guid}/stream", GetTrackStream)
.WithName("GetTrackStream") .WithName("GetTrackStream")
.WithSummary("Get A Streaming Manifest URL And Ticket For A Track. Works Anonymously When The Playback Configuration Allows It.") .WithSummary("Get a streaming manifest URL and ticket for a track. Works anonymously when the playback configuration allows it")
.Produces<TrackStreamResponse>() .Produces<TrackStreamResponse>()
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapPut("/listens/{listenId}/progress", RecordListeningProgress) group.MapPut("/listens/{listenId:guid}/progress", RecordListeningProgress)
.AddEndpointFilter<ValidationFilter<RecordListeningProgressRequest>>() .AddEndpointFilter<ValidationFilter<RecordListeningProgressRequest>>()
.WithName("RecordListeningProgress") .WithName("RecordListeningProgress")
.WithSummary("Report The Total Seconds Actually Played For A Listen Started By The Stream Endpoint.") .WithSummary("Report the total seconds actually played for a listen started by the stream endpoint")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId}", GetTracksByUserId) group.MapGet("/users/{userId:long}", GetTracksByUserId)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetTracksByUserId") .WithName("GetTracksByUserId")
.WithSummary("Get Paginated Tracks For A User.") .WithSummary("Get paginated tracks for a user")
.Produces<PaginatedResponse<TrackApplicationResponse>>(); .Produces<PaginatedResponse<TrackApplicationResponse>>();
group.MapPost("/", CreateTrack) group.MapPost("/", CreateTrack)
.WithName("CreateTrack") .WithName("CreateTrack")
.WithSummary("Create A New Track.") .WithSummary("Create a new track")
.AddEndpointFilter<ValidationFilter<CreateTrackRequest>>() .AddEndpointFilter<ValidationFilter<CreateTrackRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<TrackApplicationResponse>(StatusCodes.Status201Created) .Produces<TrackApplicationResponse>(StatusCodes.Status201Created)
@@ -71,9 +71,9 @@ public static class TrackEndpoints
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict); .Produces(StatusCodes.Status409Conflict);
group.MapDelete("/{trackId}", DeleteTrack) group.MapDelete("/{id:guid}", DeleteTrack)
.WithName("DeleteTrack") .WithName("DeleteTrack")
.WithSummary("Delete A Track.") .WithSummary("Delete a track")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
@@ -83,7 +83,7 @@ public static class TrackEndpoints
group.MapPost("/upload-urls", RequestTrackUploadUrls) group.MapPost("/upload-urls", RequestTrackUploadUrls)
.WithName("RequestTrackUploadUrls") .WithName("RequestTrackUploadUrls")
.WithSummary("Request Pre-Signed URLs To Upload Track Picture And Audio.") .WithSummary("Request pre-signed URLs to upload track picture and audio")
.AddEndpointFilter<ValidationFilter<RequestTrackUploadUrlsRequest>>() .AddEndpointFilter<ValidationFilter<RequestTrackUploadUrlsRequest>>()
.RequireAuthorization() .RequireAuthorization()
.Produces<TrackUploadUrlsResponse>() .Produces<TrackUploadUrlsResponse>()
@@ -176,10 +176,10 @@ public static class TrackEndpoints
private static async Task<IResult> DeleteTrack( private static async Task<IResult> DeleteTrack(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
Guid trackId, Guid id,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await mediator.Send(new DeleteTrackCommand(currentUser.RequiredId, trackId), cancellationToken); var result = await mediator.Send(new DeleteTrackCommand(currentUser.RequiredId, id), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -1,13 +1,13 @@
using Mediator; using Mediator;
using Musify.Api.Authentication; using Musify.Api.Authentication;
using Musify.Api.Extensions; using Musify.Api.Extensions;
using Musify.Api.Filters;
using Musify.Api.Models; using Musify.Api.Models;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks; using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Application.Users; using Musify.Application.Users;
using Musify.Application.Users.Responses; using Musify.Application.Users.Responses;
using Musify.Api.Filters;
namespace Musify.Api.Endpoints; namespace Musify.Api.Endpoints;
@@ -21,75 +21,75 @@ public static class UserEndpoints
group.MapGet("/", GetUsers) group.MapGet("/", GetUsers)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUsers") .WithName("GetUsers")
.WithSummary("Get Paginated Users.") .WithSummary("Get paginated users")
.Produces<PaginatedResponse<UserSummaryResponse>>(); .Produces<PaginatedResponse<UserSummaryResponse>>();
group.MapGet("/{id}", GetUserById) group.MapGet("/{id:long}", GetUserById)
.WithName("GetUserById") .WithName("GetUserById")
.WithSummary("Get A User By Id.") .WithSummary("Get a user by id")
.Produces<UserApplicationResponse>() .Produces<UserApplicationResponse>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/listening-history", GetListeningHistory) group.MapGet("/{id:long}/listening-history", GetListeningHistory)
.WithName("GetListeningHistory") .WithName("GetListeningHistory")
.WithSummary("Get A User'S Listening History.") .WithSummary("Get a user's listening history")
.Produces<IEnumerable<TrackApplicationResponse>>() .Produces<IEnumerable<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/listening-stats", GetListeningStats) group.MapGet("/{id:long}/listening-stats", GetListeningStats)
.WithName("GetListeningStats") .WithName("GetListeningStats")
.WithSummary("Get A User'S Listening Stats For The Current Week And Streak.") .WithSummary("Get a user's listening stats for the current week and streak")
.RequireAuthorization() .RequireAuthorization()
.Produces<ListeningStatsResponse>() .Produces<ListeningStatsResponse>()
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden); .Produces(StatusCodes.Status403Forbidden);
group.MapGet("/{id}/last-listened-track", GetLastTrackListenedByUserId) group.MapGet("/{id:long}/last-listened-track", GetLastTrackListenedByUserId)
.WithName("GetLastTrackListenedByUserId") .WithName("GetLastTrackListenedByUserId")
.WithSummary("Get The Last Track A User Listened To.") .WithSummary("Get the last track a user listened to")
.Produces<TrackApplicationResponse>() .Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status204NoContent); .Produces(StatusCodes.Status204NoContent);
group.MapGet("/{id}/profile", GetUserProfile) group.MapGet("/{id:long}/profile", GetUserProfile)
.WithName("GetUserProfile") .WithName("GetUserProfile")
.WithSummary("Get A User'S Public Profile, Including Follower Counts.") .WithSummary("Get a user's public profile, including follower counts")
.Produces<UserProfileResponse>() .Produces<UserProfileResponse>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/followers", GetFollowers) group.MapGet("/{id:long}/followers", GetFollowers)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUserFollowers") .WithName("GetUserFollowers")
.WithSummary("Get The Users Following A User. Visible To The User And To Mutual Followers.") .WithSummary("Get the users following a user. Visible to the user and to mutual followers")
.Produces<PaginatedResponse<UserSummaryResponse>>() .Produces<PaginatedResponse<UserSummaryResponse>>()
.Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/following", GetFollowing) group.MapGet("/{id:long}/following", GetFollowing)
.AddEndpointFilter<ValidationFilter<PageQuery>>() .AddEndpointFilter<ValidationFilter<PageQuery>>()
.WithName("GetUserFollowing") .WithName("GetUserFollowing")
.WithSummary("Get The Users A User Follows.") .WithSummary("Get the users a user follows")
.Produces<PaginatedResponse<UserSummaryResponse>>() .Produces<PaginatedResponse<UserSummaryResponse>>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/is-following", IsFollowing) group.MapGet("/{id:long}/is-following", IsFollowing)
.WithName("IsFollowingUser") .WithName("IsFollowingUser")
.WithSummary("Check Whether The Current User Follows Another User.") .WithSummary("Check whether the current user follows another user")
.RequireAuthorization() .RequireAuthorization()
.Produces<bool>() .Produces<bool>()
.Produces(StatusCodes.Status401Unauthorized); .Produces(StatusCodes.Status401Unauthorized);
group.MapPost("/{id}/follow", FollowUser) group.MapPost("/{id:long}/follow", FollowUser)
.WithName("FollowUser") .WithName("FollowUser")
.WithSummary("Follow A User.") .WithSummary("Follow a user")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.ProducesValidationProblem(); .ProducesValidationProblem();
group.MapDelete("/{id}/follow", UnfollowUser) group.MapDelete("/{id:long}/follow", UnfollowUser)
.WithName("UnfollowUser") .WithName("UnfollowUser")
.WithSummary("Unfollow A User.") .WithSummary("Unfollow a user")
.RequireAuthorization() .RequireAuthorization()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized); .Produces(StatusCodes.Status401Unauthorized);
@@ -117,12 +117,13 @@ public static class UserEndpoints
return result.ToHttpResult(); return result.ToHttpResult();
} }
private static async Task<IEnumerable<TrackApplicationResponse>> GetListeningHistory( private static async Task<IResult> GetListeningHistory(
IMediator mediator, IMediator mediator,
long id, long id,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await mediator.Send(new GetListeningHistoryQuery(id), cancellationToken); var result = await mediator.Send(new GetListeningHistoryQuery(id), cancellationToken);
return Results.Ok(result);
} }
private static async Task<IResult> GetListeningStats( private static async Task<IResult> GetListeningStats(
@@ -132,9 +133,10 @@ public static class UserEndpoints
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (currentUser.RequiredId != id) if (currentUser.RequiredId != id)
return Results.StatusCode(StatusCodes.Status403Forbidden); return Results.Problem(statusCode: StatusCodes.Status403Forbidden);
return Results.Ok(await mediator.Send(new GetListeningStatsQuery(id), cancellationToken)); var result = await mediator.Send(new GetListeningStatsQuery(id), cancellationToken);
return Results.Ok(result);
} }
private static async Task<IResult> GetLastTrackListenedByUserId( private static async Task<IResult> GetLastTrackListenedByUserId(
@@ -178,13 +180,14 @@ public static class UserEndpoints
return result.ToHttpResult(); return result.ToHttpResult();
} }
private static async Task<bool> IsFollowing( private static async Task<IResult> IsFollowing(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
long id, long id,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await mediator.Send(new IsFollowingUserQuery(currentUser.RequiredId, id), cancellationToken); var result = await mediator.Send(new IsFollowingUserQuery(currentUser.RequiredId, id), cancellationToken);
return Results.Ok(result);
} }
private static async Task<IResult> FollowUser( private static async Task<IResult> FollowUser(
@@ -9,9 +9,9 @@ namespace Musify.Api.OpenApi;
public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> options) public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> options)
: IConfigureNamedOptions<OpenApiOptions> : IConfigureNamedOptions<OpenApiOptions>
{ {
private const string SecuritySchemeId = "OAuth2"; internal const string SecuritySchemeId = "OAuth2";
private readonly AuthenticationConfiguration _configuration = options.Value; private readonly AuthenticationConfiguration configuration = options.Value;
public void Configure(string? name, OpenApiOptions options) => Configure(options); public void Configure(string? name, OpenApiOptions options) => Configure(options);
@@ -28,9 +28,9 @@ public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> op
{ {
AuthorizationCode = new OpenApiOAuthFlow AuthorizationCode = new OpenApiOAuthFlow
{ {
AuthorizationUrl = new Uri(_configuration.AuthorizationEndpoint), AuthorizationUrl = new Uri(configuration.AuthorizationEndpoint),
TokenUrl = new Uri(_configuration.TokenEndpoint), TokenUrl = new Uri(configuration.TokenEndpoint),
Scopes = _configuration.Scopes.ToDictionary(scope => scope, scope => scope), Scopes = configuration.Scopes.ToDictionary(scope => scope, scope => scope),
}, },
}, },
}; };
@@ -57,6 +57,16 @@ public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> op
options.AddOperationTransformer((operation, context, _) => options.AddOperationTransformer((operation, context, _) =>
{ {
// Route parameters constrained with :long are user ids, which travel as strings like every other long.
foreach (var parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
{
if (parameter.Schema is OpenApiSchema { Type: JsonSchemaType.Integer, Format: "int64" } schema)
{
schema.Type = JsonSchemaType.String;
schema.Format = null;
}
}
var requiresAuthorization = context.Description.ActionDescriptor.EndpointMetadata var requiresAuthorization = context.Description.ActionDescriptor.EndpointMetadata
.OfType<IAuthorizeData>() .OfType<IAuthorizeData>()
.Any(); .Any();
@@ -66,15 +76,13 @@ public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> op
.Any(); .Any();
if (!requiresAuthorization || allowsAnonymous) if (!requiresAuthorization || allowsAnonymous)
{
return Task.CompletedTask; return Task.CompletedTask;
}
operation.Security ??= new List<OpenApiSecurityRequirement>(); operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement operation.Security.Add(new OpenApiSecurityRequirement
{ {
[new OpenApiSecuritySchemeReference(SecuritySchemeId, context.Document)] = [new OpenApiSecuritySchemeReference(SecuritySchemeId, context.Document)] =
_configuration.Scopes.ToList(), configuration.Scopes.ToList(),
}); });
return Task.CompletedTask; return Task.CompletedTask;
+6 -5
View File
@@ -5,7 +5,9 @@ using Musify.Api.Serialization;
using Musify.Infrastructure.Observability; using Musify.Infrastructure.Observability;
using Scalar.AspNetCore; using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(); const string DevCorsPolicy = "dev-cors";
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => builder.Services.ConfigureHttpJsonOptions(options =>
{ {
@@ -14,17 +16,16 @@ builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new NullableLongAsStringConverter()); options.SerializerOptions.Converters.Add(new NullableLongAsStringConverter());
}); });
const string devCorsPolicy = "dev-cors";
builder.Services builder.Services
.AddApplicationServices(builder.Configuration) .AddApplicationServices(builder.Configuration)
.AddObservability(builder.Configuration); .AddObservability(builder.Configuration);
builder.Services.AddProblemDetails(); builder.Services.AddProblemDetails();
builder.Services.Configure<RouteHandlerOptions>(options => options.ThrowOnBadRequest = false);
if (builder.Environment.IsDevelopment()) if (builder.Environment.IsDevelopment())
{ {
builder.Services.AddCors(options => options.AddPolicy(devCorsPolicy, policy => builder.Services.AddCors(options => options.AddPolicy(DevCorsPolicy, policy =>
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
} }
@@ -34,7 +35,7 @@ app.UseExceptionHandler();
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
app.UseCors(devCorsPolicy); app.UseCors(DevCorsPolicy);
app.MapOpenApi(); app.MapOpenApi();
app.MapScalarApiReference(); app.MapScalarApiReference();
} }
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Musify.Api.Authentication; using Musify.Api.Authentication;
using Musify.Api.OpenApi;
using Scalar.AspNetCore; using Scalar.AspNetCore;
namespace Musify.Api.Scalar; namespace Musify.Api.Scalar;
@@ -7,25 +8,23 @@ namespace Musify.Api.Scalar;
public sealed class ScalarOptionsSetup(IOptions<AuthenticationConfiguration> options) public sealed class ScalarOptionsSetup(IOptions<AuthenticationConfiguration> options)
: IConfigureOptions<ScalarOptions> : IConfigureOptions<ScalarOptions>
{ {
private const string SecuritySchemeId = "OAuth2"; private readonly AuthenticationConfiguration configuration = options.Value;
private readonly AuthenticationConfiguration _configuration = options.Value;
public void Configure(ScalarOptions scalarOptions) public void Configure(ScalarOptions scalarOptions)
{ {
scalarOptions scalarOptions
.WithTitle("Musify API") .WithTitle("Musify API")
.AddPreferredSecuritySchemes(SecuritySchemeId) .AddPreferredSecuritySchemes(OpenApiOptionsSetup.SecuritySchemeId)
.AddAuthorizationCodeFlow(SecuritySchemeId, flow => .AddAuthorizationCodeFlow(OpenApiOptionsSetup.SecuritySchemeId, flow =>
{ {
flow.ClientId = _configuration.ClientId; flow.ClientId = configuration.ClientId;
flow.ClientSecret = _configuration.ClientSecret; flow.ClientSecret = configuration.ClientSecret;
flow.AuthorizationUrl = _configuration.AuthorizationEndpoint; flow.AuthorizationUrl = configuration.AuthorizationEndpoint;
flow.TokenUrl = _configuration.TokenEndpoint; flow.TokenUrl = configuration.TokenEndpoint;
flow.Pkce = Pkce.Sha256; flow.Pkce = Pkce.Sha256;
flow.SelectedScopes = _configuration.Scopes; flow.SelectedScopes = configuration.Scopes;
flow.RedirectUri = _configuration.ScalarRedirectUri; flow.RedirectUri = configuration.ScalarRedirectUri;
flow.RefreshUrl = _configuration.TokenEndpoint; flow.RefreshUrl = configuration.TokenEndpoint;
}) })
.HideModels(); .HideModels();
} }
@@ -1,8 +0,0 @@
namespace Musify.Api.Validators.Albums;
public static class AlbumReleaseYear
{
public const int Earliest = 1877;
public static int Latest => DateTime.UtcNow.Year + 1;
}
@@ -9,13 +9,13 @@ public sealed class CreateAlbumRequestValidator : AbstractValidator<CreateAlbumR
{ {
RuleFor(x => x.Title) RuleFor(x => x.Title)
.NotEmpty() .NotEmpty()
.MaximumLength(200); .MaximumLength(Limits.AlbumTitle);
RuleFor(x => x.Description) RuleFor(x => x.Description)
.MaximumLength(256); .MaximumLength(Limits.Description);
RuleFor(x => x.ReleaseYear) RuleFor(x => x.ReleaseYear)
.InclusiveBetween(AlbumReleaseYear.Earliest, AlbumReleaseYear.Latest) .InclusiveBetween(Limits.EarliestReleaseYear, Limits.LatestReleaseYear)
.When(x => x.ReleaseYear.HasValue); .When(x => x.ReleaseYear.HasValue);
RuleFor(x => x.PictureIntentId) RuleFor(x => x.PictureIntentId)
@@ -7,15 +7,15 @@ public sealed class UpdateAlbumRequestValidator : AbstractValidator<UpdateAlbumR
{ {
public UpdateAlbumRequestValidator() public UpdateAlbumRequestValidator()
{ {
RuleFor(x => x.NewTitle) RuleFor(x => x.Title)
.NotEmpty() .NotEmpty()
.MaximumLength(200); .MaximumLength(Limits.AlbumTitle);
RuleFor(x => x.NewDescription) RuleFor(x => x.Description)
.MaximumLength(256); .MaximumLength(Limits.Description);
RuleFor(x => x.NewReleaseYear) RuleFor(x => x.ReleaseYear)
.InclusiveBetween(AlbumReleaseYear.Earliest, AlbumReleaseYear.Latest) .InclusiveBetween(Limits.EarliestReleaseYear, Limits.LatestReleaseYear)
.When(x => x.NewReleaseYear.HasValue); .When(x => x.ReleaseYear.HasValue);
} }
} }
@@ -0,0 +1,12 @@
namespace Musify.Api.Validators;
public static class Limits
{
public const int TrackTitle = 200;
public const int AlbumTitle = 200;
public const int PlayListName = 50;
public const int Description = 256;
public const int EarliestReleaseYear = 1877;
public static int LatestReleaseYear => DateTime.UtcNow.Year + 1;
}
@@ -1,12 +1,12 @@
using FluentValidation; using FluentValidation;
using Musify.Api.DataTransferObjects.Albums; using Musify.Api.DataTransferObjects;
using Musify.Application.Configuration; using Musify.Application.Configuration;
namespace Musify.Api.Validators.Albums; namespace Musify.Api.Validators;
public sealed class RequestAlbumPictureUploadRequestValidator : AbstractValidator<RequestAlbumPictureUploadRequest> public sealed class PictureUploadRequestValidator : AbstractValidator<PictureUploadRequest>
{ {
public RequestAlbumPictureUploadRequestValidator(UploadIntentConfiguration uploadConfiguration) public PictureUploadRequestValidator(UploadIntentConfiguration uploadConfiguration)
{ {
RuleFor(x => x.FileType) RuleFor(x => x.FileType)
.MustBeFileType(Uploads.PictureFileTypes); .MustBeFileType(Uploads.PictureFileTypes);
@@ -9,10 +9,10 @@ public sealed class CreatePlayListRequestValidator : AbstractValidator<CreatePla
{ {
RuleFor(x => x.Name) RuleFor(x => x.Name)
.NotEmpty() .NotEmpty()
.MaximumLength(50); .MaximumLength(Limits.PlayListName);
RuleFor(x => x.Description) RuleFor(x => x.Description)
.MaximumLength(256); .MaximumLength(Limits.Description);
RuleFor(x => x.Visibility) RuleFor(x => x.Visibility)
.IsInEnum(); .IsInEnum();
@@ -1,20 +0,0 @@
using FluentValidation;
using Musify.Api.DataTransferObjects.PlayLists;
using Musify.Application.Configuration;
namespace Musify.Api.Validators.PlayLists;
public sealed class RequestPlayListPictureUploadRequestValidator : AbstractValidator<RequestPlayListPictureUploadRequest>
{
public RequestPlayListPictureUploadRequestValidator(UploadIntentConfiguration uploadConfiguration)
{
RuleFor(x => x.FileType)
.MustBeFileType(Uploads.PictureFileTypes);
RuleFor(x => x.ContentType)
.MustBeContentType(Uploads.PictureContentTypes);
RuleFor(x => x.ExpectedSizeBytes)
.MustBeValidUploadSize(uploadConfiguration.MaxUploadBytes);
}
}
@@ -7,17 +7,17 @@ public sealed class UpdatePlayListRequestValidator : AbstractValidator<UpdatePla
{ {
public UpdatePlayListRequestValidator() public UpdatePlayListRequestValidator()
{ {
RuleFor(x => x.NewName) RuleFor(x => x.Name)
.NotEmpty() .NotEmpty()
.MaximumLength(50) .MaximumLength(Limits.PlayListName)
.When(x => x.NewName != null); .When(x => x.Name != null);
RuleFor(x => x.NewDescription) RuleFor(x => x.Description)
.MaximumLength(256) .MaximumLength(Limits.Description)
.When(x => x.NewDescription != null); .When(x => x.Description != null);
RuleFor(x => x.NewVisibility) RuleFor(x => x.Visibility)
.IsInEnum() .IsInEnum()
.When(x => x.NewVisibility != null); .When(x => x.Visibility != null);
} }
} }
@@ -10,7 +10,7 @@ public sealed class CreateTrackRequestValidator : AbstractValidator<CreateTrackR
{ {
RuleFor(x => x.Title) RuleFor(x => x.Title)
.NotEmpty() .NotEmpty()
.MaximumLength(200); .MaximumLength(Limits.TrackTitle);
RuleFor(x => x.PictureIntentId) RuleFor(x => x.PictureIntentId)
.NotEmpty(); .NotEmpty();
@@ -1,5 +1,6 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using Musify.Api.DataTransferObjects;
using Musify.Api.DataTransferObjects.Albums; using Musify.Api.DataTransferObjects.Albums;
using Musify.Api.Tests.TestSupport; using Musify.Api.Tests.TestSupport;
using Musify.Application.Albums.Responses; using Musify.Application.Albums.Responses;
@@ -26,7 +27,7 @@ public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
var response = await client.PostAsJsonAsync( var response = await client.PostAsJsonAsync(
"/albums/upload-picture", "/albums/upload-picture",
new RequestAlbumPictureUploadRequest("webp", "image/webp"), new PictureUploadRequest("webp", "image/webp"),
TestContext.Current.CancellationToken); TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@@ -1,4 +1,5 @@
using Musify.Api.DataTransferObjects.Albums; using Musify.Api.DataTransferObjects.Albums;
using Musify.Api.Validators;
using Musify.Api.Validators.Albums; using Musify.Api.Validators.Albums;
using Xunit; using Xunit;
@@ -44,7 +45,7 @@ public sealed class CreateAlbumRequestValidatorTests
[Fact] [Fact]
public void Validate_ReleaseYearBeforeTheEarliestBoundary_Fails() public void Validate_ReleaseYearBeforeTheEarliestBoundary_Fails()
{ {
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Earliest - 1, Guid.NewGuid())); var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.EarliestReleaseYear - 1, Guid.NewGuid()));
Assert.False(result.IsValid); Assert.False(result.IsValid);
} }
@@ -52,7 +53,7 @@ public sealed class CreateAlbumRequestValidatorTests
[Fact] [Fact]
public void Validate_ReleaseYearAtTheEarliestBoundary_Passes() public void Validate_ReleaseYearAtTheEarliestBoundary_Passes()
{ {
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Earliest, Guid.NewGuid())); var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.EarliestReleaseYear, Guid.NewGuid()));
Assert.True(result.IsValid); Assert.True(result.IsValid);
} }
@@ -60,7 +61,7 @@ public sealed class CreateAlbumRequestValidatorTests
[Fact] [Fact]
public void Validate_ReleaseYearAtTheLatestBoundary_Passes() public void Validate_ReleaseYearAtTheLatestBoundary_Passes()
{ {
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Latest, Guid.NewGuid())); var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.LatestReleaseYear, Guid.NewGuid()));
Assert.True(result.IsValid); Assert.True(result.IsValid);
} }
@@ -68,7 +69,7 @@ public sealed class CreateAlbumRequestValidatorTests
[Fact] [Fact]
public void Validate_ReleaseYearPastTheLatestBoundary_Fails() public void Validate_ReleaseYearPastTheLatestBoundary_Fails()
{ {
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Latest + 1, Guid.NewGuid())); var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.LatestReleaseYear + 1, Guid.NewGuid()));
Assert.False(result.IsValid); Assert.False(result.IsValid);
} }
@@ -1,6 +1,6 @@
using Musify.Api.DataTransferObjects.Albums; using Musify.Api.DataTransferObjects;
using Musify.Api.DataTransferObjects.Tracks; using Musify.Api.DataTransferObjects.Tracks;
using Musify.Api.Validators.Albums; using Musify.Api.Validators;
using Musify.Api.Validators.Tracks; using Musify.Api.Validators.Tracks;
using Musify.Application.Configuration; using Musify.Application.Configuration;
using Xunit; using Xunit;
@@ -14,9 +14,9 @@ public sealed class UploadRequestValidatorTests
[Fact] [Fact]
public void PictureUpload_ValidRequest_Passes() public void PictureUpload_ValidRequest_Passes()
{ {
var validator = new RequestAlbumPictureUploadRequestValidator(configuration); var validator = new PictureUploadRequestValidator(configuration);
var result = validator.Validate(new RequestAlbumPictureUploadRequest("webp", "image/webp", 500)); var result = validator.Validate(new PictureUploadRequest("webp", "image/webp", 500));
Assert.True(result.IsValid); Assert.True(result.IsValid);
} }
@@ -30,9 +30,9 @@ public sealed class UploadRequestValidatorTests
[InlineData("webp", "image/webp", 1_001L)] [InlineData("webp", "image/webp", 1_001L)]
public void PictureUpload_InvalidRequest_Fails(string fileType, string contentType, long size) public void PictureUpload_InvalidRequest_Fails(string fileType, string contentType, long size)
{ {
var validator = new RequestAlbumPictureUploadRequestValidator(configuration); var validator = new PictureUploadRequestValidator(configuration);
var result = validator.Validate(new RequestAlbumPictureUploadRequest(fileType, contentType, size)); var result = validator.Validate(new PictureUploadRequest(fileType, contentType, size));
Assert.False(result.IsValid); Assert.False(result.IsValid);
} }
+471 -557
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -40,8 +40,8 @@ export async function addAlbumToPlaylistAction({ request, params, locals, fetch
} }
const api = createApiClient({ fetch, accessToken }); const api = createApiClient({ fetch, accessToken });
const { data: tracks, error: tracksErr } = await api.GET('/albums/{albumId}/tracks', { const { data: tracks, error: tracksErr } = await api.GET('/albums/{id}/tracks', {
params: { path: { albumId }, query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE } } params: { path: { id: albumId }, query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE } }
}); });
if (tracksErr || !tracks) { if (tracksErr || !tracks) {
@@ -18,9 +18,9 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
const [albumRes, tracksRes, libraryRes] = await Promise.all([ const [albumRes, tracksRes, libraryRes] = await Promise.all([
api.GET('/albums/{id}', { params: { path: { id: params.id } } }), api.GET('/albums/{id}', { params: { path: { id: params.id } } }),
api.GET('/albums/{albumId}/tracks', { api.GET('/albums/{id}/tracks', {
params: { params: {
path: { albumId: params.id }, path: { id: params.id },
query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE } query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE }
} }
}), }),
@@ -71,8 +71,8 @@ export const actions: Actions = {
if (!trackId) return fail(400, { message: 'Falta la canción.' }); if (!trackId) return fail(400, { message: 'Falta la canción.' });
const api = createApiClient({ fetch, accessToken }); const api = createApiClient({ fetch, accessToken });
const result = await api.DELETE('/albums/{albumId}/tracks/{trackId}', { const result = await api.DELETE('/albums/{id}/tracks/{trackId}', {
params: { path: { albumId: params.id, trackId } } params: { path: { id: params.id, trackId } }
}); });
const failure = unwrapOrFail(result, 'No se pudo quitar la canción.'); const failure = unwrapOrFail(result, 'No se pudo quitar la canción.');
if (failure) return failure; if (failure) return failure;
@@ -102,13 +102,13 @@ export const actions: Actions = {
newPictureIntentId = uploaded.intentId; newPictureIntentId = uploaded.intentId;
} }
const result = await api.PUT('/albums/{albumId}', { const result = await api.PUT('/albums/{id}', {
params: { path: { albumId: params.id } }, params: { path: { id: params.id } },
body: { body: {
newTitle: parsed.body.title, title: parsed.body.title,
newDescription: parsed.body.description, description: parsed.body.description,
newReleaseYear: parsed.body.releaseYear, releaseYear: parsed.body.releaseYear,
newPictureIntentId pictureIntentId: newPictureIntentId
} }
}); });
const failure = unwrapOrFail(result, 'No se pudo actualizar el álbum.'); const failure = unwrapOrFail(result, 'No se pudo actualizar el álbum.');
@@ -121,8 +121,8 @@ export const actions: Actions = {
if (typeof accessToken !== 'string') return accessToken; if (typeof accessToken !== 'string') return accessToken;
const api = createApiClient({ fetch, accessToken }); const api = createApiClient({ fetch, accessToken });
const result = await api.DELETE('/albums/{albumId}', { const result = await api.DELETE('/albums/{id}', {
params: { path: { albumId: params.id } } params: { path: { id: params.id } }
}); });
const failure = unwrapOrFail(result, 'No se pudo eliminar el álbum.'); const failure = unwrapOrFail(result, 'No se pudo eliminar el álbum.');
if (failure) return failure; if (failure) return failure;
@@ -28,8 +28,8 @@ export const actions: Actions = {
if (!trackId) return fail(400, { message: 'Falta la canción.' }); if (!trackId) return fail(400, { message: 'Falta la canción.' });
const api = createApiClient({ fetch, accessToken }); const api = createApiClient({ fetch, accessToken });
const result = await api.DELETE('/tracks/{trackId}', { const result = await api.DELETE('/tracks/{id}', {
params: { path: { trackId } } params: { path: { id: trackId } }
}); });
const failure = unwrapOrFail(result, 'No se pudo borrar la canción.'); const failure = unwrapOrFail(result, 'No se pudo borrar la canción.');
if (failure) return failure; if (failure) return failure;
@@ -26,8 +26,8 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
fetchAllPages( fetchAllPages(
async (pageNumber, pageSize) => async (pageNumber, pageSize) =>
( (
await api.GET('/playlists/{playlistId}/tracks', { await api.GET('/playlists/{id}/tracks', {
params: { path: { playlistId: playlist.id }, query: { pageNumber, pageSize } } params: { path: { id: playlist.id }, query: { pageNumber, pageSize } }
}) })
).data, ).data,
PLAYLIST_TRACKS_LIMIT PLAYLIST_TRACKS_LIMIT
@@ -23,8 +23,8 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
fetchAllPages( fetchAllPages(
async (pageNumber, pageSize) => async (pageNumber, pageSize) =>
( (
await api.GET('/playlists/{playlistId}/tracks', { await api.GET('/playlists/{id}/tracks', {
params: { path: { playlistId: params.id }, query: { pageNumber, pageSize } } params: { path: { id: params.id }, query: { pageNumber, pageSize } }
}) })
).data, ).data,
PLAYLIST_TRACKS_LIMIT PLAYLIST_TRACKS_LIMIT
@@ -44,8 +44,8 @@ export const actions: Actions = {
if (!trackId) return fail(400, { message: 'Falta la canción.' }); if (!trackId) return fail(400, { message: 'Falta la canción.' });
const api = createApiClient({ fetch, accessToken }); const api = createApiClient({ fetch, accessToken });
const result = await api.DELETE('/playlists/{playlistId}/tracks/{trackId}', { const result = await api.DELETE('/playlists/{id}/tracks/{trackId}', {
params: { path: { playlistId: params.id, trackId } } params: { path: { id: params.id, trackId } }
}); });
const failure = unwrapOrFail(result, 'No se pudo quitar la canción.'); const failure = unwrapOrFail(result, 'No se pudo quitar la canción.');
if (failure) return failure; if (failure) return failure;
@@ -78,9 +78,14 @@ export const actions: Actions = {
newPictureIntentId = result.intentId; newPictureIntentId = result.intentId;
} }
const result = await api.PUT('/playlists/{playlistId}', { const result = await api.PUT('/playlists/{id}', {
params: { path: { playlistId: params.id } }, params: { path: { id: params.id } },
body: { newName: name, newDescription: description, newPictureIntentId, newVisibility } body: {
name,
description,
pictureIntentId: newPictureIntentId,
visibility: newVisibility
}
}); });
const failure = unwrapOrFail(result, 'No se pudo actualizar la playlist.'); const failure = unwrapOrFail(result, 'No se pudo actualizar la playlist.');
if (failure) return failure; if (failure) return failure;
@@ -92,8 +97,8 @@ export const actions: Actions = {
if (typeof accessToken !== 'string') return accessToken; if (typeof accessToken !== 'string') return accessToken;
const api = createApiClient({ fetch, accessToken }); const api = createApiClient({ fetch, accessToken });
const result = await api.DELETE('/playlists/{playlistId}', { const result = await api.DELETE('/playlists/{id}', {
params: { path: { playlistId: params.id } } params: { path: { id: params.id } }
}); });
const failure = unwrapOrFail(result, 'No se pudo eliminar la playlist.'); const failure = unwrapOrFail(result, 'No se pudo eliminar la playlist.');
if (failure) return failure; if (failure) return failure;
@@ -25,8 +25,8 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
const tracksByPlaylist = await Promise.all( const tracksByPlaylist = await Promise.all(
playlistItems.map((playlist) => playlistItems.map((playlist) =>
api api
.GET('/playlists/{playlistId}/tracks', { .GET('/playlists/{id}/tracks', {
params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 1 } } params: { path: { id: playlist.id }, query: { pageNumber: 1, pageSize: 1 } }
}) })
.then((res) => Number(res.data?.totalItemCount ?? 0)) .then((res) => Number(res.data?.totalItemCount ?? 0))
) )
+2 -2
View File
@@ -45,9 +45,9 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
const spotlightPlaylist = playlistItems[0] ?? null; const spotlightPlaylist = playlistItems[0] ?? null;
const spotlightTracksPromise = spotlightPlaylist const spotlightTracksPromise = spotlightPlaylist
? api ? api
.GET('/playlists/{playlistId}/tracks', { .GET('/playlists/{id}/tracks', {
params: { params: {
path: { playlistId: spotlightPlaylist.id }, path: { id: spotlightPlaylist.id },
query: { pageNumber: 1, pageSize: HOME_SPOTLIGHT_TRACKS_LIMIT } query: { pageNumber: 1, pageSize: HOME_SPOTLIGHT_TRACKS_LIMIT }
} }
}) })
@@ -5,9 +5,9 @@ import { ALBUM_TRACKS_PAGE_SIZE } from '$lib/config';
export const GET: RequestHandler = async ({ params, locals, fetch }) => { export const GET: RequestHandler = async ({ params, locals, fetch }) => {
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined }); const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const result = await api.GET('/albums/{albumId}/tracks', { const result = await api.GET('/albums/{id}/tracks', {
params: { params: {
path: { albumId: params.id }, path: { id: params.id },
query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE } query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE }
} }
}); });