Change editor rule
This commit is contained in:
@@ -7,67 +7,68 @@ using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
/// <summary>Appends tracks to a playlist in the given order, skipping the ones it already has.</summary>
|
||||
public record AddTracksToPlayListCommand(long UserId, Guid PlayListId, IReadOnlyList<Guid> TrackIds)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class AddTracksToPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<AddTracksToPlayListCommandHandler> logger)
|
||||
: ICommandHandler<AddTracksToPlayListCommand, ErrorOr<Success>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToPlayListCommand request, CancellationToken cancellationToken)
|
||||
/// <summary>Appends tracks to a playlist in the given order, skipping the ones it already has.</summary>
|
||||
public record AddTracksToPlayListCommand(long UserId, Guid PlayListId, IReadOnlyList<Guid> TrackIds)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class AddTracksToPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<AddTracksToPlayListCommandHandler> logger)
|
||||
: ICommandHandler<AddTracksToPlayListCommand, ErrorOr<Success>>
|
||||
{
|
||||
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (playList.IsError)
|
||||
return playList.Errors;
|
||||
|
||||
var requestedIds = request.TrackIds.Distinct().ToList();
|
||||
|
||||
var existingIds = await database.Tracks
|
||||
.AsNoTracking()
|
||||
.Where(track => requestedIds.Contains(track.Id) && track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Select(track => track.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var missing = requestedIds.FirstOrDefault(id => !existingIds.Contains(id));
|
||||
if (missing != Guid.Empty)
|
||||
return AppErrors.NotFound("Track", missing);
|
||||
|
||||
var alreadyInPlayList = await database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.Where(link => link.PlayListId == request.PlayListId && requestedIds.Contains(link.TrackId))
|
||||
.Select(link => link.TrackId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var toAdd = requestedIds.Where(id => !alreadyInPlayList.Contains(id)).ToList();
|
||||
if (toAdd.Count == 0)
|
||||
return Result.Success;
|
||||
|
||||
// Playlist positions are internal indexes, so they start at 0.
|
||||
var nextPosition = (await database.PlayListHasTracks
|
||||
.Where(link => link.PlayListId == request.PlayListId)
|
||||
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
|
||||
|
||||
foreach (var trackId in toAdd)
|
||||
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToPlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
|
||||
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (playList.IsError)
|
||||
return playList.Errors;
|
||||
|
||||
var requestedIds = request.TrackIds.Distinct().ToList();
|
||||
|
||||
var existingIds = await database.Tracks
|
||||
.AsNoTracking()
|
||||
.Where(track => requestedIds.Contains(track.Id) && track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Select(track => track.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var missing = requestedIds.FirstOrDefault(id => !existingIds.Contains(id));
|
||||
if (missing != Guid.Empty)
|
||||
return AppErrors.NotFound("Track", missing);
|
||||
|
||||
var alreadyInPlayList = await database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.Where(link => link.PlayListId == request.PlayListId && requestedIds.Contains(link.TrackId))
|
||||
.Select(link => link.TrackId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var toAdd = requestedIds.Where(id => !alreadyInPlayList.Contains(id)).ToList();
|
||||
if (toAdd.Count == 0)
|
||||
return Result.Success;
|
||||
|
||||
// Playlist positions are internal indexes, so they start at 0.
|
||||
var nextPosition = (await database.PlayListHasTracks
|
||||
.Where(link => link.PlayListId == request.PlayListId)
|
||||
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
|
||||
|
||||
foreach (var trackId in toAdd)
|
||||
{
|
||||
PlayListId = request.PlayListId,
|
||||
TrackId = trackId,
|
||||
Position = nextPosition++
|
||||
}, cancellationToken);
|
||||
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
|
||||
{
|
||||
PlayListId = request.PlayListId,
|
||||
TrackId = trackId,
|
||||
Position = nextPosition++
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
var saved = await database.TrySaveChangesAsync(
|
||||
AppErrors.Conflict("PlayList.ConcurrentChange", "The playlist changed while adding tracks. Try again."), cancellationToken);
|
||||
if (saved.IsError)
|
||||
return saved;
|
||||
|
||||
logger.LogInformation("Added {Count} tracks to playlist {PlayListId}", toAdd.Count, request.PlayListId);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var saved = await database.TrySaveChangesAsync(
|
||||
AppErrors.Conflict("PlayList.ConcurrentChange", "The playlist changed while adding tracks. Try again."), cancellationToken);
|
||||
if (saved.IsError)
|
||||
return saved;
|
||||
|
||||
logger.LogInformation("Added {Count} tracks to playlist {PlayListId}", toAdd.Count, request.PlayListId);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,67 +12,68 @@ using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record CreatePlayListCommand(
|
||||
long UserId,
|
||||
string Name,
|
||||
string? Description,
|
||||
Guid? PictureIntentId,
|
||||
PlayListVisibility Visibility = PlayListVisibility.Private)
|
||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
public class CreatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<CreatePlayListCommandHandler> logger,
|
||||
PlayListConfiguration playListConfiguration)
|
||||
: ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
|
||||
public record CreatePlayListCommand(
|
||||
long UserId,
|
||||
string Name,
|
||||
string? Description,
|
||||
Guid? PictureIntentId,
|
||||
PlayListVisibility Visibility = PlayListVisibility.Private)
|
||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
public class CreatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<CreatePlayListCommandHandler> logger,
|
||||
PlayListConfiguration playListConfiguration)
|
||||
: ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||
{
|
||||
var userExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (!userExists)
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
PreparedPicture? picture = null;
|
||||
if (request.PictureIntentId is { } intentId)
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var prepared = await PictureSourceChange.PrepareAsync(
|
||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
||||
if (prepared.IsError)
|
||||
return prepared.Errors;
|
||||
var userExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (!userExists)
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
picture = prepared.Value;
|
||||
PreparedPicture? picture = null;
|
||||
if (request.PictureIntentId is { } intentId)
|
||||
{
|
||||
var prepared = await PictureSourceChange.PrepareAsync(
|
||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
||||
if (prepared.IsError)
|
||||
return prepared.Errors;
|
||||
|
||||
picture = prepared.Value;
|
||||
}
|
||||
|
||||
var name = request.Name.Trim();
|
||||
var playList = new PlayList
|
||||
{
|
||||
OwnerUserId = request.UserId,
|
||||
Name = name,
|
||||
NormalizedName = TextNormalizer.Normalize(name),
|
||||
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
|
||||
Pictures = picture?.Pictures,
|
||||
Visibility = request.Visibility
|
||||
};
|
||||
await database.PlayLists.AddAsync(playList, cancellationToken);
|
||||
|
||||
if (picture != null)
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new CreatePlayListResourcesEvent(
|
||||
playList.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId);
|
||||
|
||||
return PlayListApplicationResponse.FromEntity(playList);
|
||||
}
|
||||
|
||||
var name = request.Name.Trim();
|
||||
var playList = new PlayList
|
||||
{
|
||||
OwnerUserId = request.UserId,
|
||||
Name = name,
|
||||
NormalizedName = TextNormalizer.Normalize(name),
|
||||
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
|
||||
Pictures = picture?.Pictures,
|
||||
Visibility = request.Visibility
|
||||
};
|
||||
await database.PlayLists.AddAsync(playList, cancellationToken);
|
||||
|
||||
if (picture != null)
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new CreatePlayListResourcesEvent(
|
||||
playList.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId);
|
||||
|
||||
return PlayListApplicationResponse.FromEntity(playList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,31 +6,32 @@ using Musify.Application.Events;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record DeletePlayListCommand(long UserId, Guid PlayListId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class DeletePlayListCommandHandler(
|
||||
IDatabase database,
|
||||
IEventBus eventBus,
|
||||
ILogger<DeletePlayListCommandHandler> logger)
|
||||
: ICommandHandler<DeletePlayListCommand, ErrorOr<Success>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
|
||||
public record DeletePlayListCommand(long UserId, Guid PlayListId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class DeletePlayListCommandHandler(
|
||||
IDatabase database,
|
||||
IEventBus eventBus,
|
||||
ILogger<DeletePlayListCommandHandler> logger)
|
||||
: ICommandHandler<DeletePlayListCommand, ErrorOr<Success>>
|
||||
{
|
||||
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (found.IsError)
|
||||
return found.Errors;
|
||||
public async ValueTask<ErrorOr<Success>> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (found.IsError)
|
||||
return found.Errors;
|
||||
|
||||
var playList = found.Value;
|
||||
playList.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||
var playList = found.Value;
|
||||
playList.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||
|
||||
await eventBus.PublishAsync(new DeletePlayListEvent(playList.Id, request.UserId), cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
await eventBus.PublishAsync(new DeletePlayListEvent(playList.Id, request.UserId), cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Marked playlist {PlayListId} as removing", request.PlayListId);
|
||||
logger.LogInformation("Marked playlist {PlayListId} as removing", request.PlayListId);
|
||||
|
||||
return Result.Success;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,27 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Shared;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record GetPlayListByIdQuery(Guid Id, long? RequestingUserId = null)
|
||||
: IQuery<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
public class GetPlayListByIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListByIdQuery, ErrorOr<PlayListApplicationResponse>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Active()
|
||||
.VisibleTo(request.RequestingUserId)
|
||||
.Where(p => p.Id == request.Id)
|
||||
.SelectResponse()
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
public record GetPlayListByIdQuery(Guid Id, long? RequestingUserId = null)
|
||||
: IQuery<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
return playList == null
|
||||
? AppErrors.NotFound("PlayList", request.Id)
|
||||
: playList;
|
||||
public class GetPlayListByIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListByIdQuery, ErrorOr<PlayListApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Active()
|
||||
.VisibleTo(request.RequestingUserId)
|
||||
.Where(p => p.Id == request.Id)
|
||||
.SelectResponse()
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
return playList == null
|
||||
? AppErrors.NotFound("PlayList", request.Id)
|
||||
: playList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,35 +7,36 @@ using Musify.Application.Shared;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record GetPlayListTracksQuery(
|
||||
Guid PlayListId,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
long? ViewerId = null)
|
||||
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
|
||||
|
||||
public class GetPlayListTracksQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playListVisible = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Active()
|
||||
.VisibleTo(request.ViewerId)
|
||||
.AnyAsync(playList => playList.Id == request.PlayListId, cancellationToken);
|
||||
if (!playListVisible)
|
||||
return AppErrors.NotFound("PlayList", request.PlayListId);
|
||||
public record GetPlayListTracksQuery(
|
||||
Guid PlayListId,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
long? ViewerId = null)
|
||||
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
|
||||
|
||||
return await database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.Where(playListTrack => playListTrack.PlayListId == request.PlayListId && playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.OrderBy(playListTrack => playListTrack.Position)
|
||||
.ThenBy(playListTrack => playListTrack.Id)
|
||||
.Select(playListTrack => playListTrack.Track)
|
||||
.SelectResponse()
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
public class GetPlayListTracksQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playListVisible = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Active()
|
||||
.VisibleTo(request.ViewerId)
|
||||
.AnyAsync(playList => playList.Id == request.PlayListId, cancellationToken);
|
||||
if (!playListVisible)
|
||||
return AppErrors.NotFound("PlayList", request.PlayListId);
|
||||
|
||||
return await database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.Where(playListTrack => playListTrack.PlayListId == request.PlayListId && playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.OrderBy(playListTrack => playListTrack.Position)
|
||||
.ThenBy(playListTrack => playListTrack.Id)
|
||||
.Select(playListTrack => playListTrack.Track)
|
||||
.SelectResponse()
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,38 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Shared;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record GetPlayListsByUserIdQuery(
|
||||
long UserId,
|
||||
string? Name,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
long? ViewerId = null)
|
||||
: IQuery<PaginatedResponse<PlayListApplicationResponse>>;
|
||||
|
||||
public class GetPlayListsByUserIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListsByUserIdQuery, PaginatedResponse<PlayListApplicationResponse>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<PaginatedResponse<PlayListApplicationResponse>> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
|
||||
public record GetPlayListsByUserIdQuery(
|
||||
long UserId,
|
||||
string? Name,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
long? ViewerId = null)
|
||||
: IQuery<PaginatedResponse<PlayListApplicationResponse>>;
|
||||
|
||||
public class GetPlayListsByUserIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListsByUserIdQuery, PaginatedResponse<PlayListApplicationResponse>>
|
||||
{
|
||||
var playListsQuery = database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Active()
|
||||
.VisibleTo(request.ViewerId)
|
||||
.Where(playList => playList.OwnerUserId == request.UserId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Name))
|
||||
public async ValueTask<PaginatedResponse<PlayListApplicationResponse>> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedName = TextNormalizer.Normalize(request.Name);
|
||||
playListsQuery = playListsQuery.Where(playList => playList.NormalizedName.Contains(normalizedName));
|
||||
}
|
||||
var playListsQuery = database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Active()
|
||||
.VisibleTo(request.ViewerId)
|
||||
.Where(playList => playList.OwnerUserId == request.UserId);
|
||||
|
||||
return await playListsQuery
|
||||
.OrderBy(playList => playList.CreatedAt)
|
||||
.ThenBy(playList => playList.Id)
|
||||
.SelectResponse()
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
if (!string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
var normalizedName = TextNormalizer.Normalize(request.Name);
|
||||
playListsQuery = playListsQuery.Where(playList => playList.NormalizedName.Contains(normalizedName));
|
||||
}
|
||||
|
||||
return await playListsQuery
|
||||
.OrderBy(playList => playList.CreatedAt)
|
||||
.ThenBy(playList => playList.Id)
|
||||
.SelectResponse()
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,41 +5,42 @@ using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record RemoveTrackFromPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class RemoveTrackFromPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<RemoveTrackFromPlayListCommandHandler> logger)
|
||||
: ICommandHandler<RemoveTrackFromPlayListCommand, ErrorOr<Success>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromPlayListCommand request, CancellationToken cancellationToken)
|
||||
public record RemoveTrackFromPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class RemoveTrackFromPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<RemoveTrackFromPlayListCommandHandler> logger)
|
||||
: ICommandHandler<RemoveTrackFromPlayListCommand, ErrorOr<Success>>
|
||||
{
|
||||
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (playList.IsError)
|
||||
return playList.Errors;
|
||||
public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromPlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (playList.IsError)
|
||||
return playList.Errors;
|
||||
|
||||
var playListTracks = await database.PlayListHasTracks
|
||||
.Where(plt => plt.PlayListId == request.PlayListId)
|
||||
.OrderBy(plt => plt.Position)
|
||||
.ToListAsync(cancellationToken);
|
||||
var playListTracks = await database.PlayListHasTracks
|
||||
.Where(plt => plt.PlayListId == request.PlayListId)
|
||||
.OrderBy(plt => plt.Position)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var link = playListTracks.SingleOrDefault(plt => plt.TrackId == request.TrackId);
|
||||
if (link == null)
|
||||
return Error.NotFound("PlayList.TrackNotFound", "Track is not in the playlist.");
|
||||
var link = playListTracks.SingleOrDefault(plt => plt.TrackId == request.TrackId);
|
||||
if (link == null)
|
||||
return Error.NotFound("PlayList.TrackNotFound", "Track is not in the playlist.");
|
||||
|
||||
database.PlayListHasTracks.Remove(link);
|
||||
database.PlayListHasTracks.Remove(link);
|
||||
|
||||
var position = 0;
|
||||
foreach (var remaining in playListTracks.Where(plt => plt.Id != link.Id))
|
||||
remaining.Position = position++;
|
||||
var position = 0;
|
||||
foreach (var remaining in playListTracks.Where(plt => plt.Id != link.Id))
|
||||
remaining.Position = position++;
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Removed track {TrackId} from playlist {PlayListId}", request.TrackId, request.PlayListId);
|
||||
logger.LogInformation("Removed track {TrackId} from playlist {PlayListId}", request.TrackId, request.PlayListId);
|
||||
|
||||
return Result.Success;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,21 @@ using Musify.Application.Pictures.Responses;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record RequestPlayListPictureUploadCommand(
|
||||
long UserId,
|
||||
string FileType,
|
||||
string ContentType,
|
||||
long? ExpectedSizeBytes = null)
|
||||
: ICommand<ErrorOr<PictureUploadResponse>>;
|
||||
|
||||
public class RequestPlayListPictureUploadCommandHandler(UploadIntentIssuer issuer, PlayListConfiguration playListConfiguration)
|
||||
: ICommandHandler<RequestPlayListPictureUploadCommand, ErrorOr<PictureUploadResponse>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<PictureUploadResponse>> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) =>
|
||||
await issuer.IssuePictureAsync(
|
||||
request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration,
|
||||
request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken);
|
||||
public record RequestPlayListPictureUploadCommand(
|
||||
long UserId,
|
||||
string FileType,
|
||||
string ContentType,
|
||||
long? ExpectedSizeBytes = null)
|
||||
: ICommand<ErrorOr<PictureUploadResponse>>;
|
||||
|
||||
public class RequestPlayListPictureUploadCommandHandler(UploadIntentIssuer issuer, PlayListConfiguration playListConfiguration)
|
||||
: ICommandHandler<RequestPlayListPictureUploadCommand, ErrorOr<PictureUploadResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PictureUploadResponse>> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) =>
|
||||
await issuer.IssuePictureAsync(
|
||||
request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration,
|
||||
request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
+38
-37
@@ -2,44 +2,45 @@ using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
|
||||
namespace Musify.Application.PlayLists.Responses;
|
||||
|
||||
public record PlayListApplicationResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
string? SmallImageKeyName,
|
||||
string? MediumImageKeyName,
|
||||
string? LargeImageKeyName,
|
||||
PlayListVisibility Visibility,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt,
|
||||
IReadOnlyList<Guid> CoverTrackIds,
|
||||
long OwnerUserId,
|
||||
int TrackCount,
|
||||
double DurationSeconds)
|
||||
namespace Musify.Application.PlayLists.Responses
|
||||
{
|
||||
public const int CoverTrackCount = PlayListProjections.CoverTrackCount;
|
||||
|
||||
public static PlayListApplicationResponse FromEntity(
|
||||
PlayList playList,
|
||||
IReadOnlyList<Guid>? coverTrackIds = null,
|
||||
int trackCount = 0,
|
||||
double durationSeconds = 0)
|
||||
public record PlayListApplicationResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
string? SmallImageKeyName,
|
||||
string? MediumImageKeyName,
|
||||
string? LargeImageKeyName,
|
||||
PlayListVisibility Visibility,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt,
|
||||
IReadOnlyList<Guid> CoverTrackIds,
|
||||
long OwnerUserId,
|
||||
int TrackCount,
|
||||
double DurationSeconds)
|
||||
{
|
||||
return new PlayListApplicationResponse(
|
||||
playList.Id,
|
||||
playList.Name,
|
||||
playList.Description,
|
||||
playList.Pictures?.SmallName,
|
||||
playList.Pictures?.MediumName,
|
||||
playList.Pictures?.LargeName,
|
||||
playList.Visibility,
|
||||
playList.CreatedAt,
|
||||
playList.UpdatedAt,
|
||||
coverTrackIds ?? [],
|
||||
playList.OwnerUserId,
|
||||
trackCount,
|
||||
durationSeconds);
|
||||
public const int CoverTrackCount = PlayListProjections.CoverTrackCount;
|
||||
|
||||
public static PlayListApplicationResponse FromEntity(
|
||||
PlayList playList,
|
||||
IReadOnlyList<Guid>? coverTrackIds = null,
|
||||
int trackCount = 0,
|
||||
double durationSeconds = 0)
|
||||
{
|
||||
return new PlayListApplicationResponse(
|
||||
playList.Id,
|
||||
playList.Name,
|
||||
playList.Description,
|
||||
playList.Pictures?.SmallName,
|
||||
playList.Pictures?.MediumName,
|
||||
playList.Pictures?.LargeName,
|
||||
playList.Visibility,
|
||||
playList.CreatedAt,
|
||||
playList.UpdatedAt,
|
||||
coverTrackIds ?? [],
|
||||
playList.OwnerUserId,
|
||||
trackCount,
|
||||
durationSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists.Responses;
|
||||
|
||||
public static class PlayListProjections
|
||||
namespace Musify.Application.PlayLists.Responses
|
||||
{
|
||||
public const int CoverTrackCount = 4;
|
||||
public static class PlayListProjections
|
||||
{
|
||||
public const int CoverTrackCount = 4;
|
||||
|
||||
public static IQueryable<PlayList> Active(this IQueryable<PlayList> query) =>
|
||||
query.Where(playList => playList.LifeCycleStatus == LifeCycleStatus.Active);
|
||||
public static IQueryable<PlayList> Active(this IQueryable<PlayList> query) =>
|
||||
query.Where(playList => playList.LifeCycleStatus == LifeCycleStatus.Active);
|
||||
|
||||
public static IQueryable<PlayList> VisibleTo(this IQueryable<PlayList> query, long? viewerId) =>
|
||||
query.Where(playList => playList.Visibility == PlayListVisibility.Public || playList.OwnerUserId == viewerId);
|
||||
public static IQueryable<PlayList> VisibleTo(this IQueryable<PlayList> query, long? viewerId) =>
|
||||
query.Where(playList => playList.Visibility == PlayListVisibility.Public || playList.OwnerUserId == viewerId);
|
||||
|
||||
public static IQueryable<PlayListApplicationResponse> SelectResponse(this IQueryable<PlayList> query) =>
|
||||
query.Select(playList => new PlayListApplicationResponse(
|
||||
playList.Id,
|
||||
playList.Name,
|
||||
playList.Description,
|
||||
playList.Pictures!.SmallName,
|
||||
playList.Pictures!.MediumName,
|
||||
playList.Pictures!.LargeName,
|
||||
playList.Visibility,
|
||||
playList.CreatedAt,
|
||||
playList.UpdatedAt,
|
||||
playList.PlayListTracks
|
||||
.OrderBy(playListTrack => playListTrack.Position)
|
||||
.Take(CoverTrackCount)
|
||||
.Select(playListTrack => playListTrack.TrackId)
|
||||
.ToList(),
|
||||
playList.OwnerUserId,
|
||||
playList.PlayListTracks.Count(playListTrack => playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active),
|
||||
playList.PlayListTracks
|
||||
.Where(playListTrack => playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Sum(playListTrack => playListTrack.Track.DurationSeconds)));
|
||||
public static IQueryable<PlayListApplicationResponse> SelectResponse(this IQueryable<PlayList> query) =>
|
||||
query.Select(playList => new PlayListApplicationResponse(
|
||||
playList.Id,
|
||||
playList.Name,
|
||||
playList.Description,
|
||||
playList.Pictures!.SmallName,
|
||||
playList.Pictures!.MediumName,
|
||||
playList.Pictures!.LargeName,
|
||||
playList.Visibility,
|
||||
playList.CreatedAt,
|
||||
playList.UpdatedAt,
|
||||
playList.PlayListTracks
|
||||
.OrderBy(playListTrack => playListTrack.Position)
|
||||
.Take(CoverTrackCount)
|
||||
.Select(playListTrack => playListTrack.TrackId)
|
||||
.ToList(),
|
||||
playList.OwnerUserId,
|
||||
playList.PlayListTracks.Count(playListTrack => playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active),
|
||||
playList.PlayListTracks
|
||||
.Where(playListTrack => playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Sum(playListTrack => playListTrack.Track.DurationSeconds)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,70 +11,71 @@ using Musify.Application.Services;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record UpdatePlayListCommand(
|
||||
long UserId,
|
||||
Guid PlayListId,
|
||||
string? NewName,
|
||||
string? NewDescription,
|
||||
Guid? NewPictureIntentId,
|
||||
PlayListVisibility? NewVisibility = null)
|
||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
public class UpdatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<UpdatePlayListCommandHandler> logger,
|
||||
PlayListConfiguration playListConfiguration)
|
||||
: ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
|
||||
public record UpdatePlayListCommand(
|
||||
long UserId,
|
||||
Guid PlayListId,
|
||||
string? NewName,
|
||||
string? NewDescription,
|
||||
Guid? NewPictureIntentId,
|
||||
PlayListVisibility? NewVisibility = null)
|
||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
public class UpdatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<UpdatePlayListCommandHandler> logger,
|
||||
PlayListConfiguration playListConfiguration)
|
||||
: ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||
{
|
||||
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (found.IsError)
|
||||
return found.Errors;
|
||||
|
||||
var playList = found.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.NewName))
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
playList.Name = request.NewName.Trim();
|
||||
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
||||
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (found.IsError)
|
||||
return found.Errors;
|
||||
|
||||
var playList = found.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.NewName))
|
||||
{
|
||||
playList.Name = request.NewName.Trim();
|
||||
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
||||
}
|
||||
|
||||
// null means "unchanged"; a blank description clears it.
|
||||
if (request.NewDescription != null)
|
||||
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
||||
|
||||
if (request.NewVisibility is { } visibility)
|
||||
playList.Visibility = visibility;
|
||||
|
||||
if (request.NewPictureIntentId is { } intentId)
|
||||
{
|
||||
var prepared = await PictureSourceChange.PrepareAsync(
|
||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
||||
if (prepared.IsError)
|
||||
return prepared.Errors;
|
||||
|
||||
var picture = prepared.Value;
|
||||
playList.Pictures = picture.Pictures;
|
||||
|
||||
await eventBus.PublishAsync(
|
||||
new UpdatePlayListPictureSourceEvent(
|
||||
playList.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Updated playlist {PlayListId}", playList.Id);
|
||||
|
||||
return await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == playList.Id)
|
||||
.SelectResponse()
|
||||
.SingleAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// null means "unchanged"; a blank description clears it.
|
||||
if (request.NewDescription != null)
|
||||
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
||||
|
||||
if (request.NewVisibility is { } visibility)
|
||||
playList.Visibility = visibility;
|
||||
|
||||
if (request.NewPictureIntentId is { } intentId)
|
||||
{
|
||||
var prepared = await PictureSourceChange.PrepareAsync(
|
||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
||||
if (prepared.IsError)
|
||||
return prepared.Errors;
|
||||
|
||||
var picture = prepared.Value;
|
||||
playList.Pictures = picture.Pictures;
|
||||
|
||||
await eventBus.PublishAsync(
|
||||
new UpdatePlayListPictureSourceEvent(
|
||||
playList.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Updated playlist {PlayListId}", playList.Id);
|
||||
|
||||
return await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == playList.Id)
|
||||
.SelectResponse()
|
||||
.SingleAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user