Separate responsibilities: start-listening command, API-side JSON converters, bulk track endpoints, mix kinds
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record AddTrackToPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class AddTrackToPlayListCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<AddTrackToPlayListCommandHandler> logger)
|
||||
: ICommandHandler<AddTrackToPlayListCommand, ErrorOr<Success>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(AddTrackToPlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||
if (playList.IsError)
|
||||
return playList.Errors;
|
||||
|
||||
var trackExists = await database.Tracks
|
||||
.AsNoTracking()
|
||||
.AnyAsync(t => t.Id == request.TrackId && t.LifeCycleStatus == LifeCycleStatus.Active, cancellationToken);
|
||||
if (!trackExists)
|
||||
return AppErrors.NotFound("Track", request.TrackId);
|
||||
|
||||
var alreadyAdded = await database.PlayListHasTracks
|
||||
.AsNoTracking()
|
||||
.AnyAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken);
|
||||
if (alreadyAdded)
|
||||
return AlreadyInPlayList;
|
||||
|
||||
// Playlist positions are internal indexes, so they start at 0.
|
||||
var nextPosition = (await database.PlayListHasTracks
|
||||
.Where(plt => plt.PlayListId == request.PlayListId)
|
||||
.MaxAsync(plt => (int?)plt.Position, cancellationToken) ?? -1) + 1;
|
||||
|
||||
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
|
||||
{
|
||||
PlayListId = request.PlayListId,
|
||||
TrackId = request.TrackId,
|
||||
Position = nextPosition
|
||||
}, cancellationToken);
|
||||
|
||||
var saved = await database.TrySaveChangesAsync(AlreadyInPlayList, cancellationToken);
|
||||
if (saved.IsError)
|
||||
return saved;
|
||||
|
||||
logger.LogInformation("Added track {TrackId} to playlist {PlayListId} at position {Position}",
|
||||
request.TrackId, request.PlayListId, nextPosition);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Error AlreadyInPlayList => AppErrors.Conflict("PlayList.TrackAlreadyAdded", "Track is already in the playlist.");
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
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>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToPlayListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Musify.Application.Serialization;
|
||||
|
||||
namespace Musify.Application.PlayLists.Responses;
|
||||
|
||||
@@ -17,7 +15,7 @@ public record PlayListApplicationResponse(
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt,
|
||||
IReadOnlyList<Guid> CoverTrackIds,
|
||||
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
|
||||
long OwnerUserId,
|
||||
int TrackCount,
|
||||
double DurationSeconds)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user