Reorganize backend to mirror Musify.slnx folder structure

Moves the six backend projects on disk into Core/ and Hosts/
subfolders, matching the solution folders already defined in
Musify.slnx, so the file explorer reflects the same layout as the
solution:

  backend/Core/Musify.Domain
  backend/Core/Musify.Application
  backend/Core/Musify.Infrastructure
  backend/Hosts/Musify.Api
  backend/Hosts/Musify.Worker
  backend/Hosts/Musify.StreamingGateway

Updates every reference to the old paths: ProjectReference entries in
Musify.Api.csproj and Musify.Worker.csproj, the three Dockerfiles,
deploy/scripts/migrate.ps1|sh, deploy/zitadel/provision.ps1,
deploy/up.ps1, deploy/README.md and docs/development.md. Also fixes a
stale Dockerfile comment pointing at a compose file that no longer
exists.

Verified with `dotnet build Musify.slnx` against the new layout.
This commit is contained in:
2026-09-09 14:27:50 +02:00
parent d867a60126
commit 89569736b7
369 changed files with 41 additions and 41 deletions
@@ -0,0 +1,214 @@
using Mediator;
using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.Albums;
using Musify.Api.Extensions;
using Musify.Api.Filters;
using Musify.Application.Albums;
using Musify.Application.Albums.Responses;
using Musify.Application.Shared;
using Musify.Application.Tracks.Responses;
namespace Musify.Api.Endpoints;
public static class AlbumEndpoints
{
public static IEndpointRouteBuilder MapAlbumEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/albums")
.WithTags("Albums");
group.MapGet("/", GetAlbums)
.WithName("GetAlbums")
.WithSummary("Get Paginated Albums.")
.Produces<PaginatedResponse<AlbumApplicationResponse>>();
group.MapGet("/{id}", GetAlbumById)
.WithName("GetAlbumById")
.WithSummary("Get An Album By Id.")
.Produces<AlbumApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId}", GetAlbumsByUserId)
.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("/{albumId}/tracks", GetAlbumTracks)
.WithName("GetAlbumTracks")
.WithSummary("Get The Tracks Of An Album Ordered By Track Number.")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.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("/{albumId}", UpdateAlbum)
.WithName("UpdateAlbum")
.WithSummary("Update An Album.")
.AddEndpointFilter<ValidationFilter<UpdateAlbumRequest>>()
.RequireAuthorization()
.Produces<AlbumApplicationResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{albumId}", DeleteAlbum)
.WithName("DeleteAlbum")
.WithSummary("Delete An Album.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{albumId}/tracks/{trackId}", AddTrackToAlbum)
.WithName("AddTrackToAlbum")
.WithSummary("Add One Of Your Uploaded Tracks To An Album.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapDelete("/{albumId}/tracks/{trackId}", RemoveTrackFromAlbum)
.WithName("RemoveTrackFromAlbum")
.WithSummary("Remove A Track From An Album.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
return app;
}
private static async Task<IResult> GetAlbums(
IMediator mediator,
CancellationToken cancellationToken,
string? title,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetAlbumsQuery(title, pageNumber, pageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<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,
CancellationToken cancellationToken,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetAlbumsByUserIdQuery(userId, pageNumber, pageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetRecentlyListenedAlbums(
IMediator mediator,
CurrentUser currentUser,
CancellationToken cancellationToken,
int limit = 12)
{
var result = await mediator.Send(new GetRecentlyListenedAlbumsQuery(currentUser.RequiredId, limit), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetAlbumTracks(
IMediator mediator,
Guid albumId,
CancellationToken cancellationToken,
int pageNumber = 1,
int pageSize = 50)
{
var result = await mediator.Send(new GetAlbumTracksQuery(albumId, pageNumber, pageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<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),
cancellationToken);
return result.ToCreatedResult(album => $"/albums/{album.Id}");
}
private static async Task<IResult> UpdateAlbum(
IMediator mediator,
CurrentUser currentUser,
Guid albumId,
UpdateAlbumRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new UpdateAlbumCommand(currentUser.RequiredId, albumId, request.NewTitle, request.NewDescription, request.NewReleaseYear),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> DeleteAlbum(
IMediator mediator,
CurrentUser currentUser,
Guid albumId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new DeleteAlbumCommand(currentUser.RequiredId, albumId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> AddTrackToAlbum(
IMediator mediator,
CurrentUser currentUser,
Guid albumId,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new AddTrackToAlbumCommand(currentUser.RequiredId, albumId, trackId),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> RemoveTrackFromAlbum(
IMediator mediator,
CurrentUser currentUser,
Guid albumId,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RemoveTrackFromAlbumCommand(currentUser.RequiredId, albumId, trackId),
cancellationToken);
return result.ToHttpResult();
}
}
@@ -0,0 +1,51 @@
using Mediator;
using Musify.Api.Authentication;
using Musify.Api.Extensions;
using Musify.Application.Mixes;
using Musify.Application.Mixes.Responses;
namespace Musify.Api.Endpoints;
public static class MixEndpoints
{
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("/{id}", GetMixById)
.WithName("GetMixById")
.WithSummary("Get A Mix With All Of Its Songs.")
.Produces<MixApplicationResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
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 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();
}
}
@@ -0,0 +1,272 @@
using Mediator;
using Musify.Application.PlayLists;
using Musify.Application.PlayLists.Responses;
using Musify.Application.Tracks.Responses;
using Musify.Application.Shared;
using Musify.Application.Contracts;
using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.PlayLists;
using Musify.Api.Extensions;
using Musify.Api.Filters;
namespace Musify.Api.Endpoints;
public static class PlayListEndpoints
{
public static IEndpointRouteBuilder MapPlayListEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/playlists")
.WithTags("PlayLists");
group.MapGet("/", GetPlayLists)
.WithName("GetPlayLists")
.WithSummary("Get Paginated PlayLists.")
.Produces<PaginatedResponse<PlayListApplicationResponse>>();
group.MapGet("/{id}", GetPlayListById)
.WithName("GetPlayListById")
.WithSummary("Get A PlayList By Id.")
.Produces<PlayListApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId}", GetPlayListsByUserId)
.WithName("GetPlayListsByUserId")
.WithSummary("Get Paginated PlayLists For A User.")
.Produces<PaginatedResponse<PlayListApplicationResponse>>();
group.MapGet("/{playlistId}/tracks", GetPlayListTracks)
.WithName("GetPlayListTracks")
.WithSummary("Get Paginated Tracks Of A PlayList.")
.Produces<PaginatedResponse<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/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.MapPut("/{playlistId}", UpdatePlayList)
.WithName("UpdatePlayList")
.WithSummary("Update An Existing PlayList.")
.AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>()
.RequireAuthorization()
.Produces<PlayListApplicationResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapDelete("/{playlistId}", DeletePlayList)
.WithName("DeletePlayList")
.WithSummary("Delete A PlayList.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.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<RequestPlayListPictureUploadRequest>>()
.RequireAuthorization()
.Produces<PlayListPictureUploadResponse>()
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/{playlistId}/tracks/{trackId}", AddTrackToPlayList)
.WithName("AddTrackToPlayList")
.WithSummary("Add A Track To A PlayList.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapPost("/{playlistId}/youtube-tracks", AddYouTubeTrackToPlayList)
.WithName("AddYouTubeTrackToPlayList")
.WithSummary("Add A YouTube Track To A PlayList, Downloading It In The Background If Needed.")
.AddEndpointFilter<ValidationFilter<AddYouTubeTrackRequest>>()
.RequireAuthorization()
.Produces<TrackApplicationResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict);
group.MapDelete("/{playlistId}/tracks/{trackId}", RemoveTrackFromPlayList)
.WithName("RemoveTrackFromPlayList")
.WithSummary("Remove A Track From A PlayList.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
return app;
}
private static async Task<IResult> GetPlayLists(
IMediator mediator,
CancellationToken cancellationToken,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetPlayListsQuery(pageNumber, pageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListById(
IMediator mediator,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetPlayListByIdQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListsByUserId(
IMediator mediator,
long userId,
CancellationToken cancellationToken,
string? name,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetPlayListsByUserIdQuery(userId, name, pageNumber, pageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListTracks(
IMediator mediator,
Guid playlistId,
CancellationToken cancellationToken,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetPlayListTracksQuery(playlistId, pageNumber, pageSize), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetPlayListCover(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
Guid id,
CancellationToken cancellationToken,
string size = "medium")
{
var result = await mediator.Send(new GetPlayListCoverQuery(id, size), cancellationToken);
if (result.IsError)
return Results.NotFound();
var location = result.Value;
var stream = await storageService.GetFileAsync(location.Bucket, location.Key, cancellationToken);
response.Headers.CacheControl = "public, max-age=31536000, immutable";
return Results.Stream(stream, location.ContentType);
}
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),
cancellationToken);
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
}
private static async Task<IResult> UpdatePlayList(
IMediator mediator,
CurrentUser currentUser,
Guid playlistId,
UpdatePlayListRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new UpdatePlayListCommand(currentUser.RequiredId, playlistId, request.NewName, request.NewDescription, request.NewPictureIntentId),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> DeletePlayList(
IMediator mediator,
CurrentUser currentUser,
Guid playlistId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new DeletePlayListCommand(currentUser.RequiredId, playlistId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> RequestPlayListPictureUpload(
IMediator mediator,
CurrentUser currentUser,
RequestPlayListPictureUploadRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RequestPlayListPictureUploadCommand(currentUser.RequiredId, request.FileType, request.ContentType, request.ExpectedSizeBytes),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> AddTrackToPlayList(
IMediator mediator,
CurrentUser currentUser,
Guid playlistId,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new AddTrackToPlayListCommand(currentUser.RequiredId, playlistId, trackId),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> AddYouTubeTrackToPlayList(
IMediator mediator,
CurrentUser currentUser,
Guid playlistId,
AddYouTubeTrackRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new AddYouTubeTrackToPlayListCommand(
currentUser.RequiredId,
playlistId,
request.VideoId),
cancellationToken);
return result.ToCreatedResult(track => $"/tracks/{track.Id}");
}
private static async Task<IResult> RemoveTrackFromPlayList(
IMediator mediator,
CurrentUser currentUser,
Guid playlistId,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new RemoveTrackFromPlayListCommand(currentUser.RequiredId, playlistId, trackId),
cancellationToken);
return result.ToHttpResult();
}
}
@@ -0,0 +1,186 @@
using Mediator;
using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses;
using Musify.Application.Shared;
using Musify.Application.Contracts;
using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.Tracks;
using Musify.Api.Extensions;
using Musify.Api.Filters;
namespace Musify.Api.Endpoints;
public static class TrackEndpoints
{
public static IEndpointRouteBuilder MapTrackEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/tracks")
.WithTags("Tracks");
group.MapGet("/", GetTracks)
.WithName("GetTracks")
.WithSummary("Get Paginated Tracks.")
.Produces<PaginatedResponse<TrackApplicationResponse>>();
group.MapGet("/{id}", GetTrackById)
.WithName("GetTrackById")
.WithSummary("Get A Track By Id.")
.Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/cover", GetTrackCover)
.WithName("GetTrackCover")
.WithSummary("Get A Track Cover Image.")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/stream", GetTrackStream)
.WithName("GetTrackStream")
.WithSummary("Get A Streaming Manifest URL And Ticket For A Track.")
.RequireAuthorization()
.Produces<TrackStreamResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/users/{userId}", GetTracksByUserId)
.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.MapDelete("/{trackId}", DeleteTrack)
.WithName("DeleteTrack")
.WithSummary("Delete A Track.")
.RequireAuthorization()
.Produces(StatusCodes.Status204NoContent)
.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;
}
private static async Task<IResult> GetTracks(
IMediator mediator,
CancellationToken cancellationToken,
string? name,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetTracksQuery(name, pageNumber, pageSize), 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 async Task<IResult> GetTrackCover(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
Guid id,
CancellationToken cancellationToken,
string size = "medium")
{
var result = await mediator.Send(new GetTrackCoverQuery(id, size), cancellationToken);
if (result.IsError)
return Results.NotFound();
var location = result.Value;
var stream = await storageService.GetFileAsync(location.Bucket, location.Key, cancellationToken);
response.Headers.CacheControl = "public, max-age=31536000, immutable";
return Results.Stream(stream, location.ContentType);
}
private static async Task<IResult> GetTrackStream(
IMediator mediator,
CurrentUser currentUser,
Guid id,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetTrackStreamQuery(id, currentUser.RequiredId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetTracksByUserId(
IMediator mediator,
long userId,
CancellationToken cancellationToken,
string? name,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetTracksByUserIdQuery(userId, name, pageNumber, 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),
cancellationToken);
return result.ToCreatedResult(track => $"/tracks/{track.Id}");
}
private static async Task<IResult> DeleteTrack(
IMediator mediator,
CurrentUser currentUser,
Guid trackId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new DeleteTrackCommand(currentUser.RequiredId, trackId), 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);
return result.ToHttpResult();
}
}
@@ -0,0 +1,87 @@
using Mediator;
using Musify.Api.DataTransferObjects.Users;
using Musify.Api.Extensions;
using Musify.Api.Filters;
using Musify.Application.Shared;
using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses;
using Musify.Application.Users;
using Musify.Application.Users.Responses;
namespace Musify.Api.Endpoints;
public static class UserEndpoints
{
public static IEndpointRouteBuilder MapUserEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/users")
.WithTags("Users");
group.MapGet("/", GetUsers)
.WithName("GetUsers")
.WithSummary("Get Paginated Users.")
.Produces<PaginatedResponse<UserApplicationResponse>>();
group.MapGet("/{id}", GetUserById)
.WithName("GetUserById")
.WithSummary("Get A User By Id.")
.Produces<UserApplicationResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/listening-history", GetListeningHistory)
.WithName("GetListeningHistory")
.WithSummary("Get A User'S Listening History.")
.Produces<IEnumerable<TrackApplicationResponse>>()
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/", CreateUser)
.WithName("CreateUser")
.WithSummary("Create A New User.")
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>()
.Produces<UserApplicationResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.Produces(StatusCodes.Status409Conflict);
return app;
}
private static async Task<IResult> GetUsers(
IMediator mediator,
CancellationToken cancellationToken,
string? usernameSearch,
int pageNumber = 1,
int pageSize = 10)
{
var result = await mediator.Send(new GetUsersQuery(pageNumber, pageSize, usernameSearch), 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<IEnumerable<TrackApplicationResponse>> GetListeningHistory(
IMediator mediator,
long id,
CancellationToken cancellationToken)
{
return await mediator.Send(new GetListeningHistoryQuery(id), cancellationToken);
}
private static async Task<IResult> CreateUser(
IMediator mediator,
CreateUserRequest request,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new CreateUserCommand(request.Id, request.Name, request.FirstName, request.SecondName),
cancellationToken);
return result.ToCreatedResult(user => $"/users/{user.Id}");
}
}
@@ -0,0 +1,99 @@
using Mediator;
using Musify.Api.Authentication;
using Musify.Api.Extensions;
using Musify.Application.Contracts;
using Musify.Application.YouTube;
using Musify.Application.YouTube.Responses;
namespace Musify.Api.Endpoints;
public static class YouTubeEndpoints
{
public static IEndpointRouteBuilder MapYouTubeEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/youtube")
.WithTags("YouTube");
group.MapGet("/search", SearchTracks)
.WithName("SearchYouTubeTracks")
.WithSummary("Search Songs On YouTube Music.")
.RequireAuthorization()
.Produces<YouTubeSearchResult>()
.Produces(StatusCodes.Status401Unauthorized)
.ProducesValidationProblem();
group.MapGet("/albums", SearchAlbums)
.WithName("SearchYouTubeAlbums")
.WithSummary("Search Albums On YouTube Music.")
.RequireAuthorization()
.Produces<YouTubeAlbumSearchResult>()
.Produces(StatusCodes.Status401Unauthorized)
.ProducesValidationProblem();
group.MapGet("/albums/{albumId}", GetAlbum)
.WithName("GetYouTubeAlbum")
.WithSummary("Get A YouTube Music Album With Its Tracklist.")
.RequireAuthorization()
.Produces<YouTubeAlbumDetail>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/tracks/{videoId}/stream", GetTrackStream)
.WithName("GetYouTubeTrackStream")
.WithSummary("Resolve Playback For A YouTube Track: Server Stream If Downloaded, Direct YouTube Stream Otherwise.")
.RequireAuthorization()
.Produces<YouTubeStreamResponse>()
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound);
return app;
}
private static async Task<IResult> GetTrackStream(
IMediator mediator,
CurrentUser currentUser,
string videoId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(
new ResolveYouTubeTrackStreamCommand(videoId, currentUser.RequiredId),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> SearchAlbums(
IMediator mediator,
CancellationToken cancellationToken,
string query,
string? continuation = null)
{
var result = await mediator.Send(
new SearchYouTubeAlbumsQuery(query, continuation ?? string.Empty),
cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetAlbum(
IMediator mediator,
string albumId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetYouTubeAlbumQuery(albumId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> SearchTracks(
IMediator mediator,
CancellationToken cancellationToken,
string query,
string? continuation = null)
{
var result = await mediator.Send(
new SearchYouTubeTracksQuery(query, continuation ?? string.Empty),
cancellationToken);
return result.ToHttpResult();
}
}