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
@@ -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();
}
}