chore: group .NET into backend/ and drop non-.NET test artifacts
- Move all .NET (6 projects, solution, Directory.*.props, nuget.config) into backend/ so the monorepo root can host non-.NET projects later - Remove throwaway test artifacts: player.html, pl.json - Trim dead Flutter rules from .gitignore Build from backend/Musify.slnx: 0 warnings, 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
|
||||
namespace Musify.Application.PlayLists.Commands
|
||||
{
|
||||
public record AddTrackToPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
|
||||
: ICommand<Result>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Commands
|
||||
{
|
||||
public record CreatePlayListCommand(
|
||||
long UserId,
|
||||
string Name,
|
||||
string Description,
|
||||
Guid? PictureIntentId)
|
||||
: ICommand<Result<PlayListApplicationResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
|
||||
namespace Musify.Application.PlayLists.Commands
|
||||
{
|
||||
public record DeletePlayListCommand(long UserId, Guid PlayListId)
|
||||
: ICommand<Result>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
|
||||
namespace Musify.Application.PlayLists.Commands
|
||||
{
|
||||
public record RemoveTrackFromPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
|
||||
: ICommand<Result>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Commands
|
||||
{
|
||||
public record RequestPlayListPictureUploadCommand(
|
||||
long UserId,
|
||||
string FileType,
|
||||
string ContentType,
|
||||
long? ExpectedSizeBytes = null)
|
||||
: ICommand<Result<PlayListPictureUploadResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Commands
|
||||
{
|
||||
public record UpdatePlayListCommand(
|
||||
long UserId,
|
||||
Guid PlayListId,
|
||||
string? NewName,
|
||||
string? NewDescription,
|
||||
Guid? NewPictureIntentId)
|
||||
: ICommand<Result<PlayListApplicationResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class AddTrackToPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<AddTrackToPlayListCommandHandler> logger)
|
||||
: ICommandHandler<AddTrackToPlayListCommand, Result>
|
||||
{
|
||||
public async ValueTask<Result> Handle(AddTrackToPlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists
|
||||
.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
|
||||
if (playList is null)
|
||||
{
|
||||
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
|
||||
return Result.NotFound();
|
||||
}
|
||||
|
||||
if (playList.UserId != request.UserId)
|
||||
{
|
||||
logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId);
|
||||
return Result.Unauthorized();
|
||||
}
|
||||
|
||||
var trackExists = await database.Tracks
|
||||
.AsNoTracking()
|
||||
.AnyAsync(t => t.Id == request.TrackId, cancellationToken);
|
||||
if (!trackExists)
|
||||
{
|
||||
logger.LogInformation("Track {TrackId} not found", request.TrackId);
|
||||
return Result.NotFound($"Track {request.TrackId} not found");
|
||||
}
|
||||
|
||||
var alreadyAdded = await database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.AnyAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken);
|
||||
if (alreadyAdded)
|
||||
{
|
||||
logger.LogInformation("Track {TrackId} already in playlist {PlayListId}", request.TrackId, request.PlayListId);
|
||||
return Result.Conflict("Track is already in the playlist.");
|
||||
}
|
||||
|
||||
var nextPosition = await database.PlayListHasTracks
|
||||
.Where(plt => plt.PlayListId == request.PlayListId)
|
||||
.Select(plt => (int?)plt.Position)
|
||||
.MaxAsync(cancellationToken) + 1 ?? 0;
|
||||
|
||||
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
|
||||
{
|
||||
PlayListId = request.PlayListId,
|
||||
TrackId = request.TrackId,
|
||||
Position = nextPosition
|
||||
}, cancellationToken);
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Added track {TrackId} to playlist {PlayListId} at position {Position}",
|
||||
request.TrackId, request.PlayListId, nextPosition);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Extensions;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class CreatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<CreatePlayListCommandHandler> logger,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
PlayListConfiguration playListConfiguration,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: ICommandHandler<CreatePlayListCommand, Result<PlayListApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<Result<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (!userExists)
|
||||
{
|
||||
logger.LogWarning("User {UserId} not found", request.UserId);
|
||||
return Result.NotFound($"User {request.UserId} not found");
|
||||
}
|
||||
|
||||
UploadIntent? pictureIntent = null;
|
||||
string originalPictureName;
|
||||
|
||||
if (request.PictureIntentId.HasValue)
|
||||
{
|
||||
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.PictureIntentId.Value, request.UserId, cancellationToken);
|
||||
if (!validation.IsSuccess)
|
||||
return validation.As<UploadIntent, PlayListApplicationResponse>();
|
||||
|
||||
pictureIntent = validation.Value;
|
||||
originalPictureName = pictureIntent.ObjectName;
|
||||
}
|
||||
else
|
||||
{
|
||||
originalPictureName = playListConfiguration.Routes.PresetOriginalPicture;
|
||||
}
|
||||
|
||||
var playList = new PlayList
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Name = request.Name,
|
||||
NormalizedName = request.Name.Trim().ToUpperInvariant(),
|
||||
Description = request.Description,
|
||||
OriginalPictureName = originalPictureName,
|
||||
SmallPictureName = playListConfiguration.Routes.PresetSmallPicture,
|
||||
MediumPictureName = playListConfiguration.Routes.PresetMediumPicture,
|
||||
LargePictureName = playListConfiguration.Routes.PresetLargePicture
|
||||
};
|
||||
|
||||
await database.PlayLists.AddAsync(playList, cancellationToken);
|
||||
|
||||
if (pictureIntent is not null)
|
||||
{
|
||||
var finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName);
|
||||
var publishResult = await PublishCreatePlayListEventAsync(
|
||||
playList.Id, pictureIntent, finalPictureKey, cancellationToken);
|
||||
if (!publishResult.IsSuccess)
|
||||
return publishResult;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to save playlist for user {UserId}", request.UserId);
|
||||
return Result.Error($"Failed to create playlist for user {request.UserId}");
|
||||
}
|
||||
|
||||
logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId);
|
||||
return Result.Created(PlayListApplicationResponse.FromEntity(playList));
|
||||
}
|
||||
|
||||
async Task<Result<PlayListApplicationResponse>> PublishCreatePlayListEventAsync(
|
||||
Guid playListId,
|
||||
UploadIntent pictureIntent,
|
||||
string finalPictureKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new CreatePlayListResourcesEvent(
|
||||
playListId,
|
||||
pictureIntent.Id,
|
||||
storageConfiguration.Bucket,
|
||||
pictureIntent.Key,
|
||||
finalPictureKey,
|
||||
new ImageSize(
|
||||
playListConfiguration.Routes.SmallPicturesPath,
|
||||
playListConfiguration.PicturesSizes.SmallPictureWidth,
|
||||
playListConfiguration.PicturesSizes.SmallPictureHeight),
|
||||
new ImageSize(
|
||||
playListConfiguration.Routes.MediumPicturesPath,
|
||||
playListConfiguration.PicturesSizes.MediumPictureWidth,
|
||||
playListConfiguration.PicturesSizes.MediumPictureHeight),
|
||||
new ImageSize(
|
||||
playListConfiguration.Routes.LargePicturesPath,
|
||||
playListConfiguration.PicturesSizes.LargePictureWidth,
|
||||
playListConfiguration.PicturesSizes.LargePictureHeight)),
|
||||
cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to publish create playlist event for playlist {PlayListId}", playListId);
|
||||
return Result.Error($"Failed to publish create playlist event for playlist {playListId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class DeletePlayListCommandHandler(
|
||||
IDatabase database,
|
||||
IEventBus eventBus,
|
||||
ILogger<DeletePlayListCommandHandler> logger)
|
||||
: ICommandHandler<DeletePlayListCommand, Result>
|
||||
{
|
||||
public async ValueTask<Result> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == request.PlayListId, cancellationToken);
|
||||
if (playList is null)
|
||||
{
|
||||
logger.LogDebug("Playlist {PlayListId} not found", request.PlayListId);
|
||||
return Result.NotFound();
|
||||
}
|
||||
|
||||
if (playList.UserId != request.UserId)
|
||||
{
|
||||
logger.LogWarning("Playlist {PlayListId} does not belong to user {UserId}", request.PlayListId, request.UserId);
|
||||
return Result.Unauthorized();
|
||||
}
|
||||
|
||||
playList.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||
|
||||
try
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new DeletePlayListEvent(playList.Id, request.UserId),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to publish delete playlist event for playlist {PlayListId}", request.PlayListId);
|
||||
return Result.Error($"Failed to delete playlist {request.PlayListId}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to mark playlist {PlayListId} as removing", request.PlayListId);
|
||||
return Result.Error($"Failed to delete playlist {request.PlayListId}");
|
||||
}
|
||||
|
||||
return Result.NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Ardalis.Result;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.PlayLists.Queries;
|
||||
using Mediator;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class GetPlayListByIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListByIdQuery, Result<PlayListApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<Result<PlayListApplicationResponse>> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == request.Id)
|
||||
.Select(p => PlayListApplicationResponse.FromEntity(p))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
return playList is null
|
||||
? Result<PlayListApplicationResponse>.NotFound()
|
||||
: Result.Success(playList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.PlayLists.Queries;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class GetPlayListTracksQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListTracksQuery, Result<PaginatedResponse<TrackApplicationResponse>> >
|
||||
{
|
||||
public async ValueTask<Result<PaginatedResponse<TrackApplicationResponse> >> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playListExists = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.AnyAsync(p => p.Id == request.PlayListId, cancellationToken);
|
||||
if (!playListExists)
|
||||
return Result<PaginatedResponse<TrackApplicationResponse>>.NotFound();
|
||||
|
||||
var tracksQuery = database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.Include(plt => plt.Track)
|
||||
.Where(plt => plt.PlayListId == request.PlayListId);
|
||||
|
||||
var totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||
|
||||
var pagedTracks = await tracksQuery
|
||||
.OrderBy(plt => plt.Position)
|
||||
.Select(plt => TrackApplicationResponse.FromEntity(plt.Track))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return Result.Success(PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Ardalis.Result;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.PlayLists.Queries;
|
||||
using X.PagedList.EF;
|
||||
using Mediator;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class GetPlayListsByUserIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListsByUserIdQuery, Result<PaginatedResponse<PlayListApplicationResponse> > >
|
||||
{
|
||||
public async ValueTask<Result<PaginatedResponse<PlayListApplicationResponse> >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playListsQuery = database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Where(p => p.UserId == request.UserId);
|
||||
|
||||
if (request.Name is not null)
|
||||
{
|
||||
var normalizedName = request.Name.Trim().ToUpperInvariant();
|
||||
playListsQuery = playListsQuery.Where(p => p.NormalizedName.Contains(normalizedName));
|
||||
}
|
||||
|
||||
var totalCount = await playListsQuery.CountAsync(cancellationToken);
|
||||
|
||||
var pagedPlayLists = await playListsQuery
|
||||
.OrderBy(p => p.CreatedAt)
|
||||
.Select(p => PlayListApplicationResponse.FromEntity(p))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return Result.Success(PaginatedResponse<PlayListApplicationResponse>.FromPagedList(pagedPlayLists));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Ardalis.Result;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.PlayLists.Queries;
|
||||
using X.PagedList.EF;
|
||||
using Mediator;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class GetPlayListsQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListsQuery, Result<PaginatedResponse<PlayListApplicationResponse> >>
|
||||
{
|
||||
public async ValueTask<Result<PaginatedResponse<PlayListApplicationResponse> >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var totalCount = await database.PlayLists.CountAsync(cancellationToken);
|
||||
|
||||
var pagedPlayLists = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.OrderBy(p => p.Id)
|
||||
.Select(p => PlayListApplicationResponse.FromEntity(p))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return Result.Success(PaginatedResponse<PlayListApplicationResponse>.FromPagedList(pagedPlayLists));
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class RemoveTrackFromPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<RemoveTrackFromPlayListCommandHandler> logger)
|
||||
: ICommandHandler<RemoveTrackFromPlayListCommand, Result>
|
||||
{
|
||||
public async ValueTask<Result> Handle(RemoveTrackFromPlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists
|
||||
.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
|
||||
if (playList is null)
|
||||
{
|
||||
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
|
||||
return Result.NotFound();
|
||||
}
|
||||
|
||||
if (playList.UserId != request.UserId)
|
||||
{
|
||||
logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId);
|
||||
return Result.Unauthorized();
|
||||
}
|
||||
|
||||
var link = await database.PlayListHasTracks
|
||||
.SingleOrDefaultAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken);
|
||||
if (link is null)
|
||||
{
|
||||
logger.LogInformation("Track {TrackId} not in playlist {PlayListId}", request.TrackId, request.PlayListId);
|
||||
return Result.NotFound("Track is not in the playlist.");
|
||||
}
|
||||
|
||||
database.PlayListHasTracks.Remove(link);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Removed track {TrackId} from playlist {PlayListId}", request.TrackId, request.PlayListId);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class RequestPlayListPictureUploadCommandHandler(
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
IStorageService storageService,
|
||||
ILogger<RequestPlayListPictureUploadCommandHandler> logger,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
PlayListConfiguration playListConfiguration,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: ICommandHandler<RequestPlayListPictureUploadCommand, Result<PlayListPictureUploadResponse>>
|
||||
{
|
||||
public async ValueTask<Result<PlayListPictureUploadResponse>> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (!userExists)
|
||||
{
|
||||
logger.LogWarning("User {UserId} not found", request.UserId);
|
||||
return Result.NotFound($"User {request.UserId} not found");
|
||||
}
|
||||
|
||||
var effectiveSizeBytes = request.ExpectedSizeBytes
|
||||
?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes;
|
||||
|
||||
var objectName = $"{Guid.NewGuid() }.{request.FileType.TrimStart('.').ToLowerInvariant() }";
|
||||
|
||||
var tempKey = playListConfiguration.Routes.BuildTempPicturePath(
|
||||
uploadIntentConfiguration.TemporalRootPrefix, request.UserId, objectName);
|
||||
|
||||
try
|
||||
{
|
||||
var uploadUrl = await storageService.GetUploadUrlAsync(
|
||||
storageConfiguration.Bucket,
|
||||
tempKey,
|
||||
request.ContentType,
|
||||
TimeSpan.FromSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds),
|
||||
cancellationToken);
|
||||
|
||||
await using var transaction = await database.BeginTransactionAsync(
|
||||
System.Data.IsolationLevel.Serializable, cancellationToken);
|
||||
|
||||
var quotaCheck = await uploadIntentValidator.CheckQuotaAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.UserId, effectiveSizeBytes, 1, cancellationToken);
|
||||
if (!quotaCheck.IsSuccess)
|
||||
{
|
||||
logger.LogWarning("User {UserId} failed upload intent quota check", request.UserId);
|
||||
return quotaCheck;
|
||||
}
|
||||
|
||||
var intent = new UploadIntent
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Bucket = storageConfiguration.Bucket,
|
||||
Key = tempKey,
|
||||
ObjectName = objectName,
|
||||
ContentType = request.ContentType,
|
||||
ExpectedSizeBytes = effectiveSizeBytes,
|
||||
Purpose = UploadIntentPurpose.PlayListPicture,
|
||||
ExpiresAt = DateTime.UtcNow.AddSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds),
|
||||
};
|
||||
|
||||
await database.UploadIntents.AddAsync(intent, cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (PlayListPicture, temp key: {Key})", intent.Id, request.UserId, tempKey);
|
||||
|
||||
return Result.Success(new PlayListPictureUploadResponse(
|
||||
intent.Id,
|
||||
storageConfiguration.Bucket,
|
||||
tempKey,
|
||||
objectName,
|
||||
request.ContentType,
|
||||
uploadIntentConfiguration.UploadUrlExpiresInSeconds,
|
||||
uploadUrl));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to generate playlist picture upload URL for user {UserId}", request.UserId);
|
||||
return Result.Error("Failed to generate upload URL");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Extensions;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.PlayLists.Handlers
|
||||
{
|
||||
public class UpdatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<UpdatePlayListCommandHandler> logger,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
PlayListConfiguration playListConfiguration,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: ICommandHandler<UpdatePlayListCommand, Result<PlayListApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<Result<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
|
||||
if (playListEntity is null)
|
||||
{
|
||||
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
|
||||
return Result.NotFound();
|
||||
}
|
||||
|
||||
if (playListEntity.UserId != request.UserId)
|
||||
{
|
||||
logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId);
|
||||
return Result.Unauthorized();
|
||||
}
|
||||
|
||||
if (request.NewName is not null)
|
||||
{
|
||||
playListEntity.Name = request.NewName;
|
||||
playListEntity.NormalizedName = request.NewName.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
if (request.NewDescription is not null)
|
||||
{
|
||||
playListEntity.Description = request.NewDescription;
|
||||
}
|
||||
|
||||
UploadIntent? pictureIntent = null;
|
||||
string? finalPictureKey = null;
|
||||
|
||||
if (request.NewPictureIntentId.HasValue)
|
||||
{
|
||||
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.NewPictureIntentId.Value, request.UserId, cancellationToken);
|
||||
if (!validation.IsSuccess)
|
||||
return validation.As<UploadIntent, PlayListApplicationResponse>();
|
||||
|
||||
pictureIntent = validation.Value;
|
||||
playListEntity.OriginalPictureName = pictureIntent.ObjectName;
|
||||
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||
}
|
||||
|
||||
database.PlayLists.Update(playListEntity);
|
||||
|
||||
if (pictureIntent is not null && finalPictureKey is not null)
|
||||
{
|
||||
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
||||
playListEntity.Id, pictureIntent, finalPictureKey, cancellationToken);
|
||||
if (!publishResult.IsSuccess)
|
||||
return publishResult;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to update playlist {PlayListId}", playListEntity.Id);
|
||||
return Result.Error($"Failed to update playlist {playListEntity.Id}");
|
||||
}
|
||||
|
||||
logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id);
|
||||
return Result.Success(PlayListApplicationResponse.FromEntity(playListEntity));
|
||||
}
|
||||
|
||||
async Task<Result<PlayListApplicationResponse>> PublishUpdatePlayListPictureSourceEventAsync(
|
||||
Guid playListId,
|
||||
UploadIntent pictureIntent,
|
||||
string finalPictureKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new UpdatePlayListPictureSourceEvent(
|
||||
playListId,
|
||||
pictureIntent.Id,
|
||||
storageConfiguration.Bucket,
|
||||
pictureIntent.Key,
|
||||
finalPictureKey,
|
||||
new ImageSize(
|
||||
playListConfiguration.Routes.SmallPicturesPath,
|
||||
playListConfiguration.PicturesSizes.SmallPictureWidth,
|
||||
playListConfiguration.PicturesSizes.SmallPictureHeight),
|
||||
new ImageSize(
|
||||
playListConfiguration.Routes.MediumPicturesPath,
|
||||
playListConfiguration.PicturesSizes.MediumPictureWidth,
|
||||
playListConfiguration.PicturesSizes.MediumPictureHeight),
|
||||
new ImageSize(
|
||||
playListConfiguration.Routes.LargePicturesPath,
|
||||
playListConfiguration.PicturesSizes.LargePictureWidth,
|
||||
playListConfiguration.PicturesSizes.LargePictureHeight)),
|
||||
cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to publish update playlist picture source event for playlist {PlayListId}", playListId);
|
||||
return Result.Error($"Failed to publish update playlist picture source event for playlist {playListId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Queries
|
||||
{
|
||||
public record GetPlayListByIdQuery(Guid Id)
|
||||
: IQuery<Result<PlayListApplicationResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Queries
|
||||
{
|
||||
public record GetPlayListTracksQuery(
|
||||
Guid PlayListId,
|
||||
int PageNumber = 1,
|
||||
int PageSize = 10)
|
||||
: IQuery<Result<PaginatedResponse<TrackApplicationResponse>>>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Queries
|
||||
{
|
||||
public record GetPlayListsByUserIdQuery(
|
||||
long UserId,
|
||||
string? Name,
|
||||
int PageNumber = 1,
|
||||
int PageSize = 10)
|
||||
: IQuery<Result<PaginatedResponse<PlayListApplicationResponse>> >;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
|
||||
namespace Musify.Application.PlayLists.Queries
|
||||
{
|
||||
public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10)
|
||||
: IQuery<Result<PaginatedResponse<PlayListApplicationResponse>> >;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.PlayLists.Responses
|
||||
{
|
||||
public record PlayListApplicationResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Description,
|
||||
string SmallImageKeyName,
|
||||
string MediumImageKeyName,
|
||||
string LargeImageKeyName,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt)
|
||||
{
|
||||
public static PlayListApplicationResponse FromEntity(PlayList playList)
|
||||
{
|
||||
return new PlayListApplicationResponse(
|
||||
playList.Id,
|
||||
playList.Name,
|
||||
playList.Description,
|
||||
playList.SmallPictureName,
|
||||
playList.MediumPictureName,
|
||||
playList.LargePictureName,
|
||||
playList.CreatedAt,
|
||||
playList.UpdatedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Musify.Application.PlayLists.Responses
|
||||
{
|
||||
public record PlayListPictureUploadResponse(
|
||||
Guid IntentId,
|
||||
string Bucket,
|
||||
string Key,
|
||||
string PictureName,
|
||||
string ContentType,
|
||||
int ExpiresInSeconds,
|
||||
string UploadUrl);
|
||||
}
|
||||
Reference in New Issue
Block a user