Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
@@ -3,29 +3,30 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.Activities.Albums;
internal class PublishAlbumPictureProcessingEventActivity(
IPublishEndpoint publishEndpoint,
ILogger<PublishAlbumPictureProcessingEventActivity> logger)
: IExecuteActivity<PublishAlbumPictureProcessingEventArguments>
namespace Musify.Infrastructure.MassTransit.Activities.Albums
{
public const string ExecuteEndpointName = "publish-album-picture-processing-event";
public async Task<ExecutionResult> Execute(ExecuteContext<PublishAlbumPictureProcessingEventArguments> executeContext)
internal class PublishAlbumPictureProcessingEventActivity(
IPublishEndpoint publishEndpoint,
ILogger<PublishAlbumPictureProcessingEventActivity> logger)
: IExecuteActivity<PublishAlbumPictureProcessingEventArguments>
{
var arguments = executeContext.Arguments;
public const string ExecuteEndpointName = "publish-album-picture-processing-event";
await publishEndpoint.Publish(
new UpdateAlbumPictureEvent(
arguments.AlbumId,
arguments.Bucket,
arguments.FinalPictureKey,
arguments.Sizes),
executeContext.CancellationToken);
public async Task<ExecutionResult> Execute(ExecuteContext<PublishAlbumPictureProcessingEventArguments> executeContext)
{
var arguments = executeContext.Arguments;
logger.LogInformation("Published picture processing event for album {AlbumId}", arguments.AlbumId);
await publishEndpoint.Publish(
new UpdateAlbumPictureEvent(
arguments.AlbumId,
arguments.Bucket,
arguments.FinalPictureKey,
arguments.Sizes),
executeContext.CancellationToken);
return executeContext.Completed();
logger.LogInformation("Published picture processing event for album {AlbumId}", arguments.AlbumId);
return executeContext.Completed();
}
}
}
@@ -5,43 +5,44 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
internal class GenerateAudioWorkflowPathsActivity(
IDatabase database,
ILogger<GenerateAudioWorkflowPathsActivity> logger)
: IExecuteActivity<GenerateAudioWorkflowPathsArguments>
namespace Musify.Infrastructure.MassTransit.Activities.Audio
{
public const string ExecuteEndpointName = "generate-audio-workflow-paths";
public async Task<ExecutionResult> Execute(ExecuteContext<GenerateAudioWorkflowPathsArguments> executeContext)
internal class GenerateAudioWorkflowPathsActivity(
IDatabase database,
ILogger<GenerateAudioWorkflowPathsActivity> logger)
: IExecuteActivity<GenerateAudioWorkflowPathsArguments>
{
var arguments = executeContext.Arguments;
var track = await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
public const string ExecuteEndpointName = "generate-audio-workflow-paths";
try
public async Task<ExecutionResult> Execute(ExecuteContext<GenerateAudioWorkflowPathsArguments> executeContext)
{
track.Audio.TranscodeStatus = ProcessingStatus.Processing;
var arguments = executeContext.Arguments;
var track = await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
var workingDirectory = Path.Combine(arguments.TemporaryRootDirectory, Guid.NewGuid().ToString());
var sourceFilePath = Path.Combine(workingDirectory, Path.GetFileName(arguments.SourceKey));
Directory.CreateDirectory(workingDirectory);
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogDebug("Generated audio workflow paths in {WorkingDirectory}", workingDirectory);
return executeContext.CompletedWithVariables(new Dictionary<string, object>
try
{
[RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory,
[RoutingSlipVariableNames.Audio.SourceFilePath] = sourceFilePath,
[RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory
});
}
catch
{
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
track.Audio.TranscodeStatus = ProcessingStatus.Processing;
var workingDirectory = Path.Combine(arguments.TemporaryRootDirectory, Guid.NewGuid().ToString());
var sourceFilePath = Path.Combine(workingDirectory, Path.GetFileName(arguments.SourceKey));
Directory.CreateDirectory(workingDirectory);
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogDebug("Generated audio workflow paths in {WorkingDirectory}", workingDirectory);
return executeContext.CompletedWithVariables(new Dictionary<string, object>
{
[RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory,
[RoutingSlipVariableNames.Audio.SourceFilePath] = sourceFilePath,
[RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory
});
}
catch
{
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
}
}
}
}
@@ -3,22 +3,23 @@ using Musify.Application.Contracts;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
/// <summary>Shared bookkeeping of the audio processing activities.</summary>
internal static class TrackAudioStatus
namespace Musify.Infrastructure.MassTransit.Activities.Audio
{
public static async Task<Track> LoadAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken) =>
await database.Tracks.SingleOrDefaultAsync(track => track.Id == trackId, cancellationToken)
?? throw new InvalidOperationException($"Track with id {trackId} not found");
public static async Task MarkFailedAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken)
/// <summary>Shared bookkeeping of the audio processing activities.</summary>
internal static class TrackAudioStatus
{
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track == null)
return;
public static async Task<Track> LoadAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken) =>
await database.Tracks.SingleOrDefaultAsync(track => track.Id == trackId, cancellationToken)
?? throw new InvalidOperationException($"Track with id {trackId} not found");
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(cancellationToken);
public static async Task MarkFailedAsync(IDatabase database, Guid trackId, CancellationToken cancellationToken)
{
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track == null)
return;
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(cancellationToken);
}
}
}
@@ -5,61 +5,62 @@ using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
internal class TranscodeAudioActivity(
IDatabase database,
IAudioTranscoderService audioTranscoder,
ILogger<TranscodeAudioActivity> logger)
: IActivity<TranscodeAudioArguments, TranscodeAudioLog>
namespace Musify.Infrastructure.MassTransit.Activities.Audio
{
public const string ExecuteEndpointName = "transcode-audio";
public async Task<ExecutionResult> Execute(ExecuteContext<TranscodeAudioArguments> executeContext)
internal class TranscodeAudioActivity(
IDatabase database,
IAudioTranscoderService audioTranscoder,
ILogger<TranscodeAudioActivity> logger)
: IActivity<TranscodeAudioArguments, TranscodeAudioLog>
{
var arguments = executeContext.Arguments;
var sourceFilePath = executeContext.GetVariable<string>(arguments.SourceFilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var workingDirectory = executeContext.GetVariable<string>(arguments.WorkingDirectoryVariable);
ArgumentNullException.ThrowIfNull(workingDirectory);
public const string ExecuteEndpointName = "transcode-audio";
await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
try
public async Task<ExecutionResult> Execute(ExecuteContext<TranscodeAudioArguments> executeContext)
{
AudioTranscodeResult transcodeResult;
await using (var fileStream = File.OpenRead(sourceFilePath))
var arguments = executeContext.Arguments;
var sourceFilePath = executeContext.GetVariable<string>(arguments.SourceFilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var workingDirectory = executeContext.GetVariable<string>(arguments.WorkingDirectoryVariable);
ArgumentNullException.ThrowIfNull(workingDirectory);
await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
try
{
transcodeResult = await audioTranscoder.TranscodeToAudioFileAsync(
fileStream, workingDirectory, executeContext.CancellationToken);
AudioTranscodeResult transcodeResult;
await using (var fileStream = File.OpenRead(sourceFilePath))
{
transcodeResult = await audioTranscoder.TranscodeToAudioFileAsync(
fileStream, workingDirectory, executeContext.CancellationToken);
}
if (transcodeResult.StatusCode != 0)
throw new InvalidOperationException($"ffmpeg transcoding failed with exit code {transcodeResult.StatusCode}.");
File.Delete(sourceFilePath);
return executeContext.CompletedWithVariables(new TranscodeAudioLog(workingDirectory), new Dictionary<string, object>
{
[RoutingSlipVariableNames.Audio.DurationSeconds] = transcodeResult.Duration.TotalSeconds
});
}
if (transcodeResult.StatusCode != 0)
throw new InvalidOperationException($"ffmpeg transcoding failed with exit code {transcodeResult.StatusCode}.");
File.Delete(sourceFilePath);
return executeContext.CompletedWithVariables(new TranscodeAudioLog(workingDirectory), new Dictionary<string, object>
catch
{
[RoutingSlipVariableNames.Audio.DurationSeconds] = transcodeResult.Duration.TotalSeconds
});
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
}
}
catch
public Task<CompensationResult> Compensate(CompensateContext<TranscodeAudioLog> compensateContext)
{
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
var workingDirectory = compensateContext.Log.WorkingDirectory;
if (Directory.Exists(workingDirectory))
Directory.Delete(workingDirectory, recursive: true);
else
logger.LogWarning("Working directory {WorkingDirectory} not found during compensation", workingDirectory);
return Task.FromResult(compensateContext.Compensated());
}
}
public Task<CompensationResult> Compensate(CompensateContext<TranscodeAudioLog> compensateContext)
{
var workingDirectory = compensateContext.Log.WorkingDirectory;
if (Directory.Exists(workingDirectory))
Directory.Delete(workingDirectory, recursive: true);
else
logger.LogWarning("Working directory {WorkingDirectory} not found during compensation", workingDirectory);
return Task.FromResult(compensateContext.Compensated());
}
}
@@ -6,61 +6,62 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Audio;
internal class UpdateTrackAudioActivity(
IDatabase database,
IPublishEndpoint publishEndpoint,
ILogger<UpdateTrackAudioActivity> logger)
: IActivity<UpdateTrackAudioArguments, UpdateTrackAudioLog>
namespace Musify.Infrastructure.MassTransit.Activities.Audio
{
public const string ExecuteEndpointName = "update-track-audio";
public async Task<ExecutionResult> Execute(ExecuteContext<UpdateTrackAudioArguments> executeContext)
internal class UpdateTrackAudioActivity(
IDatabase database,
IPublishEndpoint publishEndpoint,
ILogger<UpdateTrackAudioActivity> logger)
: IActivity<UpdateTrackAudioArguments, UpdateTrackAudioLog>
{
var arguments = executeContext.Arguments;
var track = await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
public const string ExecuteEndpointName = "update-track-audio";
try
public async Task<ExecutionResult> Execute(ExecuteContext<UpdateTrackAudioArguments> executeContext)
{
if (string.IsNullOrWhiteSpace(arguments.AudioFolderKey))
throw new InvalidOperationException($"Audio folder key is empty for track {track.Id}");
var arguments = executeContext.Arguments;
var track = await TrackAudioStatus.LoadAsync(database, arguments.TrackId, executeContext.CancellationToken);
var durationSeconds = executeContext.GetVariable<double>(arguments.DurationSecondsVariable)
?? throw new InvalidOperationException($"Missing routing slip variable {arguments.DurationSecondsVariable}");
try
{
if (string.IsNullOrWhiteSpace(arguments.AudioFolderKey))
throw new InvalidOperationException($"Audio folder key is empty for track {track.Id}");
var log = new UpdateTrackAudioLog(track.Id, track.Audio.FolderName, track.DurationSeconds);
var durationSeconds = executeContext.GetVariable<double>(arguments.DurationSecondsVariable)
?? throw new InvalidOperationException($"Missing routing slip variable {arguments.DurationSecondsVariable}");
track.Audio.FolderName = Path.GetFileName(arguments.AudioFolderKey);
track.DurationSeconds = durationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Completed;
await database.SaveChangesAsync(executeContext.CancellationToken);
var log = new UpdateTrackAudioLog(track.Id, track.Audio.FolderName, track.DurationSeconds);
logger.LogInformation("Updated track {TrackId} audio", arguments.TrackId);
track.Audio.FolderName = Path.GetFileName(arguments.AudioFolderKey);
track.DurationSeconds = durationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Completed;
await database.SaveChangesAsync(executeContext.CancellationToken);
await publishEndpoint.Publish(new TrackAudioProcessed(arguments.TrackId), executeContext.CancellationToken);
logger.LogInformation("Updated track {TrackId} audio", arguments.TrackId);
return executeContext.Completed(log);
await publishEndpoint.Publish(new TrackAudioProcessed(arguments.TrackId), executeContext.CancellationToken);
return executeContext.Completed(log);
}
catch
{
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
}
}
catch
public async Task<CompensationResult> Compensate(CompensateContext<UpdateTrackAudioLog> compensateContext)
{
await TrackAudioStatus.MarkFailedAsync(database, arguments.TrackId, executeContext.CancellationToken);
throw;
var log = compensateContext.Log;
var track = await TrackAudioStatus.LoadAsync(database, log.TrackId, compensateContext.CancellationToken);
track.Audio.FolderName = log.PreviousAudioFolderName;
track.DurationSeconds = log.PreviousDurationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(compensateContext.CancellationToken);
logger.LogInformation("Compensated track {TrackId} audio", log.TrackId);
return compensateContext.Compensated();
}
}
public async Task<CompensationResult> Compensate(CompensateContext<UpdateTrackAudioLog> compensateContext)
{
var log = compensateContext.Log;
var track = await TrackAudioStatus.LoadAsync(database, log.TrackId, compensateContext.CancellationToken);
track.Audio.FolderName = log.PreviousAudioFolderName;
track.DurationSeconds = log.PreviousDurationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(compensateContext.CancellationToken);
logger.LogInformation("Compensated track {TrackId} audio", log.TrackId);
return compensateContext.Compensated();
}
}
@@ -4,45 +4,46 @@ using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Files;
internal class CopyFileInBucketActivity(
IStorageService storageService,
ILogger<CopyFileInBucketActivity> logger)
: IActivity<CopyFileInBucketArguments, CopyFileInBucketLog>
namespace Musify.Infrastructure.MassTransit.Activities.Files
{
public const string ExecuteEndpointName = "copy-file-in-bucket";
public async Task<ExecutionResult> Execute(ExecuteContext<CopyFileInBucketArguments> executeContext)
internal class CopyFileInBucketActivity(
IStorageService storageService,
ILogger<CopyFileInBucketActivity> logger)
: IActivity<CopyFileInBucketArguments, CopyFileInBucketLog>
{
await storageService.CopyFileAsync(
executeContext.Arguments.SourceBucket,
executeContext.Arguments.SourceKey,
executeContext.Arguments.DestinationBucket,
executeContext.Arguments.DestinationKey,
executeContext.CancellationToken);
public const string ExecuteEndpointName = "copy-file-in-bucket";
return executeContext.Completed(new CopyFileInBucketLog(
executeContext.Arguments.DestinationBucket,
executeContext.Arguments.DestinationKey));
}
public async Task<CompensationResult> Compensate(CompensateContext<CopyFileInBucketLog> compensateContext)
{
try
public async Task<ExecutionResult> Execute(ExecuteContext<CopyFileInBucketArguments> executeContext)
{
await storageService.RemoveFileAsync(
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey,
compensateContext.CancellationToken);
return compensateContext.Compensated();
await storageService.CopyFileAsync(
executeContext.Arguments.SourceBucket,
executeContext.Arguments.SourceKey,
executeContext.Arguments.DestinationBucket,
executeContext.Arguments.DestinationKey,
executeContext.CancellationToken);
return executeContext.Completed(new CopyFileInBucketLog(
executeContext.Arguments.DestinationBucket,
executeContext.Arguments.DestinationKey));
}
catch (Exception exception)
public async Task<CompensationResult> Compensate(CompensateContext<CopyFileInBucketLog> compensateContext)
{
logger.LogError(exception, "Failed to compensate copied file {Bucket}/{Key}",
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey);
return compensateContext.Failed(exception);
try
{
await storageService.RemoveFileAsync(
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey,
compensateContext.CancellationToken);
return compensateContext.Compensated();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate copied file {Bucket}/{Key}",
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey);
return compensateContext.Failed(exception);
}
}
}
}
@@ -4,52 +4,53 @@ using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Files;
internal class DownloadFileFromBucketActivity(
IStorageService storageService,
ILogger<DownloadFileFromBucketActivity> logger)
: IActivity<DownloadFileFromBucketArguments, DownloadFileFromBucketLog>
namespace Musify.Infrastructure.MassTransit.Activities.Files
{
public const string ExecuteEndpointName = "download-file-from-bucket";
public async Task<ExecutionResult> Execute(ExecuteContext<DownloadFileFromBucketArguments> executeContext)
internal class DownloadFileFromBucketActivity(
IStorageService storageService,
ILogger<DownloadFileFromBucketActivity> logger)
: IActivity<DownloadFileFromBucketArguments, DownloadFileFromBucketLog>
{
var destinationPath = executeContext.GetVariable<string>(executeContext.Arguments.DestinationFilePathVariable);
ArgumentNullException.ThrowIfNull(destinationPath);
public const string ExecuteEndpointName = "download-file-from-bucket";
using var fileStream = await storageService.GetFileAsync(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
executeContext.CancellationToken)
?? throw new FileNotFoundException($"Object {executeContext.Arguments.Bucket}/{executeContext.Arguments.Key} was not found.");
var destinationDirectory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrWhiteSpace(destinationDirectory))
Directory.CreateDirectory(destinationDirectory);
if (fileStream.CanSeek)
fileStream.Position = 0;
using var destinationStream = File.Create(destinationPath);
await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken);
return executeContext.Completed(new DownloadFileFromBucketLog(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
destinationPath));
}
public Task<CompensationResult> Compensate(CompensateContext<DownloadFileFromBucketLog> compensateContext)
{
try
public async Task<ExecutionResult> Execute(ExecuteContext<DownloadFileFromBucketArguments> executeContext)
{
File.Delete(compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Compensated());
var destinationPath = executeContext.GetVariable<string>(executeContext.Arguments.DestinationFilePathVariable);
ArgumentNullException.ThrowIfNull(destinationPath);
using var fileStream = await storageService.GetFileAsync(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
executeContext.CancellationToken)
?? throw new FileNotFoundException($"Object {executeContext.Arguments.Bucket}/{executeContext.Arguments.Key} was not found.");
var destinationDirectory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrWhiteSpace(destinationDirectory))
Directory.CreateDirectory(destinationDirectory);
if (fileStream.CanSeek)
fileStream.Position = 0;
using var destinationStream = File.Create(destinationPath);
await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken);
return executeContext.Completed(new DownloadFileFromBucketLog(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
destinationPath));
}
catch (Exception exception)
public Task<CompensationResult> Compensate(CompensateContext<DownloadFileFromBucketLog> compensateContext)
{
logger.LogError(exception, "Failed to compensate downloaded file {DestinationFilePath}", compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Failed(exception));
try
{
File.Delete(compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Compensated());
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate downloaded file {DestinationFilePath}", compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Failed(exception));
}
}
}
}
@@ -5,34 +5,35 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.Activities.Files;
internal class RemoveFileFromBucketActivity(
IStorageService storageService,
ILogger<RemoveFileFromBucketActivity> logger)
: IExecuteActivity<RemoveFileFromBucketArguments>
namespace Musify.Infrastructure.MassTransit.Activities.Files
{
public const string ExecuteEndpointName = "remove-file-from-bucket";
public async Task<ExecutionResult> Execute(ExecuteContext<RemoveFileFromBucketArguments> executeContext)
internal class RemoveFileFromBucketActivity(
IStorageService storageService,
ILogger<RemoveFileFromBucketActivity> logger)
: IExecuteActivity<RemoveFileFromBucketArguments>
{
try
public const string ExecuteEndpointName = "remove-file-from-bucket";
public async Task<ExecutionResult> Execute(ExecuteContext<RemoveFileFromBucketArguments> executeContext)
{
await storageService.RemoveFileAsync(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
executeContext.CancellationToken);
return executeContext.Completed();
}
catch (AmazonS3Exception exception) when (
exception.StatusCode == HttpStatusCode.NotFound
|| string.Equals(exception.ErrorCode, "NoSuchKey", StringComparison.OrdinalIgnoreCase))
{
logger.LogInformation(
"Key {Key} not found in bucket {Bucket}, skipping",
executeContext.Arguments.Key,
executeContext.Arguments.Bucket);
return executeContext.Completed();
try
{
await storageService.RemoveFileAsync(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
executeContext.CancellationToken);
return executeContext.Completed();
}
catch (AmazonS3Exception exception) when (
exception.StatusCode == HttpStatusCode.NotFound
|| string.Equals(exception.ErrorCode, "NoSuchKey", StringComparison.OrdinalIgnoreCase))
{
logger.LogInformation(
"Key {Key} not found in bucket {Bucket}, skipping",
executeContext.Arguments.Key,
executeContext.Arguments.Bucket);
return executeContext.Completed();
}
}
}
}
@@ -4,51 +4,52 @@ using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Files;
internal class TransferFilesToBucketActivity(
IStorageService storageService,
ILogger<TransferFilesToBucketActivity> logger)
: IActivity<TransferFilesToBucketArguments, TransferFilesToBucketLog>
namespace Musify.Infrastructure.MassTransit.Activities.Files
{
public const string ExecuteEndpointName = "transfer-files-to-bucket";
public async Task<ExecutionResult> Execute(ExecuteContext<TransferFilesToBucketArguments> executeContext)
internal class TransferFilesToBucketActivity(
IStorageService storageService,
ILogger<TransferFilesToBucketActivity> logger)
: IActivity<TransferFilesToBucketArguments, TransferFilesToBucketLog>
{
var folderPath = executeContext.GetVariable<string>(executeContext.Arguments.SourceDirectoryVariable);
ArgumentNullException.ThrowIfNull(folderPath);
var destinationKey = executeContext.Arguments.DestinationKey;
ArgumentNullException.ThrowIfNull(destinationKey);
public const string ExecuteEndpointName = "transfer-files-to-bucket";
var uploadedKeys = await storageService.TransferFilesAsync(
folderPath,
executeContext.Arguments.DestinationBucket,
destinationKey,
executeContext.CancellationToken);
return executeContext.Completed(new TransferFilesToBucketLog(
executeContext.Arguments.DestinationBucket,
uploadedKeys.ToArray()));
}
public async Task<CompensationResult> Compensate(CompensateContext<TransferFilesToBucketLog> compensateContext)
{
try
public async Task<ExecutionResult> Execute(ExecuteContext<TransferFilesToBucketArguments> executeContext)
{
foreach (var uploadedKey in compensateContext.Log.UploadedKeys)
{
await storageService.RemoveFileAsync(
compensateContext.Log.DestinationBucket,
uploadedKey,
compensateContext.CancellationToken);
}
var folderPath = executeContext.GetVariable<string>(executeContext.Arguments.SourceDirectoryVariable);
ArgumentNullException.ThrowIfNull(folderPath);
var destinationKey = executeContext.Arguments.DestinationKey;
ArgumentNullException.ThrowIfNull(destinationKey);
return compensateContext.Compensated();
var uploadedKeys = await storageService.TransferFilesAsync(
folderPath,
executeContext.Arguments.DestinationBucket,
destinationKey,
executeContext.CancellationToken);
return executeContext.Completed(new TransferFilesToBucketLog(
executeContext.Arguments.DestinationBucket,
uploadedKeys.ToArray()));
}
catch (Exception exception)
public async Task<CompensationResult> Compensate(CompensateContext<TransferFilesToBucketLog> compensateContext)
{
logger.LogError(exception, "Failed to compensate file transfers to {DestinationBucket}", compensateContext.Log.DestinationBucket);
return compensateContext.Failed(exception);
try
{
foreach (var uploadedKey in compensateContext.Log.UploadedKeys)
{
await storageService.RemoveFileAsync(
compensateContext.Log.DestinationBucket,
uploadedKey,
compensateContext.CancellationToken);
}
return compensateContext.Compensated();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate file transfers to {DestinationBucket}", compensateContext.Log.DestinationBucket);
return compensateContext.Failed(exception);
}
}
}
}
@@ -4,49 +4,50 @@ using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Files;
internal class UploadFileToBucketActivity(IStorageService storageService,
ILogger<UploadFileToBucketActivity> logger)
: IActivity<UploadFileToBucketArguments, UploadFileToBucketLog>
namespace Musify.Infrastructure.MassTransit.Activities.Files
{
public const string ExecuteEndpointName = "upload-file-to-bucket";
public async Task<ExecutionResult> Execute(ExecuteContext<UploadFileToBucketArguments> executeContext)
internal class UploadFileToBucketActivity(IStorageService storageService,
ILogger<UploadFileToBucketActivity> logger)
: IActivity<UploadFileToBucketArguments, UploadFileToBucketLog>
{
var sourceFilePath = executeContext.GetVariable<string>(executeContext.Arguments.FilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var fileName = Path.GetFileName(sourceFilePath);
var destinationKey = string.Join('/', new[] { executeContext.Arguments.DestinationRoute, fileName }
.Where(static s => !string.IsNullOrWhiteSpace(s))
.Select(static s => s.Trim().Trim('/', '\\')));
public const string ExecuteEndpointName = "upload-file-to-bucket";
await storageService.UploadFileAsync(
sourceFilePath,
executeContext.Arguments.DestinationBucket,
destinationKey,
executeContext.CancellationToken);
return executeContext.CompletedWithVariables(new UploadFileToBucketLog(
executeContext.Arguments.DestinationBucket,
destinationKey));
}
public async Task<CompensationResult> Compensate(CompensateContext<UploadFileToBucketLog> compensateContext)
{
try
public async Task<ExecutionResult> Execute(ExecuteContext<UploadFileToBucketArguments> executeContext)
{
await storageService.RemoveFileAsync(
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey,
compensateContext.CancellationToken);
return compensateContext.Compensated();
var sourceFilePath = executeContext.GetVariable<string>(executeContext.Arguments.FilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var fileName = Path.GetFileName(sourceFilePath);
var destinationKey = string.Join('/', new[] { executeContext.Arguments.DestinationRoute, fileName }
.Where(static s => !string.IsNullOrWhiteSpace(s))
.Select(static s => s.Trim().Trim('/', '\\')));
await storageService.UploadFileAsync(
sourceFilePath,
executeContext.Arguments.DestinationBucket,
destinationKey,
executeContext.CancellationToken);
return executeContext.CompletedWithVariables(new UploadFileToBucketLog(
executeContext.Arguments.DestinationBucket,
destinationKey));
}
catch (Exception exception)
public async Task<CompensationResult> Compensate(CompensateContext<UploadFileToBucketLog> compensateContext)
{
logger.LogError(exception, "Error compensating file upload for {Bucket}/{Key}",
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey);
return compensateContext.Failed(exception);
try
{
await storageService.RemoveFileAsync(
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey,
compensateContext.CancellationToken);
return compensateContext.Compensated();
}
catch (Exception exception)
{
logger.LogError(exception, "Error compensating file upload for {Bucket}/{Key}",
compensateContext.Log.DestinationBucket,
compensateContext.Log.DestinationKey);
return compensateContext.Failed(exception);
}
}
}
}
@@ -6,92 +6,93 @@ using Musify.Domain.Abstractions;
using Musify.Domain.Entities;
using Musify.Infrastructure.MassTransit.Arguments;
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)
: IExecuteActivity<MarkLifeCycleArguments>
where TEntity : class, IHasLifeCycle
namespace Musify.Infrastructure.MassTransit.Activities.LifeCycle
{
public async Task<ExecutionResult> Execute(ExecuteContext<MarkLifeCycleArguments> executeContext)
/// <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)
: IExecuteActivity<MarkLifeCycleArguments>
where TEntity : class, IHasLifeCycle
{
var arguments = executeContext.Arguments;
var entity = await database.Set<TEntity>()
.SingleOrDefaultAsync(e => e.Id == arguments.Id, executeContext.CancellationToken);
if (entity == null)
public async Task<ExecutionResult> Execute(ExecuteContext<MarkLifeCycleArguments> executeContext)
{
logger.LogInformation("{Entity} {Id} not found while marking it as {Status}, skipping", typeof(TEntity).Name, arguments.Id, arguments.Status);
var arguments = executeContext.Arguments;
var entity = await database.Set<TEntity>()
.SingleOrDefaultAsync(e => e.Id == arguments.Id, executeContext.CancellationToken);
if (entity == null)
{
logger.LogInformation("{Entity} {Id} not found while marking it as {Status}, skipping", typeof(TEntity).Name, arguments.Id, arguments.Status);
return executeContext.Completed();
}
entity.LifeCycleStatus = arguments.Status;
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Marked {Entity} {Id} as {Status}", typeof(TEntity).Name, arguments.Id, arguments.Status);
return executeContext.Completed();
}
entity.LifeCycleStatus = arguments.Status;
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Marked {Entity} {Id} as {Status}", typeof(TEntity).Name, arguments.Id, arguments.Status);
return executeContext.Completed();
}
}
/// <summary>Deletes an entity row. A missing entity is skipped.</summary>
internal abstract class DeleteEntityActivity<TEntity>(IDatabase database, ILogger logger)
: IExecuteActivity<DeleteEntityArguments>
where TEntity : class, IHasLifeCycle
{
public async Task<ExecutionResult> Execute(ExecuteContext<DeleteEntityArguments> executeContext)
/// <summary>Deletes an entity row. A missing entity is skipped.</summary>
internal abstract class DeleteEntityActivity<TEntity>(IDatabase database, ILogger logger)
: IExecuteActivity<DeleteEntityArguments>
where TEntity : class, IHasLifeCycle
{
var id = executeContext.Arguments.Id;
var entity = await database.Set<TEntity>()
.SingleOrDefaultAsync(e => e.Id == id, executeContext.CancellationToken);
if (entity == null)
public async Task<ExecutionResult> Execute(ExecuteContext<DeleteEntityArguments> executeContext)
{
logger.LogInformation("{Entity} {Id} not found while deleting it, skipping", typeof(TEntity).Name, id);
var id = executeContext.Arguments.Id;
var entity = await database.Set<TEntity>()
.SingleOrDefaultAsync(e => e.Id == id, executeContext.CancellationToken);
if (entity == null)
{
logger.LogInformation("{Entity} {Id} not found while deleting it, skipping", typeof(TEntity).Name, id);
return executeContext.Completed();
}
database.Set<TEntity>().Remove(entity);
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Deleted {Entity} {Id} from the database", typeof(TEntity).Name, id);
return executeContext.Completed();
}
}
database.Set<TEntity>().Remove(entity);
await database.SaveChangesAsync(executeContext.CancellationToken);
// 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)
: MarkLifeCycleActivity<Track>(database, logger)
{
public const string ExecuteEndpointName = "mark-track-life-cycle";
}
logger.LogInformation("Deleted {Entity} {Id} from the database", typeof(TEntity).Name, id);
return executeContext.Completed();
internal sealed class MarkPlayListLifeCycleActivity(IDatabase database, ILogger<MarkPlayListLifeCycleActivity> logger)
: MarkLifeCycleActivity<PlayList>(database, logger)
{
public const string ExecuteEndpointName = "mark-play-list-life-cycle";
}
internal sealed class MarkAlbumLifeCycleActivity(IDatabase database, ILogger<MarkAlbumLifeCycleActivity> logger)
: MarkLifeCycleActivity<Album>(database, logger)
{
public const string ExecuteEndpointName = "mark-album-life-cycle";
}
internal sealed class DeleteTrackActivity(IDatabase database, ILogger<DeleteTrackActivity> logger)
: DeleteEntityActivity<Track>(database, logger)
{
public const string ExecuteEndpointName = "delete-track";
}
internal sealed class DeletePlayListActivity(IDatabase database, ILogger<DeletePlayListActivity> logger)
: DeleteEntityActivity<PlayList>(database, logger)
{
public const string ExecuteEndpointName = "delete-play-list";
}
internal sealed class DeleteAlbumActivity(IDatabase database, ILogger<DeleteAlbumActivity> logger)
: DeleteEntityActivity<Album>(database, logger)
{
public const string ExecuteEndpointName = "delete-album";
}
}
// 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)
: MarkLifeCycleActivity<Track>(database, logger)
{
public const string ExecuteEndpointName = "mark-track-life-cycle";
}
internal sealed class MarkPlayListLifeCycleActivity(IDatabase database, ILogger<MarkPlayListLifeCycleActivity> logger)
: MarkLifeCycleActivity<PlayList>(database, logger)
{
public const string ExecuteEndpointName = "mark-play-list-life-cycle";
}
internal sealed class MarkAlbumLifeCycleActivity(IDatabase database, ILogger<MarkAlbumLifeCycleActivity> logger)
: MarkLifeCycleActivity<Album>(database, logger)
{
public const string ExecuteEndpointName = "mark-album-life-cycle";
}
internal sealed class DeleteTrackActivity(IDatabase database, ILogger<DeleteTrackActivity> logger)
: DeleteEntityActivity<Track>(database, logger)
{
public const string ExecuteEndpointName = "delete-track";
}
internal sealed class DeletePlayListActivity(IDatabase database, ILogger<DeletePlayListActivity> logger)
: DeleteEntityActivity<PlayList>(database, logger)
{
public const string ExecuteEndpointName = "delete-play-list";
}
internal sealed class DeleteAlbumActivity(IDatabase database, ILogger<DeleteAlbumActivity> logger)
: DeleteEntityActivity<Album>(database, logger)
{
public const string ExecuteEndpointName = "delete-album";
}
@@ -3,37 +3,38 @@ using Microsoft.Extensions.Logging;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Activities.Pictures;
internal class GeneratePictureWorkflowPathsActivity(
ILogger<GeneratePictureWorkflowPathsActivity> logger)
: IExecuteActivity<GeneratePictureWorkflowPathsArguments>
namespace Musify.Infrastructure.MassTransit.Activities.Pictures
{
public const string ExecuteEndpointName = "generate-picture-workflow-paths";
internal const string ResizedPictureFileExtension = ".webp";
public Task<ExecutionResult> Execute(ExecuteContext<GeneratePictureWorkflowPathsArguments> executeContext)
internal class GeneratePictureWorkflowPathsActivity(
ILogger<GeneratePictureWorkflowPathsActivity> logger)
: IExecuteActivity<GeneratePictureWorkflowPathsArguments>
{
var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, Guid.NewGuid().ToString());
public const string ExecuteEndpointName = "generate-picture-workflow-paths";
var sourceFilePath = Path.Combine(
workingDirectory, Guid.NewGuid().ToString() + Path.GetExtension(executeContext.Arguments.SourceKey));
var smallPictureFilePath = Path.Combine(workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension);
var mediumPictureFilePath = Path.Combine(workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension);
var largePictureFilePath = Path.Combine(workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension);
internal const string ResizedPictureFileExtension = ".webp";
Directory.CreateDirectory(workingDirectory);
logger.LogDebug("Generated picture workflow paths in {WorkingDirectory}", workingDirectory);
return Task.FromResult(executeContext.CompletedWithVariables(new Dictionary<string, object>
public Task<ExecutionResult> Execute(ExecuteContext<GeneratePictureWorkflowPathsArguments> executeContext)
{
[RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory,
[RoutingSlipVariableNames.Picture.OriginalFilePath] = sourceFilePath,
[RoutingSlipVariableNames.Picture.SmallResizedFilePath] = smallPictureFilePath,
[RoutingSlipVariableNames.Picture.MediumResizedFilePath] = mediumPictureFilePath,
[RoutingSlipVariableNames.Picture.LargeResizedFilePath] = largePictureFilePath
}));
var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, Guid.NewGuid().ToString());
var sourceFilePath = Path.Combine(
workingDirectory, Guid.NewGuid().ToString() + Path.GetExtension(executeContext.Arguments.SourceKey));
var smallPictureFilePath = Path.Combine(workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension);
var mediumPictureFilePath = Path.Combine(workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension);
var largePictureFilePath = Path.Combine(workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension);
Directory.CreateDirectory(workingDirectory);
logger.LogDebug("Generated picture workflow paths in {WorkingDirectory}", workingDirectory);
return Task.FromResult(executeContext.CompletedWithVariables(new Dictionary<string, object>
{
[RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory,
[RoutingSlipVariableNames.Picture.OriginalFilePath] = sourceFilePath,
[RoutingSlipVariableNames.Picture.SmallResizedFilePath] = smallPictureFilePath,
[RoutingSlipVariableNames.Picture.MediumResizedFilePath] = mediumPictureFilePath,
[RoutingSlipVariableNames.Picture.LargeResizedFilePath] = largePictureFilePath
}));
}
}
}
@@ -4,60 +4,61 @@ using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Pictures;
internal class ResizePictureActivity(
IPictureService pictureHandler,
ILogger<ResizePictureActivity> logger)
: IActivity<ResizePictureLocalArguments, ResizePictureLog>
namespace Musify.Infrastructure.MassTransit.Activities.Pictures
{
public const string ExecuteEndpointName = "resize-picture";
public async Task<ExecutionResult> Execute(ExecuteContext<ResizePictureLocalArguments> executeContext)
internal class ResizePictureActivity(
IPictureService pictureHandler,
ILogger<ResizePictureActivity> logger)
: IActivity<ResizePictureLocalArguments, ResizePictureLog>
{
var sourceFilePath = executeContext.GetVariable<string>(executeContext.Arguments.SourceFilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var destinationFilePath = executeContext.GetVariable<string>(executeContext.Arguments.DestinationFilePathVariable);
ArgumentNullException.ThrowIfNull(destinationFilePath);
public const string ExecuteEndpointName = "resize-picture";
if (!File.Exists(sourceFilePath))
public async Task<ExecutionResult> Execute(ExecuteContext<ResizePictureLocalArguments> executeContext)
{
logger.LogWarning("Source file not found: {SourceFilePath}", sourceFilePath);
throw new FileNotFoundException($"Source file not found: {sourceFilePath}");
var sourceFilePath = executeContext.GetVariable<string>(executeContext.Arguments.SourceFilePathVariable);
ArgumentNullException.ThrowIfNull(sourceFilePath);
var destinationFilePath = executeContext.GetVariable<string>(executeContext.Arguments.DestinationFilePathVariable);
ArgumentNullException.ThrowIfNull(destinationFilePath);
if (!File.Exists(sourceFilePath))
{
logger.LogWarning("Source file not found: {SourceFilePath}", sourceFilePath);
throw new FileNotFoundException($"Source file not found: {sourceFilePath}");
}
await using var fileStream = File.OpenRead(sourceFilePath);
await using var resizedPicture = await pictureHandler.ResizePictureAsWebpAsync(
fileStream,
executeContext.Arguments.Width,
executeContext.Arguments.Height,
executeContext.CancellationToken);
var destinationDirectory = Path.GetDirectoryName(destinationFilePath);
if (!string.IsNullOrEmpty(destinationDirectory))
Directory.CreateDirectory(destinationDirectory);
await using var destinationStream = File.Create(destinationFilePath);
await resizedPicture.CopyToAsync(destinationStream, executeContext.CancellationToken);
logger.LogDebug("Resized picture from {SourceFilePath} to {DestinationFilePath}",
sourceFilePath,
destinationFilePath);
return executeContext.Completed(new ResizePictureLog(destinationFilePath));
}
await using var fileStream = File.OpenRead(sourceFilePath);
await using var resizedPicture = await pictureHandler.ResizePictureAsWebpAsync(
fileStream,
executeContext.Arguments.Width,
executeContext.Arguments.Height,
executeContext.CancellationToken);
var destinationDirectory = Path.GetDirectoryName(destinationFilePath);
if (!string.IsNullOrEmpty(destinationDirectory))
Directory.CreateDirectory(destinationDirectory);
await using var destinationStream = File.Create(destinationFilePath);
await resizedPicture.CopyToAsync(destinationStream, executeContext.CancellationToken);
logger.LogDebug("Resized picture from {SourceFilePath} to {DestinationFilePath}",
sourceFilePath,
destinationFilePath);
return executeContext.Completed(new ResizePictureLog(destinationFilePath));
}
public Task<CompensationResult> Compensate(CompensateContext<ResizePictureLog> compensateContext)
{
try
public Task<CompensationResult> Compensate(CompensateContext<ResizePictureLog> compensateContext)
{
File.Delete(compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Compensated());
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate resized file {DestinationFilePath}", compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Failed(exception));
try
{
File.Delete(compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Compensated());
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate resized file {DestinationFilePath}", compensateContext.Log.DestinationFilePath);
return Task.FromResult(compensateContext.Failed(exception));
}
}
}
}
@@ -8,153 +8,154 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.Pictures;
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>(
IDatabase database,
IPublishEndpoint publishEndpoint,
ILogger logger)
: IActivity<UpdatePicturesArguments, UpdatePicturesLog>
where TEntity : class
namespace Musify.Infrastructure.MassTransit.Activities.Pictures
{
protected IDatabase Database { get; } = database;
internal sealed record PictureNames(string? Original, string? Small, string? Medium, string? Large);
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 void Write(TEntity entity, PictureNames names, bool succeeded);
protected abstract object ProcessedEvent(Guid id);
public async Task<ExecutionResult> Execute(ExecuteContext<UpdatePicturesArguments> executeContext)
/// <summary>Stores the processed picture names on the owner and reports it; compensation restores the previous names.</summary>
internal abstract class UpdatePicturesActivity<TEntity>(
IDatabase database,
IPublishEndpoint publishEndpoint,
ILogger logger)
: IActivity<UpdatePicturesArguments, UpdatePicturesLog>
where TEntity : class
{
var arguments = executeContext.Arguments;
var small = RequireVariable(executeContext, arguments.SmallPictureVariable);
var medium = RequireVariable(executeContext, arguments.MediumPictureVariable);
var large = RequireVariable(executeContext, arguments.LargePictureVariable);
protected IDatabase Database { get; } = database;
var entity = await FindAsync(arguments.SubjectId, executeContext.CancellationToken)
?? throw new InvalidOperationException($"{typeof(TEntity).Name} {arguments.SubjectId} not found");
protected abstract Task<TEntity?> FindAsync(Guid id, CancellationToken cancellationToken);
var previous = Read(entity);
Write(
entity,
new PictureNames(
Path.GetFileName(arguments.OriginalPictureKey),
Path.GetFileName(small),
Path.GetFileName(medium),
Path.GetFileName(large)),
succeeded: true);
await Database.SaveChangesAsync(executeContext.CancellationToken);
/// <summary>Reads the current names; throws when the entity has no pending picture.</summary>
protected abstract PictureNames Read(TEntity entity);
logger.LogInformation("Updated {Entity} {Id} pictures", typeof(TEntity).Name, arguments.SubjectId);
protected abstract void Write(TEntity entity, PictureNames names, bool succeeded);
await publishEndpoint.Publish(ProcessedEvent(arguments.SubjectId), executeContext.CancellationToken);
protected abstract object ProcessedEvent(Guid id);
return executeContext.Completed(new UpdatePicturesLog(
arguments.SubjectId, previous.Original, previous.Small, previous.Medium, previous.Large));
}
public async Task<CompensationResult> Compensate(CompensateContext<UpdatePicturesLog> compensateContext)
{
var log = compensateContext.Log;
try
public async Task<ExecutionResult> Execute(ExecuteContext<UpdatePicturesArguments> executeContext)
{
var entity = await FindAsync(log.SubjectId, compensateContext.CancellationToken);
if (entity == null)
var arguments = executeContext.Arguments;
var small = RequireVariable(executeContext, arguments.SmallPictureVariable);
var medium = RequireVariable(executeContext, arguments.MediumPictureVariable);
var large = RequireVariable(executeContext, arguments.LargePictureVariable);
var entity = await FindAsync(arguments.SubjectId, executeContext.CancellationToken)
?? throw new InvalidOperationException($"{typeof(TEntity).Name} {arguments.SubjectId} not found");
var previous = Read(entity);
Write(
entity,
new PictureNames(
Path.GetFileName(arguments.OriginalPictureKey),
Path.GetFileName(small),
Path.GetFileName(medium),
Path.GetFileName(large)),
succeeded: true);
await Database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Updated {Entity} {Id} pictures", typeof(TEntity).Name, arguments.SubjectId);
await publishEndpoint.Publish(ProcessedEvent(arguments.SubjectId), executeContext.CancellationToken);
return executeContext.Completed(new UpdatePicturesLog(
arguments.SubjectId, previous.Original, previous.Small, previous.Medium, previous.Large));
}
public async Task<CompensationResult> Compensate(CompensateContext<UpdatePicturesLog> compensateContext)
{
var log = compensateContext.Log;
try
{
var entity = await FindAsync(log.SubjectId, compensateContext.CancellationToken);
if (entity == null)
return compensateContext.Compensated();
var previous = new PictureNames(log.PreviousOriginalName, log.PreviousSmallName, log.PreviousMediumName, log.PreviousLargeName);
Write(entity, previous, succeeded: false);
await Database.SaveChangesAsync(compensateContext.CancellationToken);
return compensateContext.Compensated();
var previous = new PictureNames(log.PreviousOriginalName, log.PreviousSmallName, log.PreviousMediumName, log.PreviousLargeName);
Write(entity, previous, succeeded: false);
await Database.SaveChangesAsync(compensateContext.CancellationToken);
return compensateContext.Compensated();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate {Entity} {Id} pictures", typeof(TEntity).Name, log.SubjectId);
return compensateContext.Failed(exception);
}
}
catch (Exception exception)
protected static PictureNames ReadPictures(EntityPictures? pictures) =>
pictures == null
? throw new InvalidOperationException("The entity has no pending picture upload")
: new PictureNames(pictures.OriginalName, pictures.SmallName, pictures.MediumName, pictures.LargeName);
protected static void WritePictures(EntityPictures? pictures, PictureNames names)
{
logger.LogError(exception, "Failed to compensate {Entity} {Id} pictures", typeof(TEntity).Name, log.SubjectId);
return compensateContext.Failed(exception);
if (pictures == null)
return;
pictures.OriginalName = names.Original ?? pictures.OriginalName;
pictures.SmallName = names.Small;
pictures.MediumName = names.Medium;
pictures.LargeName = names.Large;
}
private static string RequireVariable(ExecuteContext<UpdatePicturesArguments> executeContext, string variableName) =>
executeContext.GetVariable<string>(variableName)
?? throw new InvalidOperationException($"Missing routing slip variable {variableName}");
}
protected static PictureNames ReadPictures(EntityPictures? pictures) =>
pictures == null
? throw new InvalidOperationException("The entity has no pending picture upload")
: new PictureNames(pictures.OriginalName, pictures.SmallName, pictures.MediumName, pictures.LargeName);
protected static void WritePictures(EntityPictures? pictures, PictureNames names)
internal sealed class UpdateTrackPictureActivity(IDatabase database, IPublishEndpoint publishEndpoint, ILogger<UpdateTrackPictureActivity> logger)
: UpdatePicturesActivity<Track>(database, publishEndpoint, logger)
{
if (pictures == null)
return;
public const string ExecuteEndpointName = "update-track-picture";
pictures.OriginalName = names.Original ?? pictures.OriginalName;
pictures.SmallName = names.Small;
pictures.MediumName = names.Medium;
pictures.LargeName = names.Large;
protected override Task<Track?> FindAsync(Guid id, CancellationToken cancellationToken) =>
Database.Tracks.SingleOrDefaultAsync(track => track.Id == id, cancellationToken);
protected override PictureNames Read(Track track) =>
new(track.Pictures.OriginalName, track.Pictures.SmallName, track.Pictures.MediumName, track.Pictures.LargeName);
protected override void Write(Track track, PictureNames names, bool succeeded)
{
track.Pictures.OriginalName = names.Original;
track.Pictures.SmallName = names.Small;
track.Pictures.MediumName = names.Medium;
track.Pictures.LargeName = names.Large;
track.Pictures.ProcessingStatus = succeeded ? ProcessingStatus.Completed : ProcessingStatus.Failed;
}
protected override object ProcessedEvent(Guid id) => new TrackPictureProcessed(id);
}
private static string RequireVariable(ExecuteContext<UpdatePicturesArguments> executeContext, string variableName) =>
executeContext.GetVariable<string>(variableName)
?? throw new InvalidOperationException($"Missing routing slip variable {variableName}");
}
internal sealed class UpdateTrackPictureActivity(IDatabase database, IPublishEndpoint publishEndpoint, ILogger<UpdateTrackPictureActivity> logger)
: UpdatePicturesActivity<Track>(database, publishEndpoint, logger)
{
public const string ExecuteEndpointName = "update-track-picture";
protected override Task<Track?> FindAsync(Guid id, CancellationToken cancellationToken) =>
Database.Tracks.SingleOrDefaultAsync(track => track.Id == id, cancellationToken);
protected override PictureNames Read(Track track) =>
new(track.Pictures.OriginalName, track.Pictures.SmallName, track.Pictures.MediumName, track.Pictures.LargeName);
protected override void Write(Track track, PictureNames names, bool succeeded)
internal sealed class UpdatePlayListPictureActivity(IDatabase database, IPublishEndpoint publishEndpoint, ILogger<UpdatePlayListPictureActivity> logger)
: UpdatePicturesActivity<PlayList>(database, publishEndpoint, logger)
{
track.Pictures.OriginalName = names.Original;
track.Pictures.SmallName = names.Small;
track.Pictures.MediumName = names.Medium;
track.Pictures.LargeName = names.Large;
track.Pictures.ProcessingStatus = succeeded ? ProcessingStatus.Completed : ProcessingStatus.Failed;
public const string ExecuteEndpointName = "update-play-list-picture";
protected override Task<PlayList?> FindAsync(Guid id, CancellationToken cancellationToken) =>
Database.PlayLists.SingleOrDefaultAsync(playList => playList.Id == id, cancellationToken);
protected override PictureNames Read(PlayList playList) => ReadPictures(playList.Pictures);
protected override void Write(PlayList playList, PictureNames names, bool succeeded) =>
WritePictures(playList.Pictures, names);
protected override object ProcessedEvent(Guid id) => new PlayListPictureProcessed(id);
}
protected override object ProcessedEvent(Guid id) => new TrackPictureProcessed(id);
}
internal sealed class UpdateAlbumPictureActivity(IDatabase database, IPublishEndpoint publishEndpoint, ILogger<UpdateAlbumPictureActivity> logger)
: UpdatePicturesActivity<Album>(database, publishEndpoint, logger)
{
public const string ExecuteEndpointName = "update-album-picture";
internal sealed class UpdatePlayListPictureActivity(IDatabase database, IPublishEndpoint publishEndpoint, ILogger<UpdatePlayListPictureActivity> logger)
: UpdatePicturesActivity<PlayList>(database, publishEndpoint, logger)
{
public const string ExecuteEndpointName = "update-play-list-picture";
protected override Task<Album?> FindAsync(Guid id, CancellationToken cancellationToken) =>
Database.Albums.SingleOrDefaultAsync(album => album.Id == id, cancellationToken);
protected override Task<PlayList?> FindAsync(Guid id, CancellationToken cancellationToken) =>
Database.PlayLists.SingleOrDefaultAsync(playList => playList.Id == id, cancellationToken);
protected override PictureNames Read(Album album) => ReadPictures(album.Pictures);
protected override PictureNames Read(PlayList playList) => ReadPictures(playList.Pictures);
protected override void Write(Album album, PictureNames names, bool succeeded) =>
WritePictures(album.Pictures, names);
protected override void Write(PlayList playList, PictureNames names, bool succeeded) =>
WritePictures(playList.Pictures, names);
protected override object ProcessedEvent(Guid id) => new PlayListPictureProcessed(id);
}
internal sealed class UpdateAlbumPictureActivity(IDatabase database, IPublishEndpoint publishEndpoint, ILogger<UpdateAlbumPictureActivity> logger)
: UpdatePicturesActivity<Album>(database, publishEndpoint, logger)
{
public const string ExecuteEndpointName = "update-album-picture";
protected override Task<Album?> FindAsync(Guid id, CancellationToken cancellationToken) =>
Database.Albums.SingleOrDefaultAsync(album => album.Id == id, cancellationToken);
protected override PictureNames Read(Album album) => ReadPictures(album.Pictures);
protected override void Write(Album album, PictureNames names, bool succeeded) =>
WritePictures(album.Pictures, names);
protected override object ProcessedEvent(Guid id) => new AlbumPictureProcessed(id);
protected override object ProcessedEvent(Guid id) => new AlbumPictureProcessed(id);
}
}
@@ -3,29 +3,30 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.Activities.PlayLists;
internal class PublishPlayListPictureProcessingEventActivity(
IPublishEndpoint publishEndpoint,
ILogger<PublishPlayListPictureProcessingEventActivity> logger)
: IExecuteActivity<PublishPlayListPictureProcessingEventArguments>
namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
{
public const string ExecuteEndpointName = "publish-play-list-picture-processing-event";
public async Task<ExecutionResult> Execute(ExecuteContext<PublishPlayListPictureProcessingEventArguments> executeContext)
internal class PublishPlayListPictureProcessingEventActivity(
IPublishEndpoint publishEndpoint,
ILogger<PublishPlayListPictureProcessingEventActivity> logger)
: IExecuteActivity<PublishPlayListPictureProcessingEventArguments>
{
var arguments = executeContext.Arguments;
public const string ExecuteEndpointName = "publish-play-list-picture-processing-event";
await publishEndpoint.Publish(
new UpdatePlayListPictureEvent(
arguments.PlayListId,
arguments.Bucket,
arguments.FinalPictureKey,
arguments.Sizes),
executeContext.CancellationToken);
public async Task<ExecutionResult> Execute(ExecuteContext<PublishPlayListPictureProcessingEventArguments> executeContext)
{
var arguments = executeContext.Arguments;
logger.LogInformation("Published picture processing event for playlist {PlayListId}", arguments.PlayListId);
await publishEndpoint.Publish(
new UpdatePlayListPictureEvent(
arguments.PlayListId,
arguments.Bucket,
arguments.FinalPictureKey,
arguments.Sizes),
executeContext.CancellationToken);
return executeContext.Completed();
logger.LogInformation("Published picture processing event for playlist {PlayListId}", arguments.PlayListId);
return executeContext.Completed();
}
}
}
@@ -3,38 +3,39 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.Activities.Tracks;
internal class PublishTrackProcessingEventsActivity(
IPublishEndpoint publishEndpoint,
ILogger<PublishTrackProcessingEventsActivity> logger)
: IExecuteActivity<PublishTrackProcessingEventsArguments>
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
{
public const string ExecuteEndpointName = "publish-track-processing-events";
public async Task<ExecutionResult> Execute(ExecuteContext<PublishTrackProcessingEventsArguments> executeContext)
internal class PublishTrackProcessingEventsActivity(
IPublishEndpoint publishEndpoint,
ILogger<PublishTrackProcessingEventsActivity> logger)
: IExecuteActivity<PublishTrackProcessingEventsArguments>
{
var arguments = executeContext.Arguments;
public const string ExecuteEndpointName = "publish-track-processing-events";
await publishEndpoint.Publish(
new UpdateTrackPictureEvent(
arguments.TrackId,
arguments.Bucket,
arguments.FinalPictureKey,
arguments.Sizes),
executeContext.CancellationToken);
public async Task<ExecutionResult> Execute(ExecuteContext<PublishTrackProcessingEventsArguments> executeContext)
{
var arguments = executeContext.Arguments;
await publishEndpoint.Publish(
new UpdateTrackAudioEvent(
arguments.TrackId,
arguments.Bucket,
arguments.FinalAudioKey,
arguments.Bucket,
arguments.AudioDestinationFolderKey),
executeContext.CancellationToken);
await publishEndpoint.Publish(
new UpdateTrackPictureEvent(
arguments.TrackId,
arguments.Bucket,
arguments.FinalPictureKey,
arguments.Sizes),
executeContext.CancellationToken);
logger.LogInformation("Published processing events for track {TrackId}", arguments.TrackId);
await publishEndpoint.Publish(
new UpdateTrackAudioEvent(
arguments.TrackId,
arguments.Bucket,
arguments.FinalAudioKey,
arguments.Bucket,
arguments.AudioDestinationFolderKey),
executeContext.CancellationToken);
return executeContext.Completed();
logger.LogInformation("Published processing events for track {TrackId}", arguments.TrackId);
return executeContext.Completed();
}
}
}
@@ -6,56 +6,57 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Logs;
namespace Musify.Infrastructure.MassTransit.Activities.UploadIntents;
internal class ConsumeUploadIntentsActivity(
IDatabase database,
ILogger<ConsumeUploadIntentsActivity> logger)
: IActivity<ConsumeUploadIntentsArguments, ConsumeUploadIntentsLog>
namespace Musify.Infrastructure.MassTransit.Activities.UploadIntents
{
public const string ExecuteEndpointName = "consume-upload-intents";
public async Task<ExecutionResult> Execute(ExecuteContext<ConsumeUploadIntentsArguments> executeContext)
internal class ConsumeUploadIntentsActivity(
IDatabase database,
ILogger<ConsumeUploadIntentsActivity> logger)
: IActivity<ConsumeUploadIntentsArguments, ConsumeUploadIntentsLog>
{
var intentIds = executeContext.Arguments.IntentIds;
public const string ExecuteEndpointName = "consume-upload-intents";
var intents = await database.UploadIntents
.Where(i => intentIds.Contains(i.Id))
.ToListAsync(executeContext.CancellationToken);
foreach (var intent in intents)
intent.Status = UploadIntentStatus.Consumed;
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Marked {Count} upload intent(s) as consumed", intents.Count);
return executeContext.Completed(new ConsumeUploadIntentsLog(intentIds));
}
public async Task<CompensationResult> Compensate(CompensateContext<ConsumeUploadIntentsLog> compensateContext)
{
try
public async Task<ExecutionResult> Execute(ExecuteContext<ConsumeUploadIntentsArguments> executeContext)
{
var intentIds = compensateContext.Log.IntentIds;
var intentIds = executeContext.Arguments.IntentIds;
var intents = await database.UploadIntents
.Where(i => intentIds.Contains(i.Id))
.ToListAsync(compensateContext.CancellationToken);
.ToListAsync(executeContext.CancellationToken);
foreach (var intent in intents)
intent.Status = UploadIntentStatus.Issued;
intent.Status = UploadIntentStatus.Consumed;
await database.SaveChangesAsync(compensateContext.CancellationToken);
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Reverted {Count} upload intent(s) back to Issued", intents.Count);
logger.LogInformation("Marked {Count} upload intent(s) as consumed", intents.Count);
return compensateContext.Compensated();
return executeContext.Completed(new ConsumeUploadIntentsLog(intentIds));
}
catch (Exception exception)
public async Task<CompensationResult> Compensate(CompensateContext<ConsumeUploadIntentsLog> compensateContext)
{
logger.LogError(exception, "Failed to compensate upload intent consumption");
return compensateContext.Failed(exception);
try
{
var intentIds = compensateContext.Log.IntentIds;
var intents = await database.UploadIntents
.Where(i => intentIds.Contains(i.Id))
.ToListAsync(compensateContext.CancellationToken);
foreach (var intent in intents)
intent.Status = UploadIntentStatus.Issued;
await database.SaveChangesAsync(compensateContext.CancellationToken);
logger.LogInformation("Reverted {Count} upload intent(s) back to Issued", intents.Count);
return compensateContext.Compensated();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to compensate upload intent consumption");
return compensateContext.Failed(exception);
}
}
}
}
@@ -1,4 +1,5 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record ConsumeUploadIntentsArguments(
Guid[] IntentIds);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record ConsumeUploadIntentsArguments(
Guid[] IntentIds);
}
@@ -1,7 +1,8 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record CopyFileInBucketArguments(
string SourceBucket,
string SourceKey,
string DestinationBucket,
string DestinationKey);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record CopyFileInBucketArguments(
string SourceBucket,
string SourceKey,
string DestinationBucket,
string DestinationKey);
}
@@ -1,3 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
internal record DeleteEntityArguments(Guid Id);
namespace Musify.Infrastructure.MassTransit.Arguments
{
internal record DeleteEntityArguments(Guid Id);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record DownloadFileFromBucketArguments(
string Bucket,
string Key,
string DestinationFilePathVariable);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record DownloadFileFromBucketArguments(
string Bucket,
string Key,
string DestinationFilePathVariable);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record GenerateAudioWorkflowPathsArguments(
Guid TrackId,
string TemporaryRootDirectory,
string SourceKey);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record GenerateAudioWorkflowPathsArguments(
Guid TrackId,
string TemporaryRootDirectory,
string SourceKey);
}
@@ -1,5 +1,6 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record GeneratePictureWorkflowPathsArguments(
string TemporaryRootDirectory,
string SourceKey);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record GeneratePictureWorkflowPathsArguments(
string TemporaryRootDirectory,
string SourceKey);
}
@@ -1,5 +1,6 @@
using Musify.Domain.ValueObjects;
namespace Musify.Infrastructure.MassTransit.Arguments;
internal record MarkLifeCycleArguments(Guid Id, LifeCycleStatus Status);
namespace Musify.Infrastructure.MassTransit.Arguments
{
internal record MarkLifeCycleArguments(Guid Id, LifeCycleStatus Status);
}
@@ -1,9 +1,10 @@
using Musify.Application.Shared;
namespace Musify.Infrastructure.MassTransit.Arguments;
public record PublishAlbumPictureProcessingEventArguments(
Guid AlbumId,
string Bucket,
string FinalPictureKey,
ImageSizes Sizes);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record PublishAlbumPictureProcessingEventArguments(
Guid AlbumId,
string Bucket,
string FinalPictureKey,
ImageSizes Sizes);
}
@@ -1,9 +1,10 @@
using Musify.Application.Shared;
namespace Musify.Infrastructure.MassTransit.Arguments;
public record PublishPlayListPictureProcessingEventArguments(
Guid PlayListId,
string Bucket,
string FinalPictureKey,
ImageSizes Sizes);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record PublishPlayListPictureProcessingEventArguments(
Guid PlayListId,
string Bucket,
string FinalPictureKey,
ImageSizes Sizes);
}
@@ -1,11 +1,12 @@
using Musify.Application.Shared;
namespace Musify.Infrastructure.MassTransit.Arguments;
public record PublishTrackProcessingEventsArguments(
Guid TrackId,
string Bucket,
string FinalPictureKey,
string FinalAudioKey,
string AudioDestinationFolderKey,
ImageSizes Sizes);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record PublishTrackProcessingEventsArguments(
Guid TrackId,
string Bucket,
string FinalPictureKey,
string FinalAudioKey,
string AudioDestinationFolderKey,
ImageSizes Sizes);
}
@@ -1,5 +1,6 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record RemoveFileFromBucketArguments(
string Bucket,
string Key);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record RemoveFileFromBucketArguments(
string Bucket,
string Key);
}
@@ -1,7 +1,8 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record ResizePictureLocalArguments(
string SourceFilePathVariable,
string DestinationFilePathVariable,
int Width,
int Height);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record ResizePictureLocalArguments(
string SourceFilePathVariable,
string DestinationFilePathVariable,
int Width,
int Height);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record TranscodeAudioArguments(
Guid TrackId,
string SourceFilePathVariable,
string WorkingDirectoryVariable);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record TranscodeAudioArguments(
Guid TrackId,
string SourceFilePathVariable,
string WorkingDirectoryVariable);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record TransferFilesToBucketArguments(
string DestinationBucket,
string SourceDirectoryVariable,
string DestinationKey);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record TransferFilesToBucketArguments(
string DestinationBucket,
string SourceDirectoryVariable,
string DestinationKey);
}
@@ -1,8 +1,9 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record UpdatePicturesArguments(
Guid SubjectId,
string OriginalPictureKey,
string SmallPictureVariable,
string MediumPictureVariable,
string LargePictureVariable);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record UpdatePicturesArguments(
Guid SubjectId,
string OriginalPictureKey,
string SmallPictureVariable,
string MediumPictureVariable,
string LargePictureVariable);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record UpdateTrackAudioArguments(
Guid TrackId,
string AudioFolderKey,
string DurationSecondsVariable);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record UpdateTrackAudioArguments(
Guid TrackId,
string AudioFolderKey,
string DurationSecondsVariable);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
public record UploadFileToBucketArguments(
string FilePathVariable,
string DestinationBucket,
string DestinationRoute);
namespace Musify.Infrastructure.MassTransit.Arguments
{
public record UploadFileToBucketArguments(
string FilePathVariable,
string DestinationBucket,
string DestinationRoute);
}
@@ -3,16 +3,17 @@ using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.Activities.LifeCycle;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class AlbumProcessingFailedConsumer(IBus bus) : IConsumer<AlbumProcessingFailed>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<AlbumProcessingFailed> context)
public class AlbumProcessingFailedConsumer(IBus bus) : IConsumer<AlbumProcessingFailed>
{
var message = context.Message;
public Task Consume(ConsumeContext<AlbumProcessingFailed> context)
{
var message = context.Message;
return ProcessingFailedCleanup.ExecuteAsync(
bus, message.AlbumId, ActivityNames.MarkAlbumAsFailed, MarkAlbumLifeCycleActivity.ExecuteEndpointName, message.Bucket,
(ActivityNames.RemoveAlbumOriginalPicture, message.PictureKey));
return ProcessingFailedCleanup.ExecuteAsync(
bus, message.AlbumId, ActivityNames.MarkAlbumAsFailed, MarkAlbumLifeCycleActivity.ExecuteEndpointName, message.Bucket,
(ActivityNames.RemoveAlbumOriginalPicture, message.PictureKey));
}
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class CreateAlbumConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<CreateAlbumResourcesEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<CreateAlbumResourcesEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class CreateAlbumConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<CreateAlbumResourcesEvent>
{
public Task Consume(ConsumeContext<CreateAlbumResourcesEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class CreatePlayListConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<CreatePlayListResourcesEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<CreatePlayListResourcesEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class CreatePlayListConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<CreatePlayListResourcesEvent>
{
public Task Consume(ConsumeContext<CreatePlayListResourcesEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class CreateTrackConsumer(IBus bus, CreateTrackRoutingSlipBuilder routingSlipBuilder)
: IConsumer<CreateTrackResourcesEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<CreateTrackResourcesEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class CreateTrackConsumer(IBus bus, CreateTrackRoutingSlipBuilder routingSlipBuilder)
: IConsumer<CreateTrackResourcesEvent>
{
public Task Consume(ConsumeContext<CreateTrackResourcesEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,17 +2,18 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class DeleteAlbumConsumer(IBus bus, DeleteRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeleteAlbumEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public async Task Consume(ConsumeContext<DeleteAlbumEvent> context)
public class DeleteAlbumConsumer(IBus bus, DeleteRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeleteAlbumEvent>
{
var message = context.Message;
var routingSlip = await routingSlipBuilder.BuildAlbumAsync(
message.AlbumId, message.UserId, context.CorrelationId, context.CancellationToken);
public async Task Consume(ConsumeContext<DeleteAlbumEvent> context)
{
var message = context.Message;
var routingSlip = await routingSlipBuilder.BuildAlbumAsync(
message.AlbumId, message.UserId, context.CorrelationId, context.CancellationToken);
await bus.Execute(routingSlip.Build());
await bus.Execute(routingSlip.Build());
}
}
}
@@ -2,17 +2,18 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class DeletePlayListConsumer(IBus bus, DeleteRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeletePlayListEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public async Task Consume(ConsumeContext<DeletePlayListEvent> context)
public class DeletePlayListConsumer(IBus bus, DeleteRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeletePlayListEvent>
{
var message = context.Message;
var routingSlip = await routingSlipBuilder.BuildPlayListAsync(
message.PlayListId, message.UserId, context.CorrelationId, context.CancellationToken);
public async Task Consume(ConsumeContext<DeletePlayListEvent> context)
{
var message = context.Message;
var routingSlip = await routingSlipBuilder.BuildPlayListAsync(
message.PlayListId, message.UserId, context.CorrelationId, context.CancellationToken);
await bus.Execute(routingSlip.Build());
await bus.Execute(routingSlip.Build());
}
}
}
@@ -2,17 +2,18 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class DeleteTrackConsumer(IBus bus, DeleteRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeleteTrackEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public async Task Consume(ConsumeContext<DeleteTrackEvent> context)
public class DeleteTrackConsumer(IBus bus, DeleteRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeleteTrackEvent>
{
var message = context.Message;
var routingSlip = await routingSlipBuilder.BuildTrackAsync(
message.TrackId, message.UserId, context.CorrelationId, context.CancellationToken);
public async Task Consume(ConsumeContext<DeleteTrackEvent> context)
{
var message = context.Message;
var routingSlip = await routingSlipBuilder.BuildTrackAsync(
message.TrackId, message.UserId, context.CorrelationId, context.CancellationToken);
await bus.Execute(routingSlip.Build());
await bus.Execute(routingSlip.Build());
}
}
}
@@ -3,16 +3,17 @@ using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.Activities.LifeCycle;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class PlayListProcessingFailedConsumer(IBus bus) : IConsumer<PlayListProcessingFailed>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<PlayListProcessingFailed> context)
public class PlayListProcessingFailedConsumer(IBus bus) : IConsumer<PlayListProcessingFailed>
{
var message = context.Message;
public Task Consume(ConsumeContext<PlayListProcessingFailed> context)
{
var message = context.Message;
return ProcessingFailedCleanup.ExecuteAsync(
bus, message.PlayListId, ActivityNames.MarkPlayListAsFailed, MarkPlayListLifeCycleActivity.ExecuteEndpointName, message.Bucket,
(ActivityNames.RemovePlayListOriginalPicture, message.PictureKey));
return ProcessingFailedCleanup.ExecuteAsync(
bus, message.PlayListId, ActivityNames.MarkPlayListAsFailed, MarkPlayListLifeCycleActivity.ExecuteEndpointName, message.Bucket,
(ActivityNames.RemovePlayListOriginalPicture, message.PictureKey));
}
}
}
@@ -3,32 +3,33 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Consumers;
internal static class ProcessingFailedCleanup
namespace Musify.Infrastructure.MassTransit.Consumers
{
/// <summary>Removes whatever files were left behind and marks the entity as failed.</summary>
public static Task ExecuteAsync(
IBus bus,
Guid subjectId,
string markStepName,
string markEndpointName,
string? bucket,
params (string StepName, string? Key)[] files)
internal static class ProcessingFailedCleanup
{
var builder = RoutingSlips.Create(correlationId: null);
if (!string.IsNullOrWhiteSpace(bucket))
/// <summary>Removes whatever files were left behind and marks the entity as failed.</summary>
public static Task ExecuteAsync(
IBus bus,
Guid subjectId,
string markStepName,
string markEndpointName,
string? bucket,
params (string StepName, string? Key)[] files)
{
foreach (var (stepName, key) in files)
var builder = RoutingSlips.Create(correlationId: null);
if (!string.IsNullOrWhiteSpace(bucket))
{
if (!string.IsNullOrWhiteSpace(key))
builder.AddRemoveFile(stepName, bucket, key);
foreach (var (stepName, key) in files)
{
if (!string.IsNullOrWhiteSpace(key))
builder.AddRemoveFile(stepName, bucket, key);
}
}
builder.AddStep(markStepName, markEndpointName, new MarkLifeCycleArguments(subjectId, LifeCycleStatus.Failed));
return bus.Execute(builder.Build());
}
builder.AddStep(markStepName, markEndpointName, new MarkLifeCycleArguments(subjectId, LifeCycleStatus.Failed));
return bus.Execute(builder.Build());
}
}
@@ -3,58 +3,59 @@ using MassTransit.Courier.Contracts;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class ProcessingSlipFaultConsumer : IConsumer<RoutingSlipFaulted>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public const string QueueName = "processing-slip-fault";
public async Task Consume(ConsumeContext<RoutingSlipFaulted> context)
public class ProcessingSlipFaultConsumer : IConsumer<RoutingSlipFaulted>
{
var variables = context.Message.Variables;
public const string QueueName = "processing-slip-fault";
if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.ProcessKind, out var kindObject))
return;
if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.SubjectId, out var subjectObject)
|| !Guid.TryParse(subjectObject?.ToString(), out var subjectId))
public async Task Consume(ConsumeContext<RoutingSlipFaulted> context)
{
return;
var variables = context.Message.Variables;
if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.ProcessKind, out var kindObject))
return;
if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.SubjectId, out var subjectObject)
|| !Guid.TryParse(subjectObject?.ToString(), out var subjectId))
{
return;
}
switch (kindObject?.ToString())
{
case RoutingSlipVariableNames.ProcessKinds.TrackPicture:
await context.Publish(new TrackPictureProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.TrackAudio:
await context.Publish(new TrackAudioProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.PlayListPicture:
await context.Publish(new PlayListPictureProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.AlbumPicture:
await context.Publish(new AlbumPictureProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.TrackCreation:
await context.Publish(new TrackProcessingFailed(
subjectId, Variable(variables, RoutingSlipVariableNames.Workflow.Bucket),
Variable(variables, RoutingSlipVariableNames.Workflow.PictureKey),
Variable(variables, RoutingSlipVariableNames.Workflow.AudioKey)));
break;
case RoutingSlipVariableNames.ProcessKinds.AlbumCreation:
await context.Publish(new AlbumProcessingFailed(
subjectId, Variable(variables, RoutingSlipVariableNames.Workflow.Bucket),
Variable(variables, RoutingSlipVariableNames.Workflow.PictureKey)));
break;
case RoutingSlipVariableNames.ProcessKinds.PlayListCreation:
await context.Publish(new PlayListProcessingFailed(
subjectId, Variable(variables, RoutingSlipVariableNames.Workflow.Bucket),
Variable(variables, RoutingSlipVariableNames.Workflow.PictureKey)));
break;
}
}
switch (kindObject?.ToString())
{
case RoutingSlipVariableNames.ProcessKinds.TrackPicture:
await context.Publish(new TrackPictureProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.TrackAudio:
await context.Publish(new TrackAudioProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.PlayListPicture:
await context.Publish(new PlayListPictureProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.AlbumPicture:
await context.Publish(new AlbumPictureProcessingFailed(subjectId));
break;
case RoutingSlipVariableNames.ProcessKinds.TrackCreation:
await context.Publish(new TrackProcessingFailed(
subjectId, Variable(variables, RoutingSlipVariableNames.Workflow.Bucket),
Variable(variables, RoutingSlipVariableNames.Workflow.PictureKey),
Variable(variables, RoutingSlipVariableNames.Workflow.AudioKey)));
break;
case RoutingSlipVariableNames.ProcessKinds.AlbumCreation:
await context.Publish(new AlbumProcessingFailed(
subjectId, Variable(variables, RoutingSlipVariableNames.Workflow.Bucket),
Variable(variables, RoutingSlipVariableNames.Workflow.PictureKey)));
break;
case RoutingSlipVariableNames.ProcessKinds.PlayListCreation:
await context.Publish(new PlayListProcessingFailed(
subjectId, Variable(variables, RoutingSlipVariableNames.Workflow.Bucket),
Variable(variables, RoutingSlipVariableNames.Workflow.PictureKey)));
break;
}
private static string? Variable(IDictionary<string, object> variables, string name) =>
variables.TryGetValue(name, out var value) ? value?.ToString() : null;
}
private static string? Variable(IDictionary<string, object> variables, string name) =>
variables.TryGetValue(name, out var value) ? value?.ToString() : null;
}
@@ -3,41 +3,42 @@ using MassTransit.Courier.Contracts;
using Microsoft.Extensions.Logging;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class RoutingSlipCleanUpConsumer(
ILogger<RoutingSlipCleanUpConsumer> logger) :
IConsumer<RoutingSlipCompleted>,
IConsumer<RoutingSlipFaulted>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public const string QueueName = "routing-slip-clean-up";
public Task Consume(ConsumeContext<RoutingSlipCompleted> context)
=> DeleteFolder(context.Message.Variables);
public Task Consume(ConsumeContext<RoutingSlipFaulted> context)
=> DeleteFolder(context.Message.Variables);
private Task DeleteFolder(IDictionary<string, object> variables)
public class RoutingSlipCleanUpConsumer(
ILogger<RoutingSlipCleanUpConsumer> logger) :
IConsumer<RoutingSlipCompleted>,
IConsumer<RoutingSlipFaulted>
{
if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.TemporalDirectory, out var pathObject))
return Task.CompletedTask;
public const string QueueName = "routing-slip-clean-up";
var pathString = pathObject.ToString();
public Task Consume(ConsumeContext<RoutingSlipCompleted> context)
=> DeleteFolder(context.Message.Variables);
if (string.IsNullOrEmpty(pathString))
return Task.CompletedTask;
public Task Consume(ConsumeContext<RoutingSlipFaulted> context)
=> DeleteFolder(context.Message.Variables);
try
private Task DeleteFolder(IDictionary<string, object> variables)
{
Directory.Delete(pathString, recursive: true);
logger.LogInformation("Deleted temporal directory at path {Path}", pathString);
}
catch (DirectoryNotFoundException)
{
logger.LogWarning("Temporal directory at path {Path} was not found for deletion", pathString);
}
if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.TemporalDirectory, out var pathObject))
return Task.CompletedTask;
return Task.CompletedTask;
var pathString = pathObject.ToString();
if (string.IsNullOrEmpty(pathString))
return Task.CompletedTask;
try
{
Directory.Delete(pathString, recursive: true);
logger.LogInformation("Deleted temporal directory at path {Path}", pathString);
}
catch (DirectoryNotFoundException)
{
logger.LogWarning("Temporal directory at path {Path} was not found for deletion", pathString);
}
return Task.CompletedTask;
}
}
}
@@ -3,17 +3,18 @@ using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.Activities.LifeCycle;
using Musify.Infrastructure.MassTransit.RoutingSlip;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class TrackProcessingFailedConsumer(IBus bus) : IConsumer<TrackProcessingFailed>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<TrackProcessingFailed> context)
public class TrackProcessingFailedConsumer(IBus bus) : IConsumer<TrackProcessingFailed>
{
var message = context.Message;
public Task Consume(ConsumeContext<TrackProcessingFailed> context)
{
var message = context.Message;
return ProcessingFailedCleanup.ExecuteAsync(
bus, message.TrackId, ActivityNames.MarkTrackAsFailed, MarkTrackLifeCycleActivity.ExecuteEndpointName, message.Bucket,
(ActivityNames.RemoveTrackOriginalPicture, message.PictureKey),
(ActivityNames.RemoveTrackOriginalAudio, message.AudioKey));
return ProcessingFailedCleanup.ExecuteAsync(
bus, message.TrackId, ActivityNames.MarkTrackAsFailed, MarkTrackLifeCycleActivity.ExecuteEndpointName, message.Bucket,
(ActivityNames.RemoveTrackOriginalPicture, message.PictureKey),
(ActivityNames.RemoveTrackOriginalAudio, message.AudioKey));
}
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class UpdateAlbumPictureConsumer(IBus bus, PictureWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateAlbumPictureEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<UpdateAlbumPictureEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class UpdateAlbumPictureConsumer(IBus bus, PictureWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateAlbumPictureEvent>
{
public Task Consume(ConsumeContext<UpdateAlbumPictureEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class UpdateAlbumPictureSourceConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateAlbumPictureSourceEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<UpdateAlbumPictureSourceEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class UpdateAlbumPictureSourceConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateAlbumPictureSourceEvent>
{
public Task Consume(ConsumeContext<UpdateAlbumPictureSourceEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class UpdatePlayListPictureConsumer(IBus bus, PictureWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdatePlayListPictureEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<UpdatePlayListPictureEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class UpdatePlayListPictureConsumer(IBus bus, PictureWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdatePlayListPictureEvent>
{
public Task Consume(ConsumeContext<UpdatePlayListPictureEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class UpdatePlayListPictureSourceConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdatePlayListPictureSourceEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<UpdatePlayListPictureSourceEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class UpdatePlayListPictureSourceConsumer(IBus bus, PictureSourceRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdatePlayListPictureSourceEvent>
{
public Task Consume(ConsumeContext<UpdatePlayListPictureSourceEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class UpdateTrackAudioConsumer(IBus bus, AudioWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateTrackAudioEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<UpdateTrackAudioEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class UpdateTrackAudioConsumer(IBus bus, AudioWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateTrackAudioEvent>
{
public Task Consume(ConsumeContext<UpdateTrackAudioEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -2,11 +2,12 @@ using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class UpdateTrackPictureConsumer(IBus bus, PictureWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateTrackPictureEvent>
namespace Musify.Infrastructure.MassTransit.Consumers
{
public Task Consume(ConsumeContext<UpdateTrackPictureEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
public class UpdateTrackPictureConsumer(IBus bus, PictureWorkflowRoutingSlipBuilder routingSlipBuilder)
: IConsumer<UpdateTrackPictureEvent>
{
public Task Consume(ConsumeContext<UpdateTrackPictureEvent> context) =>
bus.Execute(routingSlipBuilder.Build(context.Message, context.CorrelationId).Build());
}
}
@@ -13,62 +13,63 @@ using Musify.Infrastructure.MassTransit.Consumers;
using Musify.Infrastructure.MassTransit.Logs;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit;
public static partial class MassTransitDependencyInjection
namespace Musify.Infrastructure.MassTransit
{
private static void RegisterRoutingSlipBuilders(IServiceCollection services)
public static partial class MassTransitDependencyInjection
{
services.AddScoped<AudioWorkflowRoutingSlipBuilder>();
services.AddScoped<CreateTrackRoutingSlipBuilder>();
services.AddScoped<DeleteRoutingSlipBuilder>();
services.AddScoped<PictureSourceRoutingSlipBuilder>();
services.AddScoped<PictureWorkflowRoutingSlipBuilder>();
}
private static void RegisterRoutingSlipBuilders(IServiceCollection services)
{
services.AddScoped<AudioWorkflowRoutingSlipBuilder>();
services.AddScoped<CreateTrackRoutingSlipBuilder>();
services.AddScoped<DeleteRoutingSlipBuilder>();
services.AddScoped<PictureSourceRoutingSlipBuilder>();
services.AddScoped<PictureWorkflowRoutingSlipBuilder>();
}
private static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options)
{
options.AddConsumer<AlbumProcessingFailedConsumer>();
options.AddConsumer<CreateAlbumConsumer>();
options.AddConsumer<CreatePlayListConsumer>();
options.AddConsumer<CreateTrackConsumer>();
options.AddConsumer<DeleteAlbumConsumer>();
options.AddConsumer<DeletePlayListConsumer>();
options.AddConsumer<DeleteTrackConsumer>();
options.AddConsumer<PlayListProcessingFailedConsumer>();
options.AddConsumer<ProcessingSlipFaultConsumer>();
options.AddConsumer<RoutingSlipCleanUpConsumer>();
options.AddConsumer<TrackProcessingFailedConsumer>();
options.AddConsumer<UpdateAlbumPictureConsumer>();
options.AddConsumer<UpdateAlbumPictureSourceConsumer>();
options.AddConsumer<UpdatePlayListPictureConsumer>();
options.AddConsumer<UpdatePlayListPictureSourceConsumer>();
options.AddConsumer<UpdateTrackAudioConsumer>();
options.AddConsumer<UpdateTrackPictureConsumer>();
private static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options)
{
options.AddConsumer<AlbumProcessingFailedConsumer>();
options.AddConsumer<CreateAlbumConsumer>();
options.AddConsumer<CreatePlayListConsumer>();
options.AddConsumer<CreateTrackConsumer>();
options.AddConsumer<DeleteAlbumConsumer>();
options.AddConsumer<DeletePlayListConsumer>();
options.AddConsumer<DeleteTrackConsumer>();
options.AddConsumer<PlayListProcessingFailedConsumer>();
options.AddConsumer<ProcessingSlipFaultConsumer>();
options.AddConsumer<RoutingSlipCleanUpConsumer>();
options.AddConsumer<TrackProcessingFailedConsumer>();
options.AddConsumer<UpdateAlbumPictureConsumer>();
options.AddConsumer<UpdateAlbumPictureSourceConsumer>();
options.AddConsumer<UpdatePlayListPictureConsumer>();
options.AddConsumer<UpdatePlayListPictureSourceConsumer>();
options.AddConsumer<UpdateTrackAudioConsumer>();
options.AddConsumer<UpdateTrackPictureConsumer>();
options.AddExecuteActivity<DeleteAlbumActivity, DeleteEntityArguments>();
options.AddExecuteActivity<DeletePlayListActivity, DeleteEntityArguments>();
options.AddExecuteActivity<DeleteTrackActivity, DeleteEntityArguments>();
options.AddExecuteActivity<GenerateAudioWorkflowPathsActivity, GenerateAudioWorkflowPathsArguments>();
options.AddExecuteActivity<GeneratePictureWorkflowPathsActivity, GeneratePictureWorkflowPathsArguments>();
options.AddExecuteActivity<MarkAlbumLifeCycleActivity, MarkLifeCycleArguments>();
options.AddExecuteActivity<MarkPlayListLifeCycleActivity, MarkLifeCycleArguments>();
options.AddExecuteActivity<MarkTrackLifeCycleActivity, MarkLifeCycleArguments>();
options.AddExecuteActivity<PublishAlbumPictureProcessingEventActivity, PublishAlbumPictureProcessingEventArguments>();
options.AddExecuteActivity<PublishPlayListPictureProcessingEventActivity, PublishPlayListPictureProcessingEventArguments>();
options.AddExecuteActivity<PublishTrackProcessingEventsActivity, PublishTrackProcessingEventsArguments>();
options.AddExecuteActivity<RemoveFileFromBucketActivity, RemoveFileFromBucketArguments>();
options.AddExecuteActivity<DeleteAlbumActivity, DeleteEntityArguments>();
options.AddExecuteActivity<DeletePlayListActivity, DeleteEntityArguments>();
options.AddExecuteActivity<DeleteTrackActivity, DeleteEntityArguments>();
options.AddExecuteActivity<GenerateAudioWorkflowPathsActivity, GenerateAudioWorkflowPathsArguments>();
options.AddExecuteActivity<GeneratePictureWorkflowPathsActivity, GeneratePictureWorkflowPathsArguments>();
options.AddExecuteActivity<MarkAlbumLifeCycleActivity, MarkLifeCycleArguments>();
options.AddExecuteActivity<MarkPlayListLifeCycleActivity, MarkLifeCycleArguments>();
options.AddExecuteActivity<MarkTrackLifeCycleActivity, MarkLifeCycleArguments>();
options.AddExecuteActivity<PublishAlbumPictureProcessingEventActivity, PublishAlbumPictureProcessingEventArguments>();
options.AddExecuteActivity<PublishPlayListPictureProcessingEventActivity, PublishPlayListPictureProcessingEventArguments>();
options.AddExecuteActivity<PublishTrackProcessingEventsActivity, PublishTrackProcessingEventsArguments>();
options.AddExecuteActivity<RemoveFileFromBucketActivity, RemoveFileFromBucketArguments>();
options.AddActivity<ConsumeUploadIntentsActivity, ConsumeUploadIntentsArguments, ConsumeUploadIntentsLog>();
options.AddActivity<CopyFileInBucketActivity, CopyFileInBucketArguments, CopyFileInBucketLog>();
options.AddActivity<DownloadFileFromBucketActivity, DownloadFileFromBucketArguments, DownloadFileFromBucketLog>();
options.AddActivity<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>();
options.AddActivity<TranscodeAudioActivity, TranscodeAudioArguments, TranscodeAudioLog>();
options.AddActivity<TransferFilesToBucketActivity, TransferFilesToBucketArguments, TransferFilesToBucketLog>();
options.AddActivity<UpdateAlbumPictureActivity, UpdatePicturesArguments, UpdatePicturesLog>();
options.AddActivity<UpdatePlayListPictureActivity, UpdatePicturesArguments, UpdatePicturesLog>();
options.AddActivity<UpdateTrackAudioActivity, UpdateTrackAudioArguments, UpdateTrackAudioLog>();
options.AddActivity<UpdateTrackPictureActivity, UpdatePicturesArguments, UpdatePicturesLog>();
options.AddActivity<UploadFileToBucketActivity, UploadFileToBucketArguments, UploadFileToBucketLog>();
options.AddActivity<ConsumeUploadIntentsActivity, ConsumeUploadIntentsArguments, ConsumeUploadIntentsLog>();
options.AddActivity<CopyFileInBucketActivity, CopyFileInBucketArguments, CopyFileInBucketLog>();
options.AddActivity<DownloadFileFromBucketActivity, DownloadFileFromBucketArguments, DownloadFileFromBucketLog>();
options.AddActivity<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>();
options.AddActivity<TranscodeAudioActivity, TranscodeAudioArguments, TranscodeAudioLog>();
options.AddActivity<TransferFilesToBucketActivity, TransferFilesToBucketArguments, TransferFilesToBucketLog>();
options.AddActivity<UpdateAlbumPictureActivity, UpdatePicturesArguments, UpdatePicturesLog>();
options.AddActivity<UpdatePlayListPictureActivity, UpdatePicturesArguments, UpdatePicturesLog>();
options.AddActivity<UpdateTrackAudioActivity, UpdateTrackAudioArguments, UpdateTrackAudioLog>();
options.AddActivity<UpdateTrackPictureActivity, UpdatePicturesArguments, UpdatePicturesLog>();
options.AddActivity<UploadFileToBucketActivity, UploadFileToBucketArguments, UploadFileToBucketLog>();
}
}
}
@@ -7,107 +7,108 @@ using Musify.Infrastructure.Configuration;
using Musify.Infrastructure.MassTransit.Sagas;
using Musify.Infrastructure.Persistence;
namespace Musify.Infrastructure.MassTransit;
public static partial class MassTransitDependencyInjection
namespace Musify.Infrastructure.MassTransit
{
public static IServiceCollection AddMassTransitClient(this IServiceCollection services, IConfiguration configuration)
public static partial class MassTransitDependencyInjection
{
services.AddValidatedOptions<MassTransitConfiguration>(configuration);
services.AddScoped<IEventBus, MassTransitEventBus>();
services.AddMassTransit(options =>
public static IServiceCollection AddMassTransitClient(this IServiceCollection services, IConfiguration configuration)
{
AddOutbox(options);
services.AddValidatedOptions<MassTransitConfiguration>(configuration);
services.AddScoped<IEventBus, MassTransitEventBus>();
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
ConfigureRabbitMqHost(
busFactoryConfigurator,
busRegistrationContext.GetRequiredService<MassTransitConfiguration>()));
});
return services;
}
public static IServiceCollection AddMassTransitConsumers(this IServiceCollection services, IConfiguration configuration)
{
services.AddValidatedOptions<MassTransitConfiguration>(configuration);
services.AddValidatedOptions<WorkerConfiguration>(configuration);
services.AddScoped<IEventBus, MassTransitEventBus>();
RegisterRoutingSlipBuilders(services);
services.AddMassTransit(options =>
{
AddOutbox(options);
options.SetKebabCaseEndpointNameFormatter();
RegisterConsumersAndActivities(options);
options.AddSagaStateMachine<TrackProcessingStateMachine, TrackProcessingState>()
.EntityFrameworkRepository(ConfigureSagaRepository<TrackProcessingState>);
options.AddSagaStateMachine<PlayListProcessingStateMachine, PlayListProcessingState>()
.EntityFrameworkRepository(ConfigureSagaRepository<PlayListProcessingState>);
options.AddSagaStateMachine<AlbumProcessingStateMachine, AlbumProcessingState>()
.EntityFrameworkRepository(ConfigureSagaRepository<AlbumProcessingState>);
options.AddConfigureEndpointsCallback((registrationContext, _, endpointConfigurator) =>
services.AddMassTransit(options =>
{
UseStandardRetry(endpointConfigurator);
endpointConfigurator.UseEntityFrameworkOutbox<Database>(registrationContext);
AddOutbox(options);
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
ConfigureRabbitMqHost(
busFactoryConfigurator,
busRegistrationContext.GetRequiredService<MassTransitConfiguration>()));
});
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
return services;
}
public static IServiceCollection AddMassTransitConsumers(this IServiceCollection services, IConfiguration configuration)
{
services.AddValidatedOptions<MassTransitConfiguration>(configuration);
services.AddValidatedOptions<WorkerConfiguration>(configuration);
services.AddScoped<IEventBus, MassTransitEventBus>();
RegisterRoutingSlipBuilders(services);
services.AddMassTransit(options =>
{
ConfigureRabbitMqHost(
busFactoryConfigurator,
busRegistrationContext.GetRequiredService<MassTransitConfiguration>());
AddOutbox(options);
busFactoryConfigurator.ConfigureEndpoints(busRegistrationContext);
options.SetKebabCaseEndpointNameFormatter();
RegisterConsumersAndActivities(options);
options.AddSagaStateMachine<TrackProcessingStateMachine, TrackProcessingState>()
.EntityFrameworkRepository(ConfigureSagaRepository<TrackProcessingState>);
options.AddSagaStateMachine<PlayListProcessingStateMachine, PlayListProcessingState>()
.EntityFrameworkRepository(ConfigureSagaRepository<PlayListProcessingState>);
options.AddSagaStateMachine<AlbumProcessingStateMachine, AlbumProcessingState>()
.EntityFrameworkRepository(ConfigureSagaRepository<AlbumProcessingState>);
options.AddConfigureEndpointsCallback((registrationContext, _, endpointConfigurator) =>
{
UseStandardRetry(endpointConfigurator);
endpointConfigurator.UseEntityFrameworkOutbox<Database>(registrationContext);
});
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
{
ConfigureRabbitMqHost(
busFactoryConfigurator,
busRegistrationContext.GetRequiredService<MassTransitConfiguration>());
busFactoryConfigurator.ConfigureEndpoints(busRegistrationContext);
});
});
});
return services;
}
return services;
}
private static void AddOutbox(IBusRegistrationConfigurator options) =>
options.AddEntityFrameworkOutbox<Database>(outbox =>
private static void AddOutbox(IBusRegistrationConfigurator options) =>
options.AddEntityFrameworkOutbox<Database>(outbox =>
{
outbox.UsePostgres();
outbox.UseBusOutbox();
outbox.QueryDelay = TimeSpan.FromSeconds(1);
outbox.DuplicateDetectionWindow = TimeSpan.FromMinutes(30);
});
private static void ConfigureSagaRepository<TSaga>(IEntityFrameworkSagaRepositoryConfigurator<TSaga> repository)
where TSaga : class, ISaga
{
outbox.UsePostgres();
outbox.UseBusOutbox();
repository.ConcurrencyMode = ConcurrencyMode.Pessimistic;
repository.ExistingDbContext<Database>();
repository.UsePostgres();
}
outbox.QueryDelay = TimeSpan.FromSeconds(1);
outbox.DuplicateDetectionWindow = TimeSpan.FromMinutes(30);
});
private static void UseStandardRetry(IReceiveEndpointConfigurator endpointConfigurator) =>
endpointConfigurator.UseMessageRetry(retryConfigurator =>
retryConfigurator.Exponential(
retryLimit: 5,
minInterval: TimeSpan.FromSeconds(1),
maxInterval: TimeSpan.FromSeconds(30),
intervalDelta: TimeSpan.FromSeconds(2)));
private static void ConfigureSagaRepository<TSaga>(IEntityFrameworkSagaRepositoryConfigurator<TSaga> repository)
where TSaga : class, ISaga
{
repository.ConcurrencyMode = ConcurrencyMode.Pessimistic;
repository.ExistingDbContext<Database>();
repository.UsePostgres();
}
private static void UseStandardRetry(IReceiveEndpointConfigurator endpointConfigurator) =>
endpointConfigurator.UseMessageRetry(retryConfigurator =>
retryConfigurator.Exponential(
retryLimit: 5,
minInterval: TimeSpan.FromSeconds(1),
maxInterval: TimeSpan.FromSeconds(30),
intervalDelta: TimeSpan.FromSeconds(2)));
private static void ConfigureRabbitMqHost(
IRabbitMqBusFactoryConfigurator busFactoryConfigurator,
MassTransitConfiguration massTransitConfiguration)
{
busFactoryConfigurator.Host(massTransitConfiguration.Address, options =>
private static void ConfigureRabbitMqHost(
IRabbitMqBusFactoryConfigurator busFactoryConfigurator,
MassTransitConfiguration massTransitConfiguration)
{
options.Username(massTransitConfiguration.Username);
options.Password(massTransitConfiguration.Password);
});
busFactoryConfigurator.Host(massTransitConfiguration.Address, options =>
{
options.Username(massTransitConfiguration.Username);
options.Password(massTransitConfiguration.Password);
});
}
}
}
@@ -1,4 +1,5 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record ConsumeUploadIntentsLog(
Guid[] IntentIds);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record ConsumeUploadIntentsLog(
Guid[] IntentIds);
}
@@ -1,5 +1,6 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record CopyFileInBucketLog(
string DestinationBucket,
string DestinationKey);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record CopyFileInBucketLog(
string DestinationBucket,
string DestinationKey);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record DownloadFileFromBucketLog(
string Bucket,
string Key,
string DestinationFilePath);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record DownloadFileFromBucketLog(
string Bucket,
string Key,
string DestinationFilePath);
}
@@ -1,3 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record GeneratePictureWorkflowPathsLog(string WorkingDirectory);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record GeneratePictureWorkflowPathsLog(string WorkingDirectory);
}
@@ -1,3 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record ResizePictureLog(string DestinationFilePath);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record ResizePictureLog(string DestinationFilePath);
}
@@ -1,3 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record TranscodeAudioLog(string WorkingDirectory);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record TranscodeAudioLog(string WorkingDirectory);
}
@@ -1,3 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record TransferFilesToBucketLog(string DestinationBucket, string[] UploadedKeys);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record TransferFilesToBucketLog(string DestinationBucket, string[] UploadedKeys);
}
@@ -1,8 +1,9 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record UpdatePicturesLog(
Guid SubjectId,
string? PreviousOriginalName,
string? PreviousSmallName,
string? PreviousMediumName,
string? PreviousLargeName);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record UpdatePicturesLog(
Guid SubjectId,
string? PreviousOriginalName,
string? PreviousSmallName,
string? PreviousMediumName,
string? PreviousLargeName);
}
@@ -1,6 +1,7 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record UpdateTrackAudioLog(
Guid TrackId,
string? PreviousAudioFolderName,
double PreviousDurationSeconds);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record UpdateTrackAudioLog(
Guid TrackId,
string? PreviousAudioFolderName,
double PreviousDurationSeconds);
}
@@ -1,3 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Logs;
public record UploadFileToBucketLog(string DestinationBucket, string DestinationKey);
namespace Musify.Infrastructure.MassTransit.Logs
{
public record UploadFileToBucketLog(string DestinationBucket, string DestinationKey);
}
@@ -1,11 +1,12 @@
using MassTransit;
using Musify.Application.Contracts;
namespace Musify.Infrastructure.MassTransit;
public class MassTransitEventBus(IPublishEndpoint publishEndpoint)
: IEventBus
namespace Musify.Infrastructure.MassTransit
{
public Task PublishAsync<T>(T message, CancellationToken cancellationToken) where T : class =>
publishEndpoint.Publish(message, cancellationToken);
public class MassTransitEventBus(IPublishEndpoint publishEndpoint)
: IEventBus
{
public Task PublishAsync<T>(T message, CancellationToken cancellationToken) where T : class =>
publishEndpoint.Publish(message, cancellationToken);
}
}
@@ -1,62 +1,63 @@
namespace Musify.Infrastructure.MassTransit.RoutingSlip;
internal static class ActivityNames
namespace Musify.Infrastructure.MassTransit.RoutingSlip
{
internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving";
internal const string MarkTrackAsFailed = "MarkTrackAsFailed";
internal const string DeleteTrackFromDb = "DeleteTrackFromDb";
internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving";
internal const string MarkPlayListAsFailed = "MarkPlayListAsFailed";
internal const string DeletePlayListFromDb = "DeletePlayListFromDb";
internal const string MarkAlbumAsFailed = "MarkAlbumAsFailed";
internal const string MarkAlbumAsRemoving = "MarkAlbumAsRemoving";
internal const string DeleteAlbumFromDb = "DeleteAlbumFromDb";
internal static class ActivityNames
{
internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving";
internal const string MarkTrackAsFailed = "MarkTrackAsFailed";
internal const string DeleteTrackFromDb = "DeleteTrackFromDb";
internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving";
internal const string MarkPlayListAsFailed = "MarkPlayListAsFailed";
internal const string DeletePlayListFromDb = "DeletePlayListFromDb";
internal const string MarkAlbumAsFailed = "MarkAlbumAsFailed";
internal const string MarkAlbumAsRemoving = "MarkAlbumAsRemoving";
internal const string DeleteAlbumFromDb = "DeleteAlbumFromDb";
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
internal const string DownloadFile = "DownloadFile";
internal const string DownloadFile = "DownloadFile";
internal const string TranscodeAudio = "TranscodeAudio";
internal const string TranscodeAudio = "TranscodeAudio";
internal const string ResizeSmall = "ResizeSmall";
internal const string ResizeMedium = "ResizeMedium";
internal const string ResizeLarge = "ResizeLarge";
internal const string ResizeSmall = "ResizeSmall";
internal const string ResizeMedium = "ResizeMedium";
internal const string ResizeLarge = "ResizeLarge";
internal const string UploadSmall = "UploadSmall";
internal const string UploadMedium = "UploadMedium";
internal const string UploadLarge = "UploadLarge";
internal const string UploadSmall = "UploadSmall";
internal const string UploadMedium = "UploadMedium";
internal const string UploadLarge = "UploadLarge";
internal const string TransferFiles = "TransferFiles";
internal const string TransferFiles = "TransferFiles";
internal const string UpdateTrackPicture = "UpdateTrackPicture";
internal const string UpdatePlayListPicture = "UpdatePlayListPicture";
internal const string UpdateAlbumPicture = "UpdateAlbumPicture";
internal const string UpdateTrackAudio = "UpdateTrackAudio";
internal const string UpdateTrackPicture = "UpdateTrackPicture";
internal const string UpdatePlayListPicture = "UpdatePlayListPicture";
internal const string UpdateAlbumPicture = "UpdateAlbumPicture";
internal const string UpdateTrackAudio = "UpdateTrackAudio";
internal const string CopyPictureToFinal = "CopyPictureToFinal";
internal const string CopyAudioToFinal = "CopyAudioToFinal";
internal const string CopyPictureToFinal = "CopyPictureToFinal";
internal const string CopyAudioToFinal = "CopyAudioToFinal";
internal const string ConsumeUploadIntents = "ConsumeUploadIntents";
internal const string ConsumeUploadIntents = "ConsumeUploadIntents";
internal const string PublishTrackProcessingEvents = "PublishTrackProcessingEvents";
internal const string PublishPlayListPictureProcessingEvent = "PublishPlayListPictureProcessingEvent";
internal const string PublishAlbumPictureProcessingEvent = "PublishAlbumPictureProcessingEvent";
internal const string PublishTrackProcessingEvents = "PublishTrackProcessingEvents";
internal const string PublishPlayListPictureProcessingEvent = "PublishPlayListPictureProcessingEvent";
internal const string PublishAlbumPictureProcessingEvent = "PublishAlbumPictureProcessingEvent";
internal const string RemoveTrackOriginalPicture = "RemoveTrackOriginalPicture";
internal const string RemoveTrackSmallPicture = "RemoveTrackSmallPicture";
internal const string RemoveTrackMediumPicture = "RemoveTrackMediumPicture";
internal const string RemoveTrackLargePicture = "RemoveTrackLargePicture";
internal const string RemoveTrackOriginalAudio = "RemoveTrackOriginalAudio";
internal const string RemoveTrackProcessedAudio = "RemoveTrackProcessedAudio";
internal const string RemoveTrackOriginalPicture = "RemoveTrackOriginalPicture";
internal const string RemoveTrackSmallPicture = "RemoveTrackSmallPicture";
internal const string RemoveTrackMediumPicture = "RemoveTrackMediumPicture";
internal const string RemoveTrackLargePicture = "RemoveTrackLargePicture";
internal const string RemoveTrackOriginalAudio = "RemoveTrackOriginalAudio";
internal const string RemoveTrackProcessedAudio = "RemoveTrackProcessedAudio";
internal const string RemovePlayListOriginalPicture = "RemovePlayListOriginalPicture";
internal const string RemovePlayListSmallPicture = "RemovePlayListSmallPicture";
internal const string RemovePlayListMediumPicture = "RemovePlayListMediumPicture";
internal const string RemovePlayListLargePicture = "RemovePlayListLargePicture";
internal const string RemovePlayListOriginalPicture = "RemovePlayListOriginalPicture";
internal const string RemovePlayListSmallPicture = "RemovePlayListSmallPicture";
internal const string RemovePlayListMediumPicture = "RemovePlayListMediumPicture";
internal const string RemovePlayListLargePicture = "RemovePlayListLargePicture";
internal const string RemoveAlbumOriginalPicture = "RemoveAlbumOriginalPicture";
internal const string RemoveAlbumSmallPicture = "RemoveAlbumSmallPicture";
internal const string RemoveAlbumMediumPicture = "RemoveAlbumMediumPicture";
internal const string RemoveAlbumLargePicture = "RemoveAlbumLargePicture";
internal const string RemoveAlbumOriginalPicture = "RemoveAlbumOriginalPicture";
internal const string RemoveAlbumSmallPicture = "RemoveAlbumSmallPicture";
internal const string RemoveAlbumMediumPicture = "RemoveAlbumMediumPicture";
internal const string RemoveAlbumLargePicture = "RemoveAlbumLargePicture";
}
}
@@ -5,40 +5,41 @@ using Musify.Infrastructure.MassTransit.Activities.Audio;
using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(UpdateTrackAudioEvent message, Guid? correlationId) =>
RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GenerateAudioWorkflowPaths,
GenerateAudioWorkflowPathsActivity.ExecuteEndpointName,
new GenerateAudioWorkflowPathsArguments(
message.TrackId, workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(
message.SourceBucket, message.SourceKey, RoutingSlipVariableNames.Audio.SourceFilePath))
.AddStep(
ActivityNames.TranscodeAudio,
TranscodeAudioActivity.ExecuteEndpointName,
new TranscodeAudioArguments(
message.TrackId,
RoutingSlipVariableNames.Audio.SourceFilePath,
RoutingSlipVariableNames.Workflow.TemporalDirectory))
.AddStep(
ActivityNames.TransferFiles,
TransferFilesToBucketActivity.ExecuteEndpointName,
new TransferFilesToBucketArguments(
message.DestinationBucket,
RoutingSlipVariableNames.Audio.TranscodedDirectory,
message.DestinationFolderKey))
.AddStep(
ActivityNames.UpdateTrackAudio,
UpdateTrackAudioActivity.ExecuteEndpointName,
new UpdateTrackAudioArguments(
message.TrackId, message.DestinationFolderKey, RoutingSlipVariableNames.Audio.DurationSeconds))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackAudio);
public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
{
public RoutingSlipBuilder Build(UpdateTrackAudioEvent message, Guid? correlationId) =>
RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GenerateAudioWorkflowPaths,
GenerateAudioWorkflowPathsActivity.ExecuteEndpointName,
new GenerateAudioWorkflowPathsArguments(
message.TrackId, workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(
message.SourceBucket, message.SourceKey, RoutingSlipVariableNames.Audio.SourceFilePath))
.AddStep(
ActivityNames.TranscodeAudio,
TranscodeAudioActivity.ExecuteEndpointName,
new TranscodeAudioArguments(
message.TrackId,
RoutingSlipVariableNames.Audio.SourceFilePath,
RoutingSlipVariableNames.Workflow.TemporalDirectory))
.AddStep(
ActivityNames.TransferFiles,
TransferFilesToBucketActivity.ExecuteEndpointName,
new TransferFilesToBucketArguments(
message.DestinationBucket,
RoutingSlipVariableNames.Audio.TranscodedDirectory,
message.DestinationFolderKey))
.AddStep(
ActivityNames.UpdateTrackAudio,
UpdateTrackAudioActivity.ExecuteEndpointName,
new UpdateTrackAudioArguments(
message.TrackId, message.DestinationFolderKey, RoutingSlipVariableNames.Audio.DurationSeconds))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackAudio);
}
}
@@ -5,41 +5,42 @@ using Musify.Infrastructure.MassTransit.Activities.Tracks;
using Musify.Infrastructure.MassTransit.Activities.UploadIntents;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
public class CreateTrackRoutingSlipBuilder
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(CreateTrackResourcesEvent message, Guid? correlationId)
public class CreateTrackRoutingSlipBuilder
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.PictureSourceKey, message.Bucket, message.PictureDestinationKey))
.AddStep(
ActivityNames.CopyAudioToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.AudioSourceKey, message.Bucket, message.AudioDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([message.PictureIntentId, message.AudioIntentId]))
.AddStep(
ActivityNames.PublishTrackProcessingEvents,
PublishTrackProcessingEventsActivity.ExecuteEndpointName,
new PublishTrackProcessingEventsArguments(
message.TrackId,
message.Bucket,
message.PictureDestinationKey,
message.AudioDestinationKey,
message.AudioProcessedFolderKey,
message.Sizes))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackCreation);
public RoutingSlipBuilder Build(CreateTrackResourcesEvent message, Guid? correlationId)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.PictureSourceKey, message.Bucket, message.PictureDestinationKey))
.AddStep(
ActivityNames.CopyAudioToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.AudioSourceKey, message.Bucket, message.AudioDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([message.PictureIntentId, message.AudioIntentId]))
.AddStep(
ActivityNames.PublishTrackProcessingEvents,
PublishTrackProcessingEventsActivity.ExecuteEndpointName,
new PublishTrackProcessingEventsArguments(
message.TrackId,
message.Bucket,
message.PictureDestinationKey,
message.AudioDestinationKey,
message.AudioProcessedFolderKey,
message.Sizes))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackCreation);
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, message.Bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, message.PictureDestinationKey);
builder.AddVariable(RoutingSlipVariableNames.Workflow.AudioKey, message.AudioDestinationKey);
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, message.Bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, message.PictureDestinationKey);
builder.AddVariable(RoutingSlipVariableNames.Workflow.AudioKey, message.AudioDestinationKey);
return builder;
return builder;
}
}
}
@@ -6,130 +6,131 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Activities.LifeCycle;
using Musify.Infrastructure.MassTransit.Arguments;
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(
IDatabase database,
TrackConfiguration trackConfiguration,
AlbumConfiguration albumConfiguration,
PlayListConfiguration playListConfiguration,
ApplicationStorageConfiguration storageConfiguration)
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
private sealed record PictureStepNames(string Original, string Small, string Medium, string Large);
private static readonly PictureStepNames AlbumSteps = new(
ActivityNames.RemoveAlbumOriginalPicture, ActivityNames.RemoveAlbumSmallPicture,
ActivityNames.RemoveAlbumMediumPicture, ActivityNames.RemoveAlbumLargePicture);
private static readonly PictureStepNames PlayListSteps = new(
ActivityNames.RemovePlayListOriginalPicture, ActivityNames.RemovePlayListSmallPicture,
ActivityNames.RemovePlayListMediumPicture, ActivityNames.RemovePlayListLargePicture);
public async Task<RoutingSlipBuilder> BuildTrackAsync(
Guid trackId, long userId, Guid? correlationId, CancellationToken cancellationToken)
/// <summary>Marks an entity as removing, deletes its files from the bucket and finally deletes its row.</summary>
public class DeleteRoutingSlipBuilder(
IDatabase database,
TrackConfiguration trackConfiguration,
AlbumConfiguration albumConfiguration,
PlayListConfiguration playListConfiguration,
ApplicationStorageConfiguration storageConfiguration)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkTrackAsRemoving,
MarkTrackLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(trackId, LifeCycleStatus.Removing));
private sealed record PictureStepNames(string Original, string Small, string Medium, string Large);
var track = await database.Tracks.AsNoTracking().SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track != null)
private static readonly PictureStepNames AlbumSteps = new(
ActivityNames.RemoveAlbumOriginalPicture, ActivityNames.RemoveAlbumSmallPicture,
ActivityNames.RemoveAlbumMediumPicture, ActivityNames.RemoveAlbumLargePicture);
private static readonly PictureStepNames PlayListSteps = new(
ActivityNames.RemovePlayListOriginalPicture, ActivityNames.RemovePlayListSmallPicture,
ActivityNames.RemovePlayListMediumPicture, ActivityNames.RemovePlayListLargePicture);
public async Task<RoutingSlipBuilder> BuildTrackAsync(
Guid trackId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var bucket = storageConfiguration.Bucket;
var routes = trackConfiguration.Routes;
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkTrackAsRemoving,
MarkTrackLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(trackId, LifeCycleStatus.Removing));
if (!string.IsNullOrEmpty(track.Pictures.OriginalName))
var track = await database.Tracks.AsNoTracking().SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track != null)
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalPicture, bucket,
routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName));
var bucket = storageConfiguration.Bucket;
var routes = trackConfiguration.Routes;
if (!string.IsNullOrEmpty(track.Pictures.OriginalName))
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalPicture, bucket,
routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName));
}
// Unprocessed tracks still point at the shared preset pictures, which must never be deleted.
if (track.Pictures.IsProcessed)
{
builder.AddRemoveFile(ActivityNames.RemoveTrackSmallPicture, bucket, routes.BuildPicturePath(PictureSize.Small, track.Pictures.SmallName));
builder.AddRemoveFile(ActivityNames.RemoveTrackMediumPicture, bucket, routes.BuildPicturePath(PictureSize.Medium, track.Pictures.MediumName));
builder.AddRemoveFile(ActivityNames.RemoveTrackLargePicture, bucket, routes.BuildPicturePath(PictureSize.Large, track.Pictures.LargeName));
}
if (!string.IsNullOrEmpty(track.Audio.OriginalName))
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalAudio, bucket,
routes.BuildOriginalAudioPath(userId, track.Audio.OriginalName));
}
if (track.Audio.IsProcessed)
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackProcessedAudio, bucket,
routes.BuildProcessedAudioPath(track.Audio.FolderName));
}
}
// Unprocessed tracks still point at the shared preset pictures, which must never be deleted.
if (track.Pictures.IsProcessed)
{
builder.AddRemoveFile(ActivityNames.RemoveTrackSmallPicture, bucket, routes.BuildPicturePath(PictureSize.Small, track.Pictures.SmallName));
builder.AddRemoveFile(ActivityNames.RemoveTrackMediumPicture, bucket, routes.BuildPicturePath(PictureSize.Medium, track.Pictures.MediumName));
builder.AddRemoveFile(ActivityNames.RemoveTrackLargePicture, bucket, routes.BuildPicturePath(PictureSize.Large, track.Pictures.LargeName));
}
if (!string.IsNullOrEmpty(track.Audio.OriginalName))
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalAudio, bucket,
routes.BuildOriginalAudioPath(userId, track.Audio.OriginalName));
}
if (track.Audio.IsProcessed)
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackProcessedAudio, bucket,
routes.BuildProcessedAudioPath(track.Audio.FolderName));
}
return builder.AddStep(
ActivityNames.DeleteTrackFromDb, DeleteTrackActivity.ExecuteEndpointName, new DeleteEntityArguments(trackId));
}
return builder.AddStep(
ActivityNames.DeleteTrackFromDb, DeleteTrackActivity.ExecuteEndpointName, new DeleteEntityArguments(trackId));
}
public async Task<RoutingSlipBuilder> BuildAlbumAsync(
Guid albumId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkAlbumAsRemoving,
MarkAlbumLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(albumId, LifeCycleStatus.Removing));
public async Task<RoutingSlipBuilder> BuildAlbumAsync(
Guid albumId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkAlbumAsRemoving,
MarkAlbumLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(albumId, LifeCycleStatus.Removing));
var pictures = await database.Albums.AsNoTracking()
.Where(a => a.Id == albumId)
.Select(a => a.Pictures)
.SingleOrDefaultAsync(cancellationToken);
var pictures = await database.Albums.AsNoTracking()
.Where(a => a.Id == albumId)
.Select(a => a.Pictures)
.SingleOrDefaultAsync(cancellationToken);
AddPictureRemovals(builder, AlbumSteps, albumConfiguration.Routes, userId, pictures);
AddPictureRemovals(builder, AlbumSteps, albumConfiguration.Routes, userId, pictures);
return builder.AddStep(
ActivityNames.DeleteAlbumFromDb, DeleteAlbumActivity.ExecuteEndpointName, new DeleteEntityArguments(albumId));
}
return builder.AddStep(
ActivityNames.DeleteAlbumFromDb, DeleteAlbumActivity.ExecuteEndpointName, new DeleteEntityArguments(albumId));
}
public async Task<RoutingSlipBuilder> BuildPlayListAsync(
Guid playListId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkPlayListAsRemoving,
MarkPlayListLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(playListId, LifeCycleStatus.Removing));
public async Task<RoutingSlipBuilder> BuildPlayListAsync(
Guid playListId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkPlayListAsRemoving,
MarkPlayListLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(playListId, LifeCycleStatus.Removing));
var pictures = await database.PlayLists.AsNoTracking()
.Where(p => p.Id == playListId)
.Select(p => p.Pictures)
.SingleOrDefaultAsync(cancellationToken);
var pictures = await database.PlayLists.AsNoTracking()
.Where(p => p.Id == playListId)
.Select(p => p.Pictures)
.SingleOrDefaultAsync(cancellationToken);
AddPictureRemovals(builder, PlayListSteps, playListConfiguration.Routes, userId, pictures);
AddPictureRemovals(builder, PlayListSteps, playListConfiguration.Routes, userId, pictures);
return builder.AddStep(
ActivityNames.DeletePlayListFromDb, DeletePlayListActivity.ExecuteEndpointName, new DeleteEntityArguments(playListId));
}
return builder.AddStep(
ActivityNames.DeletePlayListFromDb, DeletePlayListActivity.ExecuteEndpointName, new DeleteEntityArguments(playListId));
}
private void AddPictureRemovals(
RoutingSlipBuilder builder, PictureStepNames steps, PictureRoutes routes, long userId, EntityPictures? pictures)
{
if (pictures == null)
return;
private void AddPictureRemovals(
RoutingSlipBuilder builder, PictureStepNames steps, PictureRoutes routes, long userId, EntityPictures? pictures)
{
if (pictures == null)
return;
var bucket = storageConfiguration.Bucket;
var bucket = storageConfiguration.Bucket;
if (pictures.OriginalName is { } original)
builder.AddRemoveFile(steps.Original, bucket, routes.BuildOriginalPicturePath(userId, original));
if (pictures.SmallName is { } small)
builder.AddRemoveFile(steps.Small, bucket, routes.BuildPicturePath(PictureSize.Small, small));
if (pictures.MediumName is { } medium)
builder.AddRemoveFile(steps.Medium, bucket, routes.BuildPicturePath(PictureSize.Medium, medium));
if (pictures.LargeName is { } large)
builder.AddRemoveFile(steps.Large, bucket, routes.BuildPicturePath(PictureSize.Large, large));
if (pictures.OriginalName is { } original)
builder.AddRemoveFile(steps.Original, bucket, routes.BuildOriginalPicturePath(userId, original));
if (pictures.SmallName is { } small)
builder.AddRemoveFile(steps.Small, bucket, routes.BuildPicturePath(PictureSize.Small, small));
if (pictures.MediumName is { } medium)
builder.AddRemoveFile(steps.Medium, bucket, routes.BuildPicturePath(PictureSize.Medium, medium));
if (pictures.LargeName is { } large)
builder.AddRemoveFile(steps.Large, bucket, routes.BuildPicturePath(PictureSize.Large, large));
}
}
}
@@ -6,69 +6,70 @@ using Musify.Infrastructure.MassTransit.Activities.PlayLists;
using Musify.Infrastructure.MassTransit.Activities.UploadIntents;
using Musify.Infrastructure.MassTransit.Arguments;
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
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(CreateAlbumResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumCreation,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(UpdateAlbumPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumPicture,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(CreatePlayListResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListCreation,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(UpdatePlayListPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListPicture,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
private static RoutingSlipBuilder BuildSlip(
Guid subjectId,
Guid pictureIntentId,
string bucket,
string pictureSourceKey,
string pictureDestinationKey,
Guid? correlationId,
string processKind,
string publishStepName,
string publishEndpointName,
object publishArguments)
/// <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
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(bucket, pictureSourceKey, bucket, pictureDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([pictureIntentId]))
.AddStep(publishStepName, publishEndpointName, publishArguments)
.TrackFaults(subjectId, processKind);
public RoutingSlipBuilder Build(CreateAlbumResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumCreation,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, pictureDestinationKey);
public RoutingSlipBuilder Build(UpdateAlbumPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumPicture,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
return builder;
public RoutingSlipBuilder Build(CreatePlayListResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListCreation,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(UpdatePlayListPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListPicture,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
private static RoutingSlipBuilder BuildSlip(
Guid subjectId,
Guid pictureIntentId,
string bucket,
string pictureSourceKey,
string pictureDestinationKey,
Guid? correlationId,
string processKind,
string publishStepName,
string publishEndpointName,
object publishArguments)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(bucket, pictureSourceKey, bucket, pictureDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([pictureIntentId]))
.AddStep(publishStepName, publishEndpointName, publishArguments)
.TrackFaults(subjectId, processKind);
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, pictureDestinationKey);
return builder;
}
}
}
@@ -6,81 +6,82 @@ using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Activities.Pictures;
using Musify.Infrastructure.MassTransit.Arguments;
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)
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.TrackId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateTrackPicture, UpdateTrackPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.TrackPicture);
public RoutingSlipBuilder Build(UpdatePlayListPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.PlayListId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdatePlayListPicture, UpdatePlayListPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.PlayListPicture);
public RoutingSlipBuilder Build(UpdateAlbumPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.AlbumId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateAlbumPicture, UpdateAlbumPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.AlbumPicture);
private RoutingSlipBuilder BuildPictureWorkflow(
Guid subjectId,
string bucket,
string sourceKey,
ImageSizes sizes,
Guid? correlationId,
string updateStepName,
string updateEndpointName,
string processKind)
/// <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)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GeneratePictureWorkflowPaths,
GeneratePictureWorkflowPathsActivity.ExecuteEndpointName,
new GeneratePictureWorkflowPathsArguments(workerConfiguration.Routes.TemporaryFilesDirectory, sourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(bucket, sourceKey, RoutingSlipVariableNames.Picture.OriginalFilePath));
public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.TrackId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateTrackPicture, UpdateTrackPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.TrackPicture);
AddResize(builder, ActivityNames.ResizeSmall, ActivityNames.UploadSmall, bucket, RoutingSlipVariableNames.Picture.SmallResizedFilePath, sizes.Small);
AddResize(builder, ActivityNames.ResizeMedium, ActivityNames.UploadMedium, bucket, RoutingSlipVariableNames.Picture.MediumResizedFilePath, sizes.Medium);
AddResize(builder, ActivityNames.ResizeLarge, ActivityNames.UploadLarge, bucket, RoutingSlipVariableNames.Picture.LargeResizedFilePath, sizes.Large);
public RoutingSlipBuilder Build(UpdatePlayListPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.PlayListId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdatePlayListPicture, UpdatePlayListPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.PlayListPicture);
return builder
.AddStep(
updateStepName,
updateEndpointName,
new UpdatePicturesArguments(
subjectId,
sourceKey,
RoutingSlipVariableNames.Picture.SmallResizedFilePath,
RoutingSlipVariableNames.Picture.MediumResizedFilePath,
RoutingSlipVariableNames.Picture.LargeResizedFilePath))
.TrackFaults(subjectId, processKind);
public RoutingSlipBuilder Build(UpdateAlbumPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.AlbumId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateAlbumPicture, UpdateAlbumPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.AlbumPicture);
private RoutingSlipBuilder BuildPictureWorkflow(
Guid subjectId,
string bucket,
string sourceKey,
ImageSizes sizes,
Guid? correlationId,
string updateStepName,
string updateEndpointName,
string processKind)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GeneratePictureWorkflowPaths,
GeneratePictureWorkflowPathsActivity.ExecuteEndpointName,
new GeneratePictureWorkflowPathsArguments(workerConfiguration.Routes.TemporaryFilesDirectory, sourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(bucket, sourceKey, RoutingSlipVariableNames.Picture.OriginalFilePath));
AddResize(builder, ActivityNames.ResizeSmall, ActivityNames.UploadSmall, bucket, RoutingSlipVariableNames.Picture.SmallResizedFilePath, sizes.Small);
AddResize(builder, ActivityNames.ResizeMedium, ActivityNames.UploadMedium, bucket, RoutingSlipVariableNames.Picture.MediumResizedFilePath, sizes.Medium);
AddResize(builder, ActivityNames.ResizeLarge, ActivityNames.UploadLarge, bucket, RoutingSlipVariableNames.Picture.LargeResizedFilePath, sizes.Large);
return builder
.AddStep(
updateStepName,
updateEndpointName,
new UpdatePicturesArguments(
subjectId,
sourceKey,
RoutingSlipVariableNames.Picture.SmallResizedFilePath,
RoutingSlipVariableNames.Picture.MediumResizedFilePath,
RoutingSlipVariableNames.Picture.LargeResizedFilePath))
.TrackFaults(subjectId, processKind);
}
private static void AddResize(
RoutingSlipBuilder builder,
string resizeStepName,
string uploadStepName,
string bucket,
string resizedFileVariable,
ImageSize size) =>
builder
.AddStep(
resizeStepName,
ResizePictureActivity.ExecuteEndpointName,
new ResizePictureLocalArguments(
RoutingSlipVariableNames.Picture.OriginalFilePath, resizedFileVariable, size.Width, size.Height))
.AddStep(
uploadStepName,
UploadFileToBucketActivity.ExecuteEndpointName,
new UploadFileToBucketArguments(resizedFileVariable, bucket, size.SavePath));
}
private static void AddResize(
RoutingSlipBuilder builder,
string resizeStepName,
string uploadStepName,
string bucket,
string resizedFileVariable,
ImageSize size) =>
builder
.AddStep(
resizeStepName,
ResizePictureActivity.ExecuteEndpointName,
new ResizePictureLocalArguments(
RoutingSlipVariableNames.Picture.OriginalFilePath, resizedFileVariable, size.Width, size.Height))
.AddStep(
uploadStepName,
UploadFileToBucketActivity.ExecuteEndpointName,
new UploadFileToBucketArguments(resizedFileVariable, bucket, size.SavePath));
}
@@ -1,41 +1,42 @@
namespace Musify.Infrastructure.MassTransit.RoutingSlip;
internal static class RoutingSlipVariableNames
namespace Musify.Infrastructure.MassTransit.RoutingSlip
{
internal static class Workflow
internal static class RoutingSlipVariableNames
{
internal const string TemporalDirectory = "Workflow.TemporalDirectory";
internal const string CorrelationId = "Workflow.CorrelationId";
internal const string SubjectId = "Workflow.SubjectId";
internal const string ProcessKind = "Workflow.ProcessKind";
internal const string Bucket = "Workflow.Bucket";
internal const string PictureKey = "Workflow.PictureKey";
internal const string AudioKey = "Workflow.AudioKey";
}
internal static class Workflow
{
internal const string TemporalDirectory = "Workflow.TemporalDirectory";
internal const string CorrelationId = "Workflow.CorrelationId";
internal const string SubjectId = "Workflow.SubjectId";
internal const string ProcessKind = "Workflow.ProcessKind";
internal const string Bucket = "Workflow.Bucket";
internal const string PictureKey = "Workflow.PictureKey";
internal const string AudioKey = "Workflow.AudioKey";
}
internal static class ProcessKinds
{
internal const string TrackPicture = "TrackPicture";
internal const string TrackAudio = "TrackAudio";
internal const string PlayListPicture = "PlayListPicture";
internal const string AlbumPicture = "AlbumPicture";
internal const string TrackCreation = "TrackCreation";
internal const string AlbumCreation = "AlbumCreation";
internal const string PlayListCreation = "PlayListCreation";
}
internal static class ProcessKinds
{
internal const string TrackPicture = "TrackPicture";
internal const string TrackAudio = "TrackAudio";
internal const string PlayListPicture = "PlayListPicture";
internal const string AlbumPicture = "AlbumPicture";
internal const string TrackCreation = "TrackCreation";
internal const string AlbumCreation = "AlbumCreation";
internal const string PlayListCreation = "PlayListCreation";
}
internal static class Audio
{
internal const string SourceFilePath = "Audio.SourceFilePath";
internal const string TranscodedDirectory = "Audio.TranscodedDirectory";
internal const string DurationSeconds = "Audio.DurationSeconds";
}
internal static class Audio
{
internal const string SourceFilePath = "Audio.SourceFilePath";
internal const string TranscodedDirectory = "Audio.TranscodedDirectory";
internal const string DurationSeconds = "Audio.DurationSeconds";
}
internal static class Picture
{
internal const string OriginalFilePath = "Picture.OriginalFilePath";
internal const string SmallResizedFilePath = "Picture.SmallResizedFilePath";
internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath";
internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath";
internal static class Picture
{
internal const string OriginalFilePath = "Picture.OriginalFilePath";
internal const string SmallResizedFilePath = "Picture.SmallResizedFilePath";
internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath";
internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath";
}
}
}
@@ -4,45 +4,46 @@ using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Consumers;
namespace Musify.Infrastructure.MassTransit.RoutingSlip;
/// <summary>Shared building blocks for every routing slip in the worker.</summary>
internal static class RoutingSlips
namespace Musify.Infrastructure.MassTransit.RoutingSlip
{
public static Uri ExecuteUri(string endpointName) => new($"queue:{endpointName}_execute");
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)
/// <summary>Shared building blocks for every routing slip in the worker.</summary>
internal static class RoutingSlips
{
var builder = new RoutingSlipBuilder(NewId.NextGuid());
builder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
builder.AddSubscription(
ConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
public static Uri ExecuteUri(string endpointName) => new($"queue:{endpointName}_execute");
return builder;
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)
{
var builder = new RoutingSlipBuilder(NewId.NextGuid());
builder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
builder.AddSubscription(
ConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
return builder;
}
public static RoutingSlipBuilder AddStep(
this RoutingSlipBuilder builder, string name, string executeEndpointName, object arguments)
{
builder.AddActivity(name, ExecuteUri(executeEndpointName), arguments);
return builder;
}
/// <summary>Publishes a failure event for the subject when the slip faults.</summary>
public static RoutingSlipBuilder TrackFaults(
this RoutingSlipBuilder builder, Guid subjectId, string processKind)
{
builder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, subjectId);
builder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, processKind);
builder.AddSubscription(ConsumerUri(ProcessingSlipFaultConsumer.QueueName), RoutingSlipEvents.Faulted);
return builder;
}
public static RoutingSlipBuilder AddRemoveFile(
this RoutingSlipBuilder builder, string stepName, string bucket, string key) =>
builder.AddStep(stepName, RemoveFileFromBucketActivity.ExecuteEndpointName, new RemoveFileFromBucketArguments(bucket, key));
}
public static RoutingSlipBuilder AddStep(
this RoutingSlipBuilder builder, string name, string executeEndpointName, object arguments)
{
builder.AddActivity(name, ExecuteUri(executeEndpointName), arguments);
return builder;
}
/// <summary>Publishes a failure event for the subject when the slip faults.</summary>
public static RoutingSlipBuilder TrackFaults(
this RoutingSlipBuilder builder, Guid subjectId, string processKind)
{
builder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, subjectId);
builder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, processKind);
builder.AddSubscription(ConsumerUri(ProcessingSlipFaultConsumer.QueueName), RoutingSlipEvents.Faulted);
return builder;
}
public static RoutingSlipBuilder AddRemoveFile(
this RoutingSlipBuilder builder, string stepName, string bucket, string key) =>
builder.AddStep(stepName, RemoveFileFromBucketActivity.ExecuteEndpointName, new RemoveFileFromBucketArguments(bucket, key));
}
@@ -1,18 +1,19 @@
using MassTransit;
namespace Musify.Infrastructure.MassTransit.Sagas;
public class AlbumProcessingState : SagaStateMachineInstance
namespace Musify.Infrastructure.MassTransit.Sagas
{
public Guid CorrelationId { get; set; }
public class AlbumProcessingState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = null!;
public string CurrentState { get; set; } = null!;
public string? Bucket { get; set; }
public string? Bucket { get; set; }
public string? PictureKey { get; set; }
public string? PictureKey { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
}
@@ -1,58 +1,59 @@
using MassTransit;
using Musify.Application.Events;
namespace Musify.Infrastructure.MassTransit.Sagas;
public sealed class AlbumProcessingStateMachine : MassTransitStateMachine<AlbumProcessingState>
namespace Musify.Infrastructure.MassTransit.Sagas
{
public State Processing { get; private set; } = null!;
public State Failed { get; private set; } = null!;
public Event<CreateAlbumResourcesEvent> ProcessingStarted { get; private set; } = null!;
public Event<AlbumPictureProcessed> PictureProcessed { get; private set; } = null!;
public Event<AlbumPictureProcessingFailed> PictureFailed { get; private set; } = null!;
public AlbumProcessingStateMachine()
public sealed class AlbumProcessingStateMachine : MassTransitStateMachine<AlbumProcessingState>
{
InstanceState(state => state.CurrentState);
public State Processing { get; private set; } = null!;
public State Failed { get; private set; } = null!;
Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.AlbumId));
Event(() => PictureProcessed, config =>
public Event<CreateAlbumResourcesEvent> ProcessingStarted { get; private set; } = null!;
public Event<AlbumPictureProcessed> PictureProcessed { get; private set; } = null!;
public Event<AlbumPictureProcessingFailed> PictureFailed { get; private set; } = null!;
public AlbumProcessingStateMachine()
{
config.CorrelateById(context => context.Message.AlbumId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => PictureFailed, config =>
{
config.CorrelateById(context => context.Message.AlbumId);
config.OnMissingInstance(instance => instance.Discard());
});
InstanceState(state => state.CurrentState);
Initially(
When(ProcessingStarted)
.Then(context =>
{
context.Saga.CreatedAt = DateTime.UtcNow;
context.Saga.UpdatedAt = DateTime.UtcNow;
context.Saga.Bucket = context.Message.Bucket;
context.Saga.PictureKey = context.Message.PictureDestinationKey;
})
.TransitionTo(Processing));
Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.AlbumId));
Event(() => PictureProcessed, config =>
{
config.CorrelateById(context => context.Message.AlbumId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => PictureFailed, config =>
{
config.CorrelateById(context => context.Message.AlbumId);
config.OnMissingInstance(instance => instance.Discard());
});
During(Processing,
When(PictureProcessed).Finalize(),
When(PictureFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new AlbumProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey)));
Initially(
When(ProcessingStarted)
.Then(context =>
{
context.Saga.CreatedAt = DateTime.UtcNow;
context.Saga.UpdatedAt = DateTime.UtcNow;
context.Saga.Bucket = context.Message.Bucket;
context.Saga.PictureKey = context.Message.PictureDestinationKey;
})
.TransitionTo(Processing));
During(Failed,
Ignore(PictureProcessed),
Ignore(PictureFailed));
During(Processing,
When(PictureProcessed).Finalize(),
When(PictureFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new AlbumProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey)));
SetCompletedWhenFinalized();
During(Failed,
Ignore(PictureProcessed),
Ignore(PictureFailed));
SetCompletedWhenFinalized();
}
}
}
@@ -1,18 +1,19 @@
using MassTransit;
namespace Musify.Infrastructure.MassTransit.Sagas;
public class PlayListProcessingState : SagaStateMachineInstance
namespace Musify.Infrastructure.MassTransit.Sagas
{
public Guid CorrelationId { get; set; }
public class PlayListProcessingState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = null!;
public string CurrentState { get; set; } = null!;
public string? Bucket { get; set; }
public string? Bucket { get; set; }
public string? PictureKey { get; set; }
public string? PictureKey { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
}
@@ -1,58 +1,59 @@
using MassTransit;
using Musify.Application.Events;
namespace Musify.Infrastructure.MassTransit.Sagas;
public sealed class PlayListProcessingStateMachine : MassTransitStateMachine<PlayListProcessingState>
namespace Musify.Infrastructure.MassTransit.Sagas
{
public State Processing { get; private set; } = null!;
public State Failed { get; private set; } = null!;
public Event<CreatePlayListResourcesEvent> ProcessingStarted { get; private set; } = null!;
public Event<PlayListPictureProcessed> PictureProcessed { get; private set; } = null!;
public Event<PlayListPictureProcessingFailed> PictureFailed { get; private set; } = null!;
public PlayListProcessingStateMachine()
public sealed class PlayListProcessingStateMachine : MassTransitStateMachine<PlayListProcessingState>
{
InstanceState(state => state.CurrentState);
public State Processing { get; private set; } = null!;
public State Failed { get; private set; } = null!;
Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.PlayListId));
Event(() => PictureProcessed, config =>
public Event<CreatePlayListResourcesEvent> ProcessingStarted { get; private set; } = null!;
public Event<PlayListPictureProcessed> PictureProcessed { get; private set; } = null!;
public Event<PlayListPictureProcessingFailed> PictureFailed { get; private set; } = null!;
public PlayListProcessingStateMachine()
{
config.CorrelateById(context => context.Message.PlayListId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => PictureFailed, config =>
{
config.CorrelateById(context => context.Message.PlayListId);
config.OnMissingInstance(instance => instance.Discard());
});
InstanceState(state => state.CurrentState);
Initially(
When(ProcessingStarted)
.Then(context =>
{
context.Saga.CreatedAt = DateTime.UtcNow;
context.Saga.UpdatedAt = DateTime.UtcNow;
context.Saga.Bucket = context.Message.Bucket;
context.Saga.PictureKey = context.Message.PictureDestinationKey;
})
.TransitionTo(Processing));
Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.PlayListId));
Event(() => PictureProcessed, config =>
{
config.CorrelateById(context => context.Message.PlayListId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => PictureFailed, config =>
{
config.CorrelateById(context => context.Message.PlayListId);
config.OnMissingInstance(instance => instance.Discard());
});
During(Processing,
When(PictureProcessed).Finalize(),
When(PictureFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new PlayListProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey)));
Initially(
When(ProcessingStarted)
.Then(context =>
{
context.Saga.CreatedAt = DateTime.UtcNow;
context.Saga.UpdatedAt = DateTime.UtcNow;
context.Saga.Bucket = context.Message.Bucket;
context.Saga.PictureKey = context.Message.PictureDestinationKey;
})
.TransitionTo(Processing));
During(Failed,
Ignore(PictureProcessed),
Ignore(PictureFailed));
During(Processing,
When(PictureProcessed).Finalize(),
When(PictureFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new PlayListProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey)));
SetCompletedWhenFinalized();
During(Failed,
Ignore(PictureProcessed),
Ignore(PictureFailed));
SetCompletedWhenFinalized();
}
}
}
@@ -1,24 +1,25 @@
using MassTransit;
namespace Musify.Infrastructure.MassTransit.Sagas;
public class TrackProcessingState : SagaStateMachineInstance
namespace Musify.Infrastructure.MassTransit.Sagas
{
public Guid CorrelationId { get; set; }
public class TrackProcessingState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = null!;
public string CurrentState { get; set; } = null!;
public bool PictureProcessed { get; set; }
public bool PictureProcessed { get; set; }
public bool AudioProcessed { get; set; }
public bool AudioProcessed { get; set; }
public string? Bucket { get; set; }
public string? Bucket { get; set; }
public string? PictureKey { get; set; }
public string? PictureKey { get; set; }
public string? AudioKey { get; set; }
public string? AudioKey { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
}
@@ -1,95 +1,96 @@
using MassTransit;
using Musify.Application.Events;
namespace Musify.Infrastructure.MassTransit.Sagas;
public sealed class TrackProcessingStateMachine : MassTransitStateMachine<TrackProcessingState>
namespace Musify.Infrastructure.MassTransit.Sagas
{
public State Processing { get; private set; } = null!;
public State Failed { get; private set; } = null!;
public Event<CreateTrackResourcesEvent> ProcessingStarted { get; private set; } = null!;
public Event<TrackPictureProcessed> PictureProcessed { get; private set; } = null!;
public Event<TrackAudioProcessed> AudioProcessed { get; private set; } = null!;
public Event<TrackPictureProcessingFailed> PictureFailed { get; private set; } = null!;
public Event<TrackAudioProcessingFailed> AudioFailed { get; private set; } = null!;
public TrackProcessingStateMachine()
public sealed class TrackProcessingStateMachine : MassTransitStateMachine<TrackProcessingState>
{
InstanceState(state => state.CurrentState);
public State Processing { get; private set; } = null!;
public State Failed { get; private set; } = null!;
Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.TrackId));
Event(() => PictureProcessed, config =>
public Event<CreateTrackResourcesEvent> ProcessingStarted { get; private set; } = null!;
public Event<TrackPictureProcessed> PictureProcessed { get; private set; } = null!;
public Event<TrackAudioProcessed> AudioProcessed { get; private set; } = null!;
public Event<TrackPictureProcessingFailed> PictureFailed { get; private set; } = null!;
public Event<TrackAudioProcessingFailed> AudioFailed { get; private set; } = null!;
public TrackProcessingStateMachine()
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => AudioProcessed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => PictureFailed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => AudioFailed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
InstanceState(state => state.CurrentState);
Initially(
When(ProcessingStarted)
.Then(context =>
{
context.Saga.CreatedAt = DateTime.UtcNow;
context.Saga.UpdatedAt = DateTime.UtcNow;
context.Saga.Bucket = context.Message.Bucket;
context.Saga.PictureKey = context.Message.PictureDestinationKey;
context.Saga.AudioKey = context.Message.AudioDestinationKey;
})
.TransitionTo(Processing));
Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.TrackId));
Event(() => PictureProcessed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => AudioProcessed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => PictureFailed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
Event(() => AudioFailed, config =>
{
config.CorrelateById(context => context.Message.TrackId);
config.OnMissingInstance(instance => instance.Discard());
});
During(Processing,
When(PictureProcessed)
.Then(context =>
{
context.Saga.PictureProcessed = true;
context.Saga.UpdatedAt = DateTime.UtcNow;
})
.If(context => context.Saga.AudioProcessed, binder => binder.Finalize()),
When(AudioProcessed)
.Then(context =>
{
context.Saga.AudioProcessed = true;
context.Saga.UpdatedAt = DateTime.UtcNow;
})
.If(context => context.Saga.PictureProcessed, binder => binder.Finalize()),
When(PictureFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new TrackProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey,
context.Saga.AudioKey)),
When(AudioFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new TrackProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey,
context.Saga.AudioKey)));
Initially(
When(ProcessingStarted)
.Then(context =>
{
context.Saga.CreatedAt = DateTime.UtcNow;
context.Saga.UpdatedAt = DateTime.UtcNow;
context.Saga.Bucket = context.Message.Bucket;
context.Saga.PictureKey = context.Message.PictureDestinationKey;
context.Saga.AudioKey = context.Message.AudioDestinationKey;
})
.TransitionTo(Processing));
During(Failed,
Ignore(PictureProcessed),
Ignore(AudioProcessed),
Ignore(PictureFailed),
Ignore(AudioFailed));
During(Processing,
When(PictureProcessed)
.Then(context =>
{
context.Saga.PictureProcessed = true;
context.Saga.UpdatedAt = DateTime.UtcNow;
})
.If(context => context.Saga.AudioProcessed, binder => binder.Finalize()),
When(AudioProcessed)
.Then(context =>
{
context.Saga.AudioProcessed = true;
context.Saga.UpdatedAt = DateTime.UtcNow;
})
.If(context => context.Saga.PictureProcessed, binder => binder.Finalize()),
When(PictureFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new TrackProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey,
context.Saga.AudioKey)),
When(AudioFailed)
.Then(context => context.Saga.UpdatedAt = DateTime.UtcNow)
.TransitionTo(Failed)
.Publish(context => new TrackProcessingFailed(
context.Saga.CorrelationId,
context.Saga.Bucket,
context.Saga.PictureKey,
context.Saga.AudioKey)));
SetCompletedWhenFinalized();
During(Failed,
Ignore(PictureProcessed),
Ignore(AudioProcessed),
Ignore(PictureFailed),
Ignore(AudioFailed));
SetCompletedWhenFinalized();
}
}
}