Changes
This commit is contained in:
@@ -9,7 +9,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.Albums
|
namespace Musify.Application.Albums
|
||||||
{
|
{
|
||||||
/// <summary>Appends your own tracks to an album in the given order, skipping the ones it already has.</summary>
|
|
||||||
public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList<Guid> TrackIds)
|
public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList<Guid> TrackIds)
|
||||||
: ICommand<ErrorOr<Success>>;
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
@@ -50,7 +49,6 @@ namespace Musify.Application.Albums
|
|||||||
if (toAdd.Count == 0)
|
if (toAdd.Count == 0)
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|
||||||
// Album track numbers are user-visible, so they start at 1.
|
|
||||||
var nextTrackNumber = (await database.AlbumHasTracks
|
var nextTrackNumber = (await database.AlbumHasTracks
|
||||||
.Where(link => link.AlbumId == request.AlbumId)
|
.Where(link => link.AlbumId == request.AlbumId)
|
||||||
.MaxAsync(link => (int?)link.TrackNumber, cancellationToken) ?? 0) + 1;
|
.MaxAsync(link => (int?)link.TrackNumber, cancellationToken) ?? 0) + 1;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ namespace Musify.Application.Albums
|
|||||||
var album = found.Value;
|
var album = found.Value;
|
||||||
var title = request.Title.Trim();
|
var title = request.Title.Trim();
|
||||||
|
|
||||||
// This is a full replace of the plain fields; the picture has its own command.
|
|
||||||
album.Title = title;
|
album.Title = title;
|
||||||
album.NormalizedTitle = TextNormalizer.Normalize(title);
|
album.NormalizedTitle = TextNormalizer.Normalize(title);
|
||||||
album.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
|
album.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.Albums
|
namespace Musify.Application.Albums
|
||||||
{
|
{
|
||||||
/// <summary>Replaces an album's picture with a previously uploaded image; the plain fields are untouched.</summary>
|
|
||||||
public record UpdateAlbumPictureCommand(long UserId, Guid AlbumId, Guid PictureIntentId)
|
public record UpdateAlbumPictureCommand(long UserId, Guid AlbumId, Guid PictureIntentId)
|
||||||
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ namespace Musify.Application.Configuration
|
|||||||
|
|
||||||
public static class PictureSizeParser
|
public static class PictureSizeParser
|
||||||
{
|
{
|
||||||
/// <summary>Parses the <c>size</c> query value; anything unknown means <see cref="PictureSize.Medium"/>.</summary>
|
|
||||||
public static PictureSize Parse(string? value) =>
|
public static PictureSize Parse(string? value) =>
|
||||||
Enum.TryParse<PictureSize>(value, ignoreCase: true, out var size) && Enum.IsDefined(size)
|
Enum.TryParse<PictureSize>(value, ignoreCase: true, out var size) && Enum.IsDefined(size)
|
||||||
? size
|
? size
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ namespace Musify.Application.Contracts
|
|||||||
{
|
{
|
||||||
public interface IStorageService
|
public interface IStorageService
|
||||||
{
|
{
|
||||||
/// <summary>Opens an object for reading, or returns null when it does not exist.</summary>
|
|
||||||
public Task<Stream?> GetFileAsync(string bucket, string key, CancellationToken cancellationToken);
|
public Task<Stream?> GetFileAsync(string bucket, string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
public Task<ObjectMetaData?> HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken);
|
public Task<ObjectMetaData?> HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ namespace Musify.Application.Likes
|
|||||||
nowLiked = true;
|
nowLiked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A concurrent double click can hit the unique index; the outcome is already in place, so treat it as a no-op.
|
|
||||||
var saved = await database.TrySaveChangesAsync(
|
var saved = await database.TrySaveChangesAsync(
|
||||||
AppErrors.Conflict("TrackLike.Duplicate", "Track is already liked."), cancellationToken);
|
AppErrors.Conflict("TrackLike.Duplicate", "Track is already liked."), cancellationToken);
|
||||||
if (saved.IsError)
|
if (saved.IsError)
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ namespace Musify.Application.Pictures
|
|||||||
_ => source.Medium
|
_ => source.Medium
|
||||||
};
|
};
|
||||||
|
|
||||||
// The resized files only exist once processing has finished; until then serve the original.
|
|
||||||
var (name, key) = !string.IsNullOrEmpty(resizedName)
|
var (name, key) = !string.IsNullOrEmpty(resizedName)
|
||||||
? (resizedName, routes.BuildPicturePath(request.Size, resizedName))
|
? (resizedName, routes.BuildPicturePath(request.Size, resizedName))
|
||||||
: !string.IsNullOrEmpty(source.Original)
|
: !string.IsNullOrEmpty(source.Original)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.Pictures
|
namespace Musify.Application.Pictures
|
||||||
{
|
{
|
||||||
/// <summary>What an album or playlist needs to switch to a freshly uploaded picture.</summary>
|
|
||||||
public sealed record PreparedPicture(UploadIntent Intent, EntityPictures Pictures, string FinalKey, ImageSizes Sizes);
|
public sealed record PreparedPicture(UploadIntent Intent, EntityPictures Pictures, string FinalKey, ImageSizes Sizes);
|
||||||
|
|
||||||
public static class PictureSourceChange
|
public static class PictureSourceChange
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.PlayLists
|
namespace Musify.Application.PlayLists
|
||||||
{
|
{
|
||||||
/// <summary>Appends every track of an album to a playlist in album order, skipping the ones it already has.</summary>
|
|
||||||
public record AddAlbumToPlayListCommand(long UserId, Guid PlayListId, Guid AlbumId)
|
public record AddAlbumToPlayListCommand(long UserId, Guid PlayListId, Guid AlbumId)
|
||||||
: ICommand<ErrorOr<AddAlbumToPlayListResponse>>;
|
: ICommand<ErrorOr<AddAlbumToPlayListResponse>>;
|
||||||
|
|
||||||
@@ -54,7 +53,6 @@ namespace Musify.Application.PlayLists
|
|||||||
if (newTrackIds.Count == 0)
|
if (newTrackIds.Count == 0)
|
||||||
return new AddAlbumToPlayListResponse(0);
|
return new AddAlbumToPlayListResponse(0);
|
||||||
|
|
||||||
// Playlist positions are internal indexes, so they start at 0.
|
|
||||||
var nextPosition = existingTrackIds.Count > 0
|
var nextPosition = existingTrackIds.Count > 0
|
||||||
? await playListTracksQuery.MaxAsync(link => link.Position, cancellationToken) + 1
|
? await playListTracksQuery.MaxAsync(link => link.Position, cancellationToken) + 1
|
||||||
: 0;
|
: 0;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.PlayLists
|
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)
|
public record AddTracksToPlayListCommand(long UserId, Guid PlayListId, IReadOnlyList<Guid> TrackIds)
|
||||||
: ICommand<ErrorOr<Success>>;
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
@@ -46,7 +45,6 @@ namespace Musify.Application.PlayLists
|
|||||||
if (toAdd.Count == 0)
|
if (toAdd.Count == 0)
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|
||||||
// Playlist positions are internal indexes, so they start at 0.
|
|
||||||
var playListTracksQuery = database.PlayListHasTracks
|
var playListTracksQuery = database.PlayListHasTracks
|
||||||
.Where(link => link.PlayListId == request.PlayListId);
|
.Where(link => link.PlayListId == request.PlayListId);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ namespace Musify.Application.PlayLists
|
|||||||
|
|
||||||
var playList = found.Value;
|
var playList = found.Value;
|
||||||
|
|
||||||
// This is a full replace of the plain fields; the picture has its own command.
|
|
||||||
playList.Name = request.NewName.Trim();
|
playList.Name = request.NewName.Trim();
|
||||||
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
||||||
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.PlayLists
|
namespace Musify.Application.PlayLists
|
||||||
{
|
{
|
||||||
/// <summary>Replaces a playlist's picture with a previously uploaded image; the plain fields are untouched.</summary>
|
|
||||||
public record UpdatePlayListPictureCommand(long UserId, Guid PlayListId, Guid PictureIntentId)
|
public record UpdatePlayListPictureCommand(long UserId, Guid PlayListId, Guid PictureIntentId)
|
||||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using Musify.Application.Tracks.Responses;
|
|||||||
|
|
||||||
namespace Musify.Application.Services
|
namespace Musify.Application.Services
|
||||||
{
|
{
|
||||||
/// <summary>Builds the stream URL and signed ticket for a processed track. It never touches the database.</summary>
|
|
||||||
public sealed class TrackStreamIssuer(
|
public sealed class TrackStreamIssuer(
|
||||||
IStreamTicketService ticketService,
|
IStreamTicketService ticketService,
|
||||||
TrackConfiguration trackConfiguration,
|
TrackConfiguration trackConfiguration,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ namespace Musify.Application.Services
|
|||||||
string ContentType,
|
string ContentType,
|
||||||
string UploadUrl);
|
string UploadUrl);
|
||||||
|
|
||||||
/// <summary>Issues pre-signed upload URLs and the upload intents that track them, checking the user quota first.</summary>
|
|
||||||
public sealed class UploadIntentIssuer(
|
public sealed class UploadIntentIssuer(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ namespace Musify.Application.Shared
|
|||||||
{
|
{
|
||||||
public static class OwnershipExtensions
|
public static class OwnershipExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Loads an active, tracked entity owned by <paramref name="userId"/>: NotFound when it is missing
|
|
||||||
/// or not active, Forbidden when someone else owns it.
|
|
||||||
/// </summary>
|
|
||||||
public static async Task<ErrorOr<T>> FindOwnedAsync<T>(
|
public static async Task<ErrorOr<T>> FindOwnedAsync<T>(
|
||||||
this DbSet<T> set, Guid id, long userId, CancellationToken cancellationToken)
|
this DbSet<T> set, Guid id, long userId, CancellationToken cancellationToken)
|
||||||
where T : class, IOwnedEntity, IHasLifeCycle
|
where T : class, IOwnedEntity, IHasLifeCycle
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ namespace Musify.Application.Shared
|
|||||||
|
|
||||||
public static class PaginationExtensions
|
public static class PaginationExtensions
|
||||||
{
|
{
|
||||||
/// <summary>Counts and pages the same (already filtered, ordered and projected) query.</summary>
|
|
||||||
public static async Task<PaginatedResponse<T>> ToPaginatedAsync<T>(
|
public static async Task<PaginatedResponse<T>> ToPaginatedAsync<T>(
|
||||||
this IQueryable<T> query, PageRequest page, CancellationToken cancellationToken)
|
this IQueryable<T> query, PageRequest page, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.Tracks
|
namespace Musify.Application.Tracks
|
||||||
{
|
{
|
||||||
/// <summary>Starts a listen: records it for signed-in users and issues the stream ticket.</summary>
|
|
||||||
public record StartListeningCommand(Guid TrackId, long? UserId)
|
public record StartListeningCommand(Guid TrackId, long? UserId)
|
||||||
: ICommand<ErrorOr<TrackStreamResponse>>;
|
: ICommand<ErrorOr<TrackStreamResponse>>;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Musify.Application.Contracts;
|
|||||||
|
|
||||||
namespace Musify.Application.UploadIntents
|
namespace Musify.Application.UploadIntents
|
||||||
{
|
{
|
||||||
/// <summary>Deletes temporary upload objects that nobody claimed within the retention window.</summary>
|
|
||||||
public record DeleteStaleTempUploadsCommand : ICommand<int>;
|
public record DeleteStaleTempUploadsCommand : ICommand<int>;
|
||||||
|
|
||||||
public class DeleteStaleTempUploadsCommandHandler(
|
public class DeleteStaleTempUploadsCommandHandler(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Application.UploadIntents
|
namespace Musify.Application.UploadIntents
|
||||||
{
|
{
|
||||||
/// <summary>Marks overdue upload intents as expired and deletes the ones expired for longer than the retention window.</summary>
|
|
||||||
public record ExpireUploadIntentsCommand : ICommand<int>;
|
public record ExpireUploadIntentsCommand : ICommand<int>;
|
||||||
|
|
||||||
public class ExpireUploadIntentsCommandHandler(
|
public class ExpireUploadIntentsCommandHandler(
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ namespace Musify.Application.Users
|
|||||||
FollowedId = request.FollowedId
|
FollowedId = request.FollowedId
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
// A concurrent double click can hit the unique index; already following is a successful no-op.
|
|
||||||
var saved = await database.TrySaveChangesAsync(
|
var saved = await database.TrySaveChangesAsync(
|
||||||
AppErrors.Conflict("UserFollow.Duplicate", "Already following this user."), cancellationToken);
|
AppErrors.Conflict("UserFollow.Duplicate", "Already following this user."), cancellationToken);
|
||||||
if (saved.IsError)
|
if (saved.IsError)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Musify.Domain.ValueObjects
|
namespace Musify.Domain.ValueObjects
|
||||||
{
|
{
|
||||||
/// <summary>The picture file names of an album or playlist. Resized names stay null until processing finishes.</summary>
|
|
||||||
public class EntityPictures
|
public class EntityPictures
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ namespace Musify.Domain.ValueObjects
|
|||||||
{
|
{
|
||||||
public enum Genre
|
public enum Genre
|
||||||
{
|
{
|
||||||
/// <summary>Unset value (the enum default); it is not a real genre and is never stored or offered to clients.</summary>
|
|
||||||
None = 0,
|
None = 0,
|
||||||
Pop = 1,
|
Pop = 1,
|
||||||
Rock = 2,
|
Rock = 2,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using Musify.Application.Configuration;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Jobs
|
namespace Musify.Infrastructure.Jobs
|
||||||
{
|
{
|
||||||
/// <summary>Registers every recurring Hangfire job when the host starts.</summary>
|
|
||||||
public sealed class RecurringJobsRegistrar(
|
public sealed class RecurringJobsRegistrar(
|
||||||
IRecurringJobManager recurringJobManager,
|
IRecurringJobManager recurringJobManager,
|
||||||
UploadIntentConfiguration uploadIntentConfiguration)
|
UploadIntentConfiguration uploadIntentConfiguration)
|
||||||
@@ -30,7 +29,6 @@ namespace Musify.Infrastructure.Jobs
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <summary>Hangfire cron has a one-minute resolution, so the interval is rounded up to whole minutes.</summary>
|
|
||||||
internal static string EveryCron(int intervalSeconds)
|
internal static string EveryCron(int intervalSeconds)
|
||||||
{
|
{
|
||||||
var minutes = Math.Max(1, (int)Math.Ceiling(intervalSeconds / 60.0));
|
var minutes = Math.Max(1, (int)Math.Ceiling(intervalSeconds / 60.0));
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.Activities.Audio
|
namespace Musify.Infrastructure.MassTransit.Activities.Audio
|
||||||
{
|
{
|
||||||
/// <summary>Shared bookkeeping of the audio processing activities.</summary>
|
|
||||||
internal static class TrackAudioStatus
|
internal static class TrackAudioStatus
|
||||||
{
|
{
|
||||||
public static async Task<Track> LoadAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken) =>
|
public static async Task<Track> LoadAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken) =>
|
||||||
|
|||||||
-4
@@ -8,7 +8,6 @@ using Musify.Infrastructure.MassTransit.Arguments;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.Activities.LifeCycle
|
namespace Musify.Infrastructure.MassTransit.Activities.LifeCycle
|
||||||
{
|
{
|
||||||
/// <summary>Sets the life-cycle status of an entity (removing, failed, ...). A missing entity is skipped.</summary>
|
|
||||||
internal abstract class MarkLifeCycleActivity<TEntity>(IDatabase database, ILogger logger)
|
internal abstract class MarkLifeCycleActivity<TEntity>(IDatabase database, ILogger logger)
|
||||||
: IExecuteActivity<MarkLifeCycleArguments>
|
: IExecuteActivity<MarkLifeCycleArguments>
|
||||||
where TEntity : class, IHasLifeCycle
|
where TEntity : class, IHasLifeCycle
|
||||||
@@ -33,7 +32,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.LifeCycle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Deletes an entity row. A missing entity is skipped.</summary>
|
|
||||||
internal abstract class DeleteEntityActivity<TEntity>(IDatabase database, ILogger logger)
|
internal abstract class DeleteEntityActivity<TEntity>(IDatabase database, ILogger logger)
|
||||||
: IExecuteActivity<DeleteEntityArguments>
|
: IExecuteActivity<DeleteEntityArguments>
|
||||||
where TEntity : class, IHasLifeCycle
|
where TEntity : class, IHasLifeCycle
|
||||||
@@ -58,8 +56,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.LifeCycle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One concrete activity per entity keeps a separate queue for each of them:
|
|
||||||
// the endpoint name is the kebab-cased class name.
|
|
||||||
internal sealed class MarkTrackLifeCycleActivity(IDatabase database, ILogger<MarkTrackLifeCycleActivity> logger)
|
internal sealed class MarkTrackLifeCycleActivity(IDatabase database, ILogger<MarkTrackLifeCycleActivity> logger)
|
||||||
: MarkLifeCycleActivity<Track>(database, logger)
|
: MarkLifeCycleActivity<Track>(database, logger)
|
||||||
{
|
{
|
||||||
|
|||||||
-2
@@ -12,7 +12,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
|
|||||||
{
|
{
|
||||||
internal sealed record PictureNames(string? Original, string? Small, string? Medium, string? Large);
|
internal sealed record PictureNames(string? Original, string? Small, string? Medium, string? Large);
|
||||||
|
|
||||||
/// <summary>Stores the processed picture names on the owner and reports it; compensation restores the previous names.</summary>
|
|
||||||
internal abstract class UpdatePicturesActivity<TEntity>(
|
internal abstract class UpdatePicturesActivity<TEntity>(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
IPublishEndpoint publishEndpoint,
|
IPublishEndpoint publishEndpoint,
|
||||||
@@ -24,7 +23,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
|
|||||||
|
|
||||||
protected abstract Task<TEntity?> FindAsync(Guid id, CancellationToken cancellationToken);
|
protected abstract Task<TEntity?> FindAsync(Guid id, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>Reads the current names; throws when the entity has no pending picture.</summary>
|
|
||||||
protected abstract PictureNames Read(TEntity entity);
|
protected abstract PictureNames Read(TEntity entity);
|
||||||
|
|
||||||
protected abstract void Write(TEntity entity, PictureNames names, bool succeeded);
|
protected abstract void Write(TEntity entity, PictureNames names, bool succeeded);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ namespace Musify.Infrastructure.MassTransit.Consumers
|
|||||||
{
|
{
|
||||||
internal static class ProcessingFailedCleanup
|
internal static class ProcessingFailedCleanup
|
||||||
{
|
{
|
||||||
/// <summary>Removes whatever files were left behind and marks the entity as failed.</summary>
|
|
||||||
public static Task ExecuteAsync(
|
public static Task ExecuteAsync(
|
||||||
IBus bus,
|
IBus bus,
|
||||||
Guid subjectId,
|
Guid subjectId,
|
||||||
|
|||||||
-2
@@ -8,7 +8,6 @@ using Musify.Infrastructure.MassTransit.Arguments;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
||||||
{
|
{
|
||||||
/// <summary>Marks an entity as removing, deletes its files from the bucket and finally deletes its row.</summary>
|
|
||||||
public class DeleteRoutingSlipBuilder(
|
public class DeleteRoutingSlipBuilder(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
TrackConfiguration trackConfiguration,
|
TrackConfiguration trackConfiguration,
|
||||||
@@ -48,7 +47,6 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
|||||||
routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName));
|
routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unprocessed tracks still point at the shared preset pictures, which must never be deleted.
|
|
||||||
if (track.Pictures.IsProcessed)
|
if (track.Pictures.IsProcessed)
|
||||||
{
|
{
|
||||||
builder.AddRemoveFile(ActivityNames.RemoveTrackSmallPicture, bucket, routes.BuildPicturePath(PictureSize.Small, track.Pictures.SmallName));
|
builder.AddRemoveFile(ActivityNames.RemoveTrackSmallPicture, bucket, routes.BuildPicturePath(PictureSize.Small, track.Pictures.SmallName));
|
||||||
|
|||||||
-4
@@ -8,10 +8,6 @@ using Musify.Infrastructure.MassTransit.Arguments;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Moves an uploaded album/playlist picture to its final key, consumes the upload intent and hands over to the picture workflow.
|
|
||||||
/// Creation faults mark the whole entity as failed; update faults only report the picture as failed.
|
|
||||||
/// </summary>
|
|
||||||
public class PictureSourceRoutingSlipBuilder
|
public class PictureSourceRoutingSlipBuilder
|
||||||
{
|
{
|
||||||
public RoutingSlipBuilder Build(CreateAlbumResourcesEvent message, Guid? correlationId) =>
|
public RoutingSlipBuilder Build(CreateAlbumResourcesEvent message, Guid? correlationId) =>
|
||||||
|
|||||||
-1
@@ -8,7 +8,6 @@ using Musify.Infrastructure.MassTransit.Arguments;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
||||||
{
|
{
|
||||||
/// <summary>Downloads a picture, resizes it to the three sizes, uploads them and records the result on the owner.</summary>
|
|
||||||
public class PictureWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
|
public class PictureWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
|
||||||
{
|
{
|
||||||
public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) =>
|
public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) =>
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ using Musify.Infrastructure.MassTransit.Consumers;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.RoutingSlip
|
namespace Musify.Infrastructure.MassTransit.RoutingSlip
|
||||||
{
|
{
|
||||||
/// <summary>Shared building blocks for every routing slip in the worker.</summary>
|
|
||||||
internal static class RoutingSlips
|
internal static class RoutingSlips
|
||||||
{
|
{
|
||||||
public static Uri ExecuteUri(string endpointName) => new($"queue:{endpointName}_execute");
|
public static Uri ExecuteUri(string endpointName) => new($"queue:{endpointName}_execute");
|
||||||
|
|
||||||
public static Uri ConsumerUri(string queueName) => new($"queue:{queueName}");
|
public static Uri ConsumerUri(string queueName) => new($"queue:{queueName}");
|
||||||
|
|
||||||
/// <summary>A new slip that carries the correlation id and always cleans its temporary directory up.</summary>
|
|
||||||
public static RoutingSlipBuilder Create(Guid? correlationId)
|
public static RoutingSlipBuilder Create(Guid? correlationId)
|
||||||
{
|
{
|
||||||
var builder = new RoutingSlipBuilder(NewId.NextGuid());
|
var builder = new RoutingSlipBuilder(NewId.NextGuid());
|
||||||
@@ -32,7 +30,6 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Publishes a failure event for the subject when the slip faults.</summary>
|
|
||||||
public static RoutingSlipBuilder TrackFaults(
|
public static RoutingSlipBuilder TrackFaults(
|
||||||
this RoutingSlipBuilder builder, Guid subjectId, string processKind)
|
this RoutingSlipBuilder builder, Guid subjectId, string processKind)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ using Musify.Infrastructure.MassTransit.Sagas;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence
|
namespace Musify.Infrastructure.Persistence
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Entity model shared by <see cref="Database"/> and the in-memory test database, so both always agree.
|
|
||||||
/// </summary>
|
|
||||||
public static class ModelConfiguration
|
public static class ModelConfiguration
|
||||||
{
|
{
|
||||||
public static void Apply(ModelBuilder modelBuilder)
|
public static void Apply(ModelBuilder modelBuilder)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Api.Models
|
namespace Musify.Api.Models
|
||||||
{
|
{
|
||||||
/// <summary>Query-string binding for a genre name (case-insensitive); unknown names are rejected with a 400.</summary>
|
|
||||||
public readonly record struct GenreParameter(Genre Value)
|
public readonly record struct GenreParameter(Genre Value)
|
||||||
{
|
{
|
||||||
public static bool TryParse(string? value, out GenreParameter result)
|
public static bool TryParse(string? value, out GenreParameter result)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Musify.Application.Shared;
|
|
||||||
|
|
||||||
namespace Musify.Api.Models
|
namespace Musify.Api.Models
|
||||||
{
|
{
|
||||||
/// <summary>Query-string binding for <see cref="PageRequest"/>, keeping the camelCase parameter names of the API.</summary>
|
|
||||||
public record PageQuery(
|
public record PageQuery(
|
||||||
[property: FromQuery(Name = "pageNumber")] int PageNumber = 1,
|
[property: FromQuery(Name = "pageNumber")] int PageNumber = 1,
|
||||||
[property: FromQuery(Name = "pageSize")] int PageSize = 20);
|
[property: FromQuery(Name = "pageSize")] int PageSize = 20);
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ namespace Musify.Api.OpenApi
|
|||||||
|
|
||||||
options.AddSchemaTransformer((schema, context, _) =>
|
options.AddSchemaTransformer((schema, context, _) =>
|
||||||
{
|
{
|
||||||
// Every long the API returns is a user id, serialized as a string (JS loses precision above 2^53).
|
|
||||||
// Request DTOs only carry byte sizes, which may be sent as numbers or strings.
|
|
||||||
if (context.JsonPropertyInfo is { } property
|
if (context.JsonPropertyInfo is { } property
|
||||||
&& (property.PropertyType == typeof(long) || property.PropertyType == typeof(long?)))
|
&& (property.PropertyType == typeof(long) || property.PropertyType == typeof(long?)))
|
||||||
{
|
{
|
||||||
@@ -57,7 +55,6 @@ namespace Musify.Api.OpenApi
|
|||||||
|
|
||||||
options.AddOperationTransformer((operation, context, _) =>
|
options.AddOperationTransformer((operation, context, _) =>
|
||||||
{
|
{
|
||||||
// Route parameters constrained with :long are user ids, which travel as strings like every other long.
|
|
||||||
foreach (var parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
|
foreach (var parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
|
||||||
{
|
{
|
||||||
if (parameter.Schema is OpenApiSchema { Type: JsonSchemaType.Integer, Format: "int64" } schema)
|
if (parameter.Schema is OpenApiSchema { Type: JsonSchemaType.Integer, Format: "int64" } schema)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace Musify.Infrastructure.Tests.MassTransit
|
|||||||
|
|
||||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||||
|
|
||||||
// Track
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Track_PictureAndAudioProcessed_CompletesTheSaga()
|
public async Task Track_PictureAndAudioProcessed_CompletesTheSaga()
|
||||||
@@ -86,7 +85,6 @@ namespace Musify.Infrastructure.Tests.MassTransit
|
|||||||
Assert.False(await harness.Published.Any<TrackProcessingFailed>(Ct));
|
Assert.False(await harness.Published.Any<TrackProcessingFailed>(Ct));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Album
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Album_PictureProcessed_CompletesTheSaga()
|
public async Task Album_PictureProcessed_CompletesTheSaga()
|
||||||
@@ -137,7 +135,6 @@ namespace Musify.Infrastructure.Tests.MassTransit
|
|||||||
Assert.False(await harness.Published.Any<Fault<AlbumPictureProcessingFailed>>(Ct));
|
Assert.False(await harness.Published.Any<Fault<AlbumPictureProcessingFailed>>(Ct));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayList
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PlayList_PictureProcessed_CompletesTheSaga()
|
public async Task PlayList_PictureProcessed_CompletesTheSaga()
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ using Xunit;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Tests.MassTransit
|
namespace Musify.Infrastructure.Tests.MassTransit
|
||||||
{
|
{
|
||||||
/// <summary>Runs the picture-update activities inside real routing slips on the MassTransit test harness.</summary>
|
|
||||||
[Collection(InfrastructureCollection.Name)]
|
[Collection(InfrastructureCollection.Name)]
|
||||||
public sealed class UpdatePicturesActivityTests(InfrastructureTestFixture fixture)
|
public sealed class UpdatePicturesActivityTests(InfrastructureTestFixture fixture)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user