Design Prot Merge #1
@@ -0,0 +1,82 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record AddTrackToAlbumCommand(long UserId, Guid AlbumId, Guid TrackId)
|
||||||
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
|
public class AddTrackToAlbumCommandHandler(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<AddTrackToAlbumCommandHandler> logger)
|
||||||
|
: ICommandHandler<AddTrackToAlbumCommand, ErrorOr<Success>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<Success>> Handle(AddTrackToAlbumCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var album = await database.UserAlbums
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
|
||||||
|
if (album is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Album {AlbumId} not found", request.AlbumId);
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (album.OwnerUserId != request.UserId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User {UserId} is not the owner of album {AlbumId}", request.UserId, request.AlbumId);
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var track = await database.LocalTracks
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken);
|
||||||
|
if (track is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Local track {TrackId} not found", request.TrackId);
|
||||||
|
return Error.NotFound(description: "Only tracks you uploaded can be added to an album.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.OwnerUserId != request.UserId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User {UserId} does not own track {TrackId}", request.UserId, request.TrackId);
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var alreadyAdded = await database.AlbumHasTracks
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(albumTrack => albumTrack.AlbumId == request.AlbumId && albumTrack.TrackId == request.TrackId, cancellationToken);
|
||||||
|
if (alreadyAdded)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Track {TrackId} already in album {AlbumId}", request.TrackId, request.AlbumId);
|
||||||
|
return Error.Conflict(description: "Track is already in the album.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var albumTracksQuery = database.AlbumHasTracks
|
||||||
|
.Where(albumTrack => albumTrack.AlbumId == request.AlbumId);
|
||||||
|
|
||||||
|
var hasTracks = await albumTracksQuery.AnyAsync(cancellationToken);
|
||||||
|
var nextTrackNumber = hasTracks
|
||||||
|
? await albumTracksQuery.MaxAsync(albumTrack => albumTrack.TrackNumber, cancellationToken) + 1
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
await database.AlbumHasTracks.AddAsync(new AlbumHasTrack
|
||||||
|
{
|
||||||
|
AlbumId = request.AlbumId,
|
||||||
|
TrackId = request.TrackId,
|
||||||
|
TrackNumber = nextTrackNumber
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Added track {TrackId} to album {AlbumId} as number {TrackNumber}",
|
||||||
|
request.TrackId, request.AlbumId, nextTrackNumber);
|
||||||
|
|
||||||
|
return new Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record CreateAlbumCommand(
|
||||||
|
long UserId,
|
||||||
|
string Title,
|
||||||
|
string? Description,
|
||||||
|
int? ReleaseYear)
|
||||||
|
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
||||||
|
|
||||||
|
public class CreateAlbumCommandHandler(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<CreateAlbumCommandHandler> logger)
|
||||||
|
: ICommandHandler<CreateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(CreateAlbumCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userExists = await database.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(user => user.Id == request.UserId, cancellationToken);
|
||||||
|
if (!userExists)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User {UserId} not found", request.UserId);
|
||||||
|
return Error.NotFound(description: $"User {request.UserId} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = request.Title.Trim();
|
||||||
|
|
||||||
|
var album = new UserAlbum
|
||||||
|
{
|
||||||
|
OwnerUserId = request.UserId,
|
||||||
|
Title = title,
|
||||||
|
NormalizedTitle = title.ToUpperInvariant(),
|
||||||
|
Description = request.Description,
|
||||||
|
ReleaseYear = request.ReleaseYear
|
||||||
|
};
|
||||||
|
|
||||||
|
await database.UserAlbums.AddAsync(album, cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "Failed to save album for user {UserId}", request.UserId);
|
||||||
|
return Error.Failure(description: $"Failed to create album for user {request.UserId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Created album {AlbumId} for user {UserId}", album.Id, request.UserId);
|
||||||
|
|
||||||
|
return AlbumApplicationResponse.FromEntity(album, trackCount: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record DeleteAlbumCommand(long UserId, Guid AlbumId)
|
||||||
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
|
public class DeleteAlbumCommandHandler(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<DeleteAlbumCommandHandler> logger)
|
||||||
|
: ICommandHandler<DeleteAlbumCommand, ErrorOr<Success>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<Success>> Handle(DeleteAlbumCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var album = await database.UserAlbums
|
||||||
|
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
|
||||||
|
if (album is null)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Album {AlbumId} not found", request.AlbumId);
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (album.OwnerUserId != request.UserId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Album {AlbumId} does not belong to user {UserId}", request.AlbumId, request.UserId);
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
database.UserAlbums.Remove(album);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "Failed to delete album {AlbumId}", request.AlbumId);
|
||||||
|
return Error.Failure(description: $"Failed to delete album {request.AlbumId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Deleted album {AlbumId} for user {UserId}", request.AlbumId, request.UserId);
|
||||||
|
|
||||||
|
return new Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record GetAlbumByIdQuery(Guid Id)
|
||||||
|
: IQuery<ErrorOr<AlbumApplicationResponse>>;
|
||||||
|
|
||||||
|
public class GetAlbumByIdQueryHandler(IDatabase database)
|
||||||
|
: IQueryHandler<GetAlbumByIdQuery, ErrorOr<AlbumApplicationResponse>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(GetAlbumByIdQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var album = await database.UserAlbums
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.Id == request.Id)
|
||||||
|
.Select(a => new { Album = a, TrackCount = a.AlbumTracks.Count })
|
||||||
|
.SingleOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (album is null)
|
||||||
|
return Error.NotFound();
|
||||||
|
|
||||||
|
return AlbumApplicationResponse.FromEntity(album.Album, album.TrackCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.Shared;
|
||||||
|
using Musify.Application.Tracks.Responses;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
using X.PagedList;
|
||||||
|
using X.PagedList.EF;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record GetAlbumTracksQuery(
|
||||||
|
Guid AlbumId,
|
||||||
|
int PageNumber,
|
||||||
|
int PageSize)
|
||||||
|
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
|
||||||
|
|
||||||
|
public class GetAlbumTracksQueryHandler(IDatabase database)
|
||||||
|
: IQueryHandler<GetAlbumTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetAlbumTracksQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var albumExists = await database.Albums
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(album => album.Id == request.AlbumId, cancellationToken);
|
||||||
|
if (!albumExists)
|
||||||
|
return Error.NotFound();
|
||||||
|
|
||||||
|
var tracksQuery = database.AlbumHasTracks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(albumTrack => ((ExternalTrack)albumTrack.Track).TrackArtists)
|
||||||
|
.ThenInclude(trackArtist => trackArtist.Artist)
|
||||||
|
.Include(albumTrack => ((LocalTrack)albumTrack.Track).Owner)
|
||||||
|
.Where(albumTrack => albumTrack.AlbumId == request.AlbumId);
|
||||||
|
|
||||||
|
var totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var pagedEntities = await tracksQuery
|
||||||
|
.OrderBy(albumTrack => albumTrack.TrackNumber)
|
||||||
|
.Select(albumTrack => new { albumTrack.Track, ListensCount = albumTrack.Track.ListeningHistories.Count })
|
||||||
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
|
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||||
|
pagedEntities.Select(entry => TrackApplicationResponse.FromEntity(entry.Track, entry.ListensCount)),
|
||||||
|
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
|
||||||
|
|
||||||
|
return PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.Shared;
|
||||||
|
using X.PagedList;
|
||||||
|
using X.PagedList.EF;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record GetAlbumsQuery(string? Title, int PageNumber, int PageSize)
|
||||||
|
: IQuery<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>;
|
||||||
|
|
||||||
|
public class GetAlbumsQueryHandler(IDatabase database)
|
||||||
|
: IQueryHandler<GetAlbumsQuery, ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>> Handle(GetAlbumsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var albumsQuery = database.UserAlbums
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Title))
|
||||||
|
{
|
||||||
|
var normalizedTitle = request.Title.Trim().ToUpperInvariant();
|
||||||
|
albumsQuery = albumsQuery.Where(album => album.NormalizedTitle.Contains(normalizedTitle));
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await albumsQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var pagedEntities = await albumsQuery
|
||||||
|
.OrderByDescending(album => album.CreatedAt)
|
||||||
|
.Select(album => new { Album = album, TrackCount = album.AlbumTracks.Count })
|
||||||
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
|
var pagedAlbums = new StaticPagedList<AlbumApplicationResponse>(
|
||||||
|
pagedEntities.Select(entry => AlbumApplicationResponse.FromEntity(entry.Album, entry.TrackCount)),
|
||||||
|
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
|
||||||
|
|
||||||
|
return PaginatedResponse<AlbumApplicationResponse>.FromPagedList(pagedAlbums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.Shared;
|
||||||
|
using X.PagedList;
|
||||||
|
using X.PagedList.EF;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record GetAlbumsByUserIdQuery(long UserId, int PageNumber, int PageSize)
|
||||||
|
: IQuery<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>;
|
||||||
|
|
||||||
|
public class GetAlbumsByUserIdQueryHandler(IDatabase database)
|
||||||
|
: IQueryHandler<GetAlbumsByUserIdQuery, ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>> Handle(GetAlbumsByUserIdQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userExists = await database.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(user => user.Id == request.UserId, cancellationToken);
|
||||||
|
if (!userExists)
|
||||||
|
return Error.NotFound(description: $"User {request.UserId} not found");
|
||||||
|
|
||||||
|
var albumsQuery = database.UserAlbums
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(album => album.OwnerUserId == request.UserId);
|
||||||
|
|
||||||
|
var totalCount = await albumsQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var pagedEntities = await albumsQuery
|
||||||
|
.OrderByDescending(album => album.CreatedAt)
|
||||||
|
.Select(album => new { Album = album, TrackCount = album.AlbumTracks.Count })
|
||||||
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
|
var pagedAlbums = new StaticPagedList<AlbumApplicationResponse>(
|
||||||
|
pagedEntities.Select(entry => AlbumApplicationResponse.FromEntity(entry.Album, entry.TrackCount)),
|
||||||
|
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
|
||||||
|
|
||||||
|
return PaginatedResponse<AlbumApplicationResponse>.FromPagedList(pagedAlbums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record RemoveTrackFromAlbumCommand(long UserId, Guid AlbumId, Guid TrackId)
|
||||||
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
|
public class RemoveTrackFromAlbumCommandHandler(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<RemoveTrackFromAlbumCommandHandler> logger)
|
||||||
|
: ICommandHandler<RemoveTrackFromAlbumCommand, ErrorOr<Success>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromAlbumCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var album = await database.UserAlbums
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
|
||||||
|
if (album is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Album {AlbumId} not found", request.AlbumId);
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (album.OwnerUserId != request.UserId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("User {UserId} is not the owner of album {AlbumId}", request.UserId, request.AlbumId);
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var albumTracks = await database.AlbumHasTracks
|
||||||
|
.Where(albumTrack => albumTrack.AlbumId == request.AlbumId)
|
||||||
|
.OrderBy(albumTrack => albumTrack.TrackNumber)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var link = albumTracks.SingleOrDefault(albumTrack => albumTrack.TrackId == request.TrackId);
|
||||||
|
if (link is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Track {TrackId} not in album {AlbumId}", request.TrackId, request.AlbumId);
|
||||||
|
return Error.NotFound(description: "Track is not in the album.");
|
||||||
|
}
|
||||||
|
|
||||||
|
database.AlbumHasTracks.Remove(link);
|
||||||
|
|
||||||
|
var trackNumber = 1;
|
||||||
|
foreach (var remaining in albumTracks.Where(albumTrack => albumTrack.Id != link.Id))
|
||||||
|
remaining.TrackNumber = trackNumber++;
|
||||||
|
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Removed track {TrackId} from album {AlbumId}", request.TrackId, request.AlbumId);
|
||||||
|
|
||||||
|
return new Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums.Responses
|
||||||
|
{
|
||||||
|
public record AlbumApplicationResponse(
|
||||||
|
Guid Id,
|
||||||
|
string Title,
|
||||||
|
string? Description,
|
||||||
|
int? ReleaseYear,
|
||||||
|
long OwnerUserId,
|
||||||
|
int TrackCount,
|
||||||
|
string? SmallImageKeyName,
|
||||||
|
string? MediumImageKeyName,
|
||||||
|
string? LargeImageKeyName,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
DateTime UpdatedAt)
|
||||||
|
{
|
||||||
|
public static AlbumApplicationResponse FromEntity(UserAlbum album, int trackCount)
|
||||||
|
{
|
||||||
|
return new AlbumApplicationResponse(
|
||||||
|
album.Id,
|
||||||
|
album.Title,
|
||||||
|
album.Description,
|
||||||
|
album.ReleaseYear,
|
||||||
|
album.OwnerUserId,
|
||||||
|
trackCount,
|
||||||
|
album.Pictures?.SmallName,
|
||||||
|
album.Pictures?.MediumName,
|
||||||
|
album.Pictures?.LargeName,
|
||||||
|
album.CreatedAt,
|
||||||
|
album.UpdatedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
public record UpdateAlbumCommand(
|
||||||
|
long UserId,
|
||||||
|
Guid AlbumId,
|
||||||
|
string Title,
|
||||||
|
string? Description,
|
||||||
|
int? ReleaseYear)
|
||||||
|
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
||||||
|
|
||||||
|
public class UpdateAlbumCommandHandler(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<UpdateAlbumCommandHandler> logger)
|
||||||
|
: ICommandHandler<UpdateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var album = await database.UserAlbums
|
||||||
|
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
|
||||||
|
if (album is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Album {AlbumId} not found", request.AlbumId);
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (album.OwnerUserId != request.UserId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Album {AlbumId} does not belong to user {UserId}", request.AlbumId, request.UserId);
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = request.Title.Trim();
|
||||||
|
|
||||||
|
album.Title = title;
|
||||||
|
album.NormalizedTitle = title.ToUpperInvariant();
|
||||||
|
album.Description = request.Description;
|
||||||
|
album.ReleaseYear = request.ReleaseYear;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "Failed to update album {AlbumId}", request.AlbumId);
|
||||||
|
return Error.Failure(description: $"Failed to update album {request.AlbumId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackCount = await database.AlbumHasTracks
|
||||||
|
.AsNoTracking()
|
||||||
|
.CountAsync(albumTrack => albumTrack.AlbumId == album.Id, cancellationToken);
|
||||||
|
|
||||||
|
return AlbumApplicationResponse.FromEntity(album, trackCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user