Experimental changes
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
|
||||
namespace Musify.Infrastructure.Audio
|
||||
{
|
||||
public static class AudioTranscoderDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors)
|
||||
{
|
||||
serviceDescriptors.AddScoped<IAudioTranscoder, AudioTranscoder>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Musify.Infrastructure.Configuration
|
||||
{
|
||||
public static class ConfigurationDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructureConfigurations(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddValidatedConfiguration<DatabaseConfiguration>(configuration, DatabaseConfiguration.SectionName)
|
||||
.AddValidatedConfiguration<KeycloakConfiguration>(configuration, KeycloakConfiguration.SectionName)
|
||||
.AddValidatedConfiguration<InfrastructureStorageConfiguration>(configuration, InfrastructureStorageConfiguration.SectionName)
|
||||
.AddValidatedConfiguration<MessagingConfiguration>(configuration, MessagingConfiguration.SectionName)
|
||||
.AddValidatedConfiguration<OpenTelemetryConfiguration>(configuration, OpenTelemetryConfiguration.SectionName)
|
||||
.AddValidatedConfiguration<AudioTranscoderConfiguration>(configuration, AudioTranscoderConfiguration.SectionName)
|
||||
.AddValidatedConfiguration<WorkerConfiguration>(configuration, WorkerConfiguration.SectionName);
|
||||
|
||||
return serviceDescriptors;
|
||||
}
|
||||
|
||||
static IServiceCollection AddValidatedConfiguration<TConfiguration>(
|
||||
this IServiceCollection serviceDescriptors,
|
||||
IConfiguration configuration,
|
||||
string sectionName)
|
||||
where TConfiguration : class
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddOptionsWithValidateOnStart<TConfiguration>()
|
||||
.Bind(configuration.GetRequiredSection(sectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<TConfiguration>>().Value);
|
||||
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Musify.Infrastructure.Configuration
|
||||
{
|
||||
public class MessagingConfiguration
|
||||
public class MassTransitConfiguration
|
||||
{
|
||||
public const string SectionName = "Messaging";
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Keycloak.Net;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Identity
|
||||
{
|
||||
public static class KeycloakDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddKeycloakService(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors.AddOptionsWithValidateOnStart<KeycloakConfiguration>()
|
||||
.Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
serviceDescriptors.AddScoped(serviceProvider =>
|
||||
{
|
||||
var keycloakConfiguration = serviceProvider.GetRequiredService<KeycloakConfiguration>();
|
||||
|
||||
return new KeycloakClient(
|
||||
keycloakConfiguration.BaseUrl,
|
||||
keycloakConfiguration.Username,
|
||||
keycloakConfiguration.Password);
|
||||
} );
|
||||
serviceDescriptors.AddScoped<IKeycloakUserService, KeycloakUserService>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Keycloak.Net.Models.Users;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Infrastructure.Identity
|
||||
{
|
||||
public static class KeycloakUserMapper
|
||||
{
|
||||
public static KeycloakUserResponse Map(User keycloakUser)
|
||||
{
|
||||
return new KeycloakUserResponse(
|
||||
Guid.Parse(keycloakUser.Id ?? throw new InvalidOperationException("Keycloak user Id is required.")),
|
||||
keycloakUser.UserName ?? throw new InvalidOperationException("Keycloak user Name is required."),
|
||||
keycloakUser.FirstName ?? throw new InvalidOperationException("Keycloak user FirstName is required."),
|
||||
keycloakUser.LastName ?? throw new InvalidOperationException("Keycloak user SecondName is required."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Infrastructure.Audio;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
using Musify.Infrastructure.Identity;
|
||||
using Musify.Infrastructure.MassTransit;
|
||||
using Musify.Infrastructure.Observability;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Musify.Infrastructure.Pictures;
|
||||
using Musify.Infrastructure.Storage;
|
||||
|
||||
namespace Musify.Infrastructure
|
||||
{
|
||||
public static class InfrastructureDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructureModules(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddInfrastructureConfigurations(configuration)
|
||||
.AddDatabase(configuration)
|
||||
.AddKeycloakService(configuration)
|
||||
.AddStorageHandler()
|
||||
.AddPictureHandler()
|
||||
.AddAudioTranscoder()
|
||||
.AddInfrastructureOpenTelemetry()
|
||||
.AddMassTransitClient()
|
||||
.AddScoped<IEventBus, MassTransitEventBus>()
|
||||
.AddScoped<IProcessTrackingStore, ProcessTrackingStore>();
|
||||
|
||||
return serviceDescriptors;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddInfrastructureWorkerModules(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddInfrastructureConfigurations(configuration)
|
||||
.AddDatabase(configuration)
|
||||
.AddStorageHandler()
|
||||
.AddPictureHandler()
|
||||
.AddAudioTranscoder()
|
||||
.AddInfrastructureOpenTelemetry()
|
||||
.AddMassTransitConsumers()
|
||||
.AddScoped<IEventBus, MassTransitEventBus>()
|
||||
.AddScoped<IProcessTrackingStore, ProcessTrackingStore>();
|
||||
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Logs;
|
||||
namespace Musify.Infrastructure.MassTransit.Activities
|
||||
{
|
||||
public class TranscodeDashAudioActivity(
|
||||
IAudioTranscoder audioTranscoder,
|
||||
IAudioTranscoderService audioTranscoder,
|
||||
ILogger<TranscodeDashAudioActivity> logger,
|
||||
IProcessTrackingStore processTrackingStore)
|
||||
: IActivity<TranscodeDashAudioArguments, TranscodeDashAudioLog>
|
||||
@@ -40,18 +40,10 @@ namespace Musify.Infrastructure.MassTransit.Activities
|
||||
{
|
||||
await using (var fileStream = File.OpenRead(sourceFilePath))
|
||||
{
|
||||
var result = await audioTranscoder.TranscodeToDashAsync(
|
||||
await audioTranscoder.TranscodeToDashAsync(
|
||||
fileStream,
|
||||
workingDirectory,
|
||||
executeContext.CancellationToken);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
var errorMessage = string.Join("; ", result.Errors);
|
||||
logger.LogError("Transcoding failed for {SourceFilePath}: {Errors}",
|
||||
sourceFilePath, errorMessage);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
File.Delete(sourceFilePath);
|
||||
|
||||
@@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Logs;
|
||||
namespace Musify.Infrastructure.MassTransit.Activities
|
||||
{
|
||||
public class DownloadFileFromBucketActivity(
|
||||
IStorageHandler storageHandler,
|
||||
IStorageService storageHandler,
|
||||
ILogger<DownloadFileFromBucketActivity> logger,
|
||||
IProcessTrackingStore processTrackingStore)
|
||||
: IActivity<DownloadFileFromBucketArguments, DownloadFileFromBucketLog>
|
||||
@@ -36,26 +36,15 @@ namespace Musify.Infrastructure.MassTransit.Activities
|
||||
|
||||
try
|
||||
{
|
||||
var getFile = await storageHandler.GetFileAsync(
|
||||
using var fileStream = await storageHandler.GetFileAsync(
|
||||
executeContext.Arguments.Bucket,
|
||||
executeContext.Arguments.Key,
|
||||
executeContext.CancellationToken);
|
||||
|
||||
if (!getFile.IsSuccess)
|
||||
{
|
||||
var errorMessage = string.Join("; ", getFile.Errors);
|
||||
logger.LogWarning("Failed to download {Bucket}/{Key}: {Errors}",
|
||||
executeContext.Arguments.Bucket,
|
||||
executeContext.Arguments.Key,
|
||||
errorMessage);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
var destinationDirectory = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrWhiteSpace(destinationDirectory))
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
|
||||
using var fileStream = getFile.Value;
|
||||
if (fileStream.CanSeek)
|
||||
fileStream.Position = 0;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using Musify.Infrastructure.MassTransit.Activities.Arguments;
|
||||
namespace Musify.Infrastructure.MassTransit.Activities
|
||||
{
|
||||
public class RemoveFileFromBucketActivity(
|
||||
IStorageHandler storageHandler,
|
||||
IStorageService storageHandler,
|
||||
ILogger<RemoveFileFromBucketActivity> logger,
|
||||
IProcessTrackingStore processTrackingStore)
|
||||
: IExecuteActivity<RemoveFileFromBucketArguments>
|
||||
@@ -32,20 +32,11 @@ namespace Musify.Infrastructure.MassTransit.Activities
|
||||
|
||||
try
|
||||
{
|
||||
var removeFile = await storageHandler.RemoveFileAsync(
|
||||
await storageHandler.RemoveFileAsync(
|
||||
executeContext.Arguments.Bucket,
|
||||
executeContext.Arguments.Key,
|
||||
executeContext.CancellationToken);
|
||||
|
||||
if (!removeFile.IsSuccess)
|
||||
{
|
||||
var errorMessage = string.Join("; ", removeFile.Errors);
|
||||
logger.LogWarning("Failed to remove {Key} from bucket {Bucket}: {Errors}",
|
||||
executeContext.Arguments.Key,
|
||||
executeContext.Arguments.Bucket,
|
||||
errorMessage);
|
||||
}
|
||||
|
||||
await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken);
|
||||
return executeContext.Completed();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Logs;
|
||||
namespace Musify.Infrastructure.MassTransit.Activities
|
||||
{
|
||||
public class TransferFilesToBucketActivity(
|
||||
IStorageHandler storageHandler,
|
||||
IStorageService storageHandler,
|
||||
ILogger<TransferFilesToBucketActivity> logger,
|
||||
IProcessTrackingStore processTrackingStore)
|
||||
: IActivity<TransferFilesToBucketArguments, TransferFilesToBucketLog>
|
||||
@@ -42,23 +42,12 @@ namespace Musify.Infrastructure.MassTransit.Activities
|
||||
.Select(file => Path.Combine(destinationKey, Path.GetFileName(file)))
|
||||
.ToArray();
|
||||
|
||||
var transferFilesResult = await storageHandler.TransferFilesAsync(
|
||||
await storageHandler.TransferFilesAsync(
|
||||
folderPath,
|
||||
executeContext.Arguments.DestinationBucket,
|
||||
destinationKey,
|
||||
executeContext.CancellationToken);
|
||||
|
||||
if (!transferFilesResult.IsSuccess)
|
||||
{
|
||||
var errorMessage = string.Join("; ", transferFilesResult.Errors);
|
||||
logger.LogWarning("Failed to transfer files from {FolderPath} to {DestinationBucket}/{DestinationKey}: {Errors}",
|
||||
folderPath,
|
||||
executeContext.Arguments.DestinationBucket,
|
||||
destinationKey,
|
||||
errorMessage);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken);
|
||||
return executeContext.Completed();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Logs;
|
||||
|
||||
namespace Musify.Infrastructure.MassTransit.Activities.Files
|
||||
{
|
||||
public class UploadFileToBucketActivity(IStorageHandler storageHandler,
|
||||
public class UploadFileToBucketActivity(IStorageService storageHandler,
|
||||
ILogger<UploadFileToBucketActivity> logger,
|
||||
IProcessTrackingStore processTrackingStore)
|
||||
: IActivity<UploadFileToBucketArguments, UploadFileToBucketLog>
|
||||
@@ -37,22 +37,12 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files
|
||||
|
||||
try
|
||||
{
|
||||
var uploadResult = await storageHandler.UploadFileAsync(
|
||||
await storageHandler.UploadFileAsync(
|
||||
sourceFilePath,
|
||||
executeContext.Arguments.DestinationBucket,
|
||||
Path.Combine(executeContext.Arguments.DestinationRoute, fileName),
|
||||
executeContext.CancellationToken);
|
||||
|
||||
if (!uploadResult.IsSuccess)
|
||||
{
|
||||
var errors = string.Join("; ", uploadResult.Errors);
|
||||
logger.LogWarning("Failed to upload file to bucket {Bucket}/{Route}: {Errors}",
|
||||
executeContext.Arguments.DestinationBucket,
|
||||
executeContext.Arguments.DestinationRoute,
|
||||
errors);
|
||||
throw new Exception(errors);
|
||||
}
|
||||
|
||||
await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken);
|
||||
return executeContext.CompletedWithVariables(new UploadFileToBucketLog(
|
||||
executeContext.Arguments.DestinationBucket,
|
||||
@@ -70,19 +60,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files
|
||||
{
|
||||
try
|
||||
{
|
||||
var uploadResult = await storageHandler.RemoveFileAsync(
|
||||
await storageHandler.RemoveFileAsync(
|
||||
compensateContext.Log.DestinationBucket,
|
||||
compensateContext.Log.DestinationKey,
|
||||
compensateContext.CancellationToken);
|
||||
|
||||
if (!uploadResult.IsSuccess)
|
||||
{
|
||||
var errors = string.Join("; ", uploadResult.Errors);
|
||||
logger.LogError("Failed to remove file from bucket during compensation. Bucket: {Bucket}, Key: {Key}, Errors: {Errors}",
|
||||
compensateContext.Log.DestinationBucket, compensateContext.Log.DestinationKey, errors);
|
||||
|
||||
return compensateContext.Failed(new Exception($"Failed to remove file from bucket during compensation. Bucket: {compensateContext.Log.DestinationBucket}, Key: {compensateContext.Log.DestinationKey}, Errors: {errors}"));
|
||||
}
|
||||
return compensateContext.Compensated();
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
||||
@@ -9,7 +9,7 @@ using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
|
||||
namespace Musify.Infrastructure.MassTransit.Activities
|
||||
{
|
||||
public class ResizePictureActivity(
|
||||
IPictureHandler pictureHandler,
|
||||
IPictureService pictureHandler,
|
||||
ILogger<ResizePictureActivity> logger,
|
||||
IProcessTrackingStore processTrackingStore)
|
||||
: IActivity<ResizePictureLocalArguments, ResizePictureLog>
|
||||
@@ -53,23 +53,12 @@ namespace Musify.Infrastructure.MassTransit.Activities
|
||||
executeContext.Arguments.Height,
|
||||
executeContext.CancellationToken);
|
||||
|
||||
if (!resizedPicture.IsSuccess)
|
||||
{
|
||||
var errorMessage = string.Join("; ", resizedPicture.Errors);
|
||||
logger.LogError("Failed to resize picture {SourceFilePath} to {Width}x{Height}: {Errors}",
|
||||
sourceFilePath,
|
||||
executeContext.Arguments.Width,
|
||||
executeContext.Arguments.Height,
|
||||
errorMessage);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
var destinationDirectory = Path.GetDirectoryName(destinationFilePath);
|
||||
if (!string.IsNullOrEmpty(destinationDirectory))
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
|
||||
await using var destinationStream = File.Create(destinationFilePath);
|
||||
await resizedPicture.Value.CopyToAsync(destinationStream, executeContext.CancellationToken);
|
||||
await resizedPicture.CopyToAsync(destinationStream, executeContext.CancellationToken);
|
||||
|
||||
logger.LogDebug("Resized picture from {SourceFilePath} to {DestinationFilePath}",
|
||||
sourceFilePath,
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Musify.Infrastructure.MassTransit
|
||||
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
|
||||
ConfigureRabbitMqHost(
|
||||
busFactoryConfigurator,
|
||||
busRegistrationContext.GetRequiredService<MessagingConfiguration>()));
|
||||
busRegistrationContext.GetRequiredService<MassTransitConfiguration>()));
|
||||
} );
|
||||
|
||||
return serviceDescriptors;
|
||||
@@ -60,7 +60,7 @@ namespace Musify.Infrastructure.MassTransit
|
||||
{
|
||||
ConfigureRabbitMqHost(
|
||||
busFactoryConfigurator,
|
||||
busRegistrationContext.GetRequiredService<MessagingConfiguration>());
|
||||
busRegistrationContext.GetRequiredService<MassTransitConfiguration>());
|
||||
|
||||
busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext);
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace Musify.Infrastructure.MassTransit
|
||||
|
||||
static void ConfigureRabbitMqHost(
|
||||
IRabbitMqBusFactoryConfigurator busFactoryConfigurator,
|
||||
MessagingConfiguration massTransitConfiguration)
|
||||
MassTransitConfiguration massTransitConfiguration)
|
||||
{
|
||||
busFactoryConfigurator.Host(massTransitConfiguration.Host, options =>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence
|
||||
{
|
||||
@@ -8,6 +10,14 @@ namespace Musify.Infrastructure.Persistence
|
||||
{
|
||||
public static IServiceCollection AddDatabase(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddOptionsWithValidateOnStart<DatabaseConfiguration>()
|
||||
.Bind(configuration.GetRequiredSection(DatabaseConfiguration.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
||||
|
||||
serviceDescriptors.AddDbContext<Database>();
|
||||
serviceDescriptors.AddScoped<IDatabase>(serviceProvider => serviceProvider.GetRequiredService<Database>());
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
|
||||
namespace Musify.Infrastructure.Pictures
|
||||
{
|
||||
public static class PictureDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddPictureHandler(this IServiceCollection serviceDescriptors)
|
||||
{
|
||||
serviceDescriptors.AddScoped<IPictureHandler, PictureHandler>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -1,17 +1,18 @@
|
||||
using Ardalis.Result;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Musify.Infrastructure.Audio
|
||||
namespace Musify.Infrastructure.Services
|
||||
{
|
||||
public class AudioTranscoder(
|
||||
ILogger<AudioTranscoder> logger,
|
||||
public class AudioTranscoderService(
|
||||
ILogger<AudioTranscoderService> logger,
|
||||
AudioTranscoderConfiguration audioTranscoderConfiguration)
|
||||
: IAudioTranscoder
|
||||
: IAudioTranscoderService
|
||||
{
|
||||
public async Task<Result> TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken)
|
||||
record FfmpegExecutionResult(int ExitCode, string StandardOutput, string StandardError);
|
||||
|
||||
public async Task<int> TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -30,30 +31,32 @@ namespace Musify.Infrastructure.Audio
|
||||
{
|
||||
logger.LogError("Audio transcoding to DASH failed with exit code {ExitCode}. Output: {StandardOutput}, Error: {StandardError}",
|
||||
executionResult.ExitCode, executionResult.StandardOutput, executionResult.StandardError);
|
||||
return Result.Error($"Audio transcoding failed (ExitCode: {executionResult.ExitCode})");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Audio transcoding to DASH completed for {DestinationPath}", destinationPath);
|
||||
}
|
||||
|
||||
logger.LogInformation("Audio transcoding to DASH completed for {DestinationPath}", destinationPath);
|
||||
return Result.Success();
|
||||
return executionResult.ExitCode;
|
||||
}
|
||||
catch (TimeoutException timeoutException)
|
||||
{
|
||||
logger.LogError(timeoutException, "Audio transcoding timed out after {Timeout} for {DestinationPath}", audioTranscoderConfiguration.TranscodingTimeout, destinationPath);
|
||||
return Result.Error("Audio transcoding timed out");
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to transcode audio to DASH for {DestinationPath}", destinationPath);
|
||||
return Result.Error(exception.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<bool>> IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken)
|
||||
public async Task<bool> IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
logger.LogWarning("Audio validation requested for non-existing file {FilePath}", filePath);
|
||||
return Result<bool>.Error($"Audio file not found: {filePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -76,18 +79,17 @@ namespace Musify.Infrastructure.Audio
|
||||
logger.LogDebug("Audio validation successful for {FilePath}", filePath);
|
||||
}
|
||||
|
||||
return Result<bool>.Success(isValid);
|
||||
return isValid;
|
||||
}
|
||||
catch (TimeoutException timeoutException)
|
||||
{
|
||||
logger.LogError(timeoutException, "Audio validation timed out for {FilePath}", filePath);
|
||||
return Result<bool>.Error("Audio validation timed out.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to validate audio file {FilePath}", filePath);
|
||||
return Result<bool>.Error(exception.Message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async Task<FfmpegExecutionResult> ExecuteFfmpegWithInputAsync(
|
||||
@@ -216,7 +218,5 @@ namespace Musify.Infrastructure.Audio
|
||||
|
||||
static string BuildValidateAudioArguments(string filePath)
|
||||
=> $"-v error -i \"{filePath}\" -map 0:a:0 -f null -";
|
||||
|
||||
sealed record FfmpegExecutionResult(int ExitCode, string StandardOutput, string StandardError);
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -1,4 +1,3 @@
|
||||
using Ardalis.Result;
|
||||
using Keycloak.Net;
|
||||
using Keycloak.Net.Models.Users;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -6,14 +5,14 @@ using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Application.Users.Responses;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Identity
|
||||
namespace Musify.Infrastructure.Services
|
||||
{
|
||||
public class KeycloakUserService(
|
||||
KeycloakClient keycloakClient,
|
||||
KeycloakConfiguration keycloakConfiguration,
|
||||
ILogger<KeycloakUserService> logger) : IKeycloakUserService
|
||||
{
|
||||
public async Task<Result<KeycloakUserResponse>> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken)
|
||||
public async Task<KeycloakUserResponse> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken)
|
||||
{
|
||||
User keycloakUser;
|
||||
|
||||
@@ -24,14 +23,14 @@ namespace Musify.Infrastructure.Identity
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogWarning("Operation to retrieve Keycloak user {KeycloakId} was cancelled", keycloakId);
|
||||
return Result.Error("Operation cancelled");
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Failed to retrieve Keycloak user {KeycloakId}", keycloakId);
|
||||
return Result.NotFound();
|
||||
throw;
|
||||
}
|
||||
return Result.Success(KeycloakUserMapper.Map(keycloakUser));
|
||||
return ToResponse(keycloakUser);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<KeycloakUserResponse>> GetUsersAsync(CancellationToken cancellationToken, string search = "", int first = 0, int max = 20, string username = "")
|
||||
@@ -45,7 +44,7 @@ namespace Musify.Infrastructure.Identity
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return users
|
||||
.Select(KeycloakUserMapper.Map)
|
||||
.Select(ToResponse)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -57,5 +56,14 @@ namespace Musify.Infrastructure.Identity
|
||||
username: username,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
KeycloakUserResponse ToResponse(User keycloakUser)
|
||||
{
|
||||
return new KeycloakUserResponse(
|
||||
Guid.Parse(keycloakUser.Id ?? throw new InvalidOperationException("Keycloak user Id is required.")),
|
||||
keycloakUser.UserName ?? throw new InvalidOperationException("Keycloak user Name is required."),
|
||||
keycloakUser.FirstName ?? throw new InvalidOperationException("Keycloak user FirstName is required."),
|
||||
keycloakUser.LastName ?? throw new InvalidOperationException("Keycloak user SecondName is required."));
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-10
@@ -1,15 +1,14 @@
|
||||
using Ardalis.Result;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Musify.Infrastructure.Pictures
|
||||
namespace Musify.Infrastructure.Services
|
||||
{
|
||||
public class PictureHandler(ILogger<PictureHandler> logger)
|
||||
: IPictureHandler
|
||||
public class PictureService(ILogger<PictureService> logger)
|
||||
: IPictureService
|
||||
{
|
||||
public async Task<Result<Stream>> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken)
|
||||
public async Task<Stream> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -18,7 +17,6 @@ namespace Musify.Infrastructure.Pictures
|
||||
pictureStream.Position = 0;
|
||||
|
||||
using var picture = await Image.LoadAsync(pictureStream, cancellationToken);
|
||||
|
||||
picture.Mutate(options => options.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(width, height),
|
||||
@@ -26,18 +24,16 @@ namespace Musify.Infrastructure.Pictures
|
||||
} ));
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
await picture.SaveAsWebpAsync(memoryStream, cancellationToken);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
logger.LogDebug("Picture resized to {Width}x{Height}, length: {Length}", width, height, memoryStream.Length);
|
||||
return Result<Stream>.Success(memoryStream);
|
||||
return memoryStream;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to resize picture to {Width}x{Height}", width, height);
|
||||
return Result.Error("Failed to resize picture");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Amazon.S3;
|
||||
using Keycloak.Net;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Services
|
||||
{
|
||||
public static class ServicesDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddStorageService(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddOptionsWithValidateOnStart<InfrastructureStorageConfiguration>()
|
||||
.Bind(configuration.GetRequiredSection(InfrastructureStorageConfiguration.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<InfrastructureStorageConfiguration>>().Value);
|
||||
|
||||
serviceDescriptors.AddScoped<IAmazonS3>(serviceProvider =>
|
||||
{
|
||||
var configuration = serviceProvider.GetRequiredService<InfrastructureStorageConfiguration>();
|
||||
var s3Configuration = new AmazonS3Config
|
||||
{
|
||||
ServiceURL = configuration.ServiceUrl,
|
||||
ForcePathStyle = configuration.ForcePathStyle,
|
||||
UseHttp = configuration.UseHttp,
|
||||
};
|
||||
return new AmazonS3Client(configuration.AccessKey, configuration.SecretAccessKey, s3Configuration);
|
||||
} );
|
||||
|
||||
serviceDescriptors.AddScoped<IStorageService, StorageService>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddPictureService(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors.AddScoped<IPictureService, PictureService>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddKeycloakService(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors.AddOptionsWithValidateOnStart<KeycloakConfiguration>()
|
||||
.Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<KeycloakConfiguration>>().Value);
|
||||
|
||||
serviceDescriptors.AddScoped(serviceProvider =>
|
||||
{
|
||||
var keycloakConfiguration = serviceProvider.GetRequiredService<KeycloakConfiguration>();
|
||||
|
||||
return new KeycloakClient(
|
||||
keycloakConfiguration.BaseUrl,
|
||||
keycloakConfiguration.Username,
|
||||
keycloakConfiguration.Password);
|
||||
} );
|
||||
serviceDescriptors.AddScoped<IKeycloakUserService, KeycloakUserService>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration)
|
||||
{
|
||||
serviceDescriptors
|
||||
.AddOptionsWithValidateOnStart<AudioTranscoderConfiguration>()
|
||||
.Bind(configuration.GetRequiredSection(AudioTranscoderConfiguration.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<AudioTranscoderConfiguration>>().Value);
|
||||
|
||||
serviceDescriptors.AddScoped<IAudioTranscoderService, AudioTranscoderService>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-29
@@ -1,19 +1,18 @@
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Amazon.S3.Transfer;
|
||||
using Ardalis.Result;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeMapping;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Storage
|
||||
namespace Musify.Infrastructure.Services
|
||||
{
|
||||
public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, InfrastructureStorageConfiguration storageClientConfiguration, ILogger<StorageHandler> logger)
|
||||
: IStorageHandler
|
||||
public class StorageService(IDatabase database, IAmazonS3 amazonS3, InfrastructureStorageConfiguration storageClientConfiguration, ILogger<StorageService> logger)
|
||||
: IStorageService
|
||||
{
|
||||
public async Task<Result<Stream>> GetFileAsync(string bucket, string key, CancellationToken cancellationToken)
|
||||
public async Task<Stream> GetFileAsync(string bucket, string key, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new GetObjectRequest
|
||||
{
|
||||
@@ -24,14 +23,13 @@ namespace Musify.Infrastructure.Storage
|
||||
try
|
||||
{
|
||||
var response = await amazonS3.GetObjectAsync(request, cancellationToken);
|
||||
|
||||
logger.LogDebug("Retrieved file from S3 {Bucket}/{Key}", bucket, key);
|
||||
return Result.Success(response.ResponseStream);
|
||||
return response.ResponseStream;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Failed to get file from S3 {Bucket}/{Key}", bucket, key);
|
||||
return Result.NotFound($"File {bucket}/{key} not found");
|
||||
logger.LogError(exception, "Failed to get file from S3 {Bucket}/{Key}", bucket, key);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,12 +44,10 @@ namespace Musify.Infrastructure.Storage
|
||||
};
|
||||
|
||||
logger.LogDebug("Generating pre-signed URL for {Bucket}/{Key}", bucket, key);
|
||||
var url = await amazonS3.GetPreSignedURLAsync(request);
|
||||
|
||||
return url;
|
||||
return await amazonS3.GetPreSignedURLAsync(request);
|
||||
}
|
||||
|
||||
public async Task<Result> RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken)
|
||||
public async Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new DeleteObjectRequest
|
||||
{
|
||||
@@ -60,12 +56,10 @@ namespace Musify.Infrastructure.Storage
|
||||
};
|
||||
|
||||
logger.LogDebug("Removing file from S3 {Bucket}/{Key}", bucket, key);
|
||||
var response = await amazonS3.DeleteObjectAsync(request, cancellationToken);
|
||||
|
||||
return Result.NoContent();
|
||||
await amazonS3.DeleteObjectAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result> TransferFilesAsync(string sourceDirectory, string bucket, string route, CancellationToken cancellationToken)
|
||||
public async Task TransferFilesAsync(string sourceDirectory, string bucket, string route, CancellationToken cancellationToken)
|
||||
{
|
||||
var trasnsferUtility = new TransferUtility(amazonS3);
|
||||
|
||||
@@ -101,31 +95,30 @@ namespace Musify.Infrastructure.Storage
|
||||
failedCount++;
|
||||
upload.State = UploadState.Failed;
|
||||
logger.LogError(exception, "Failed to transfer file to S3 {Bucket}/{Key}", bucket, key);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("File transfer completed to {Bucket}/{Route}. Success: {SuccessCount}, Failed: {FailedCount}", bucket, route, successCount, failedCount);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<Result<string>> UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken)
|
||||
public async Task UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fileStream = File.OpenRead(filePath);
|
||||
var contentType = MimeUtility.GetMimeMapping(filePath);
|
||||
return await UploadFileAsync(fileStream, contentType, bucket, key, cancellationToken);
|
||||
await UploadFileAsync(fileStream, contentType, bucket, key, cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to upload file to S3 {Bucket}/{Key}", bucket, key);
|
||||
return Result.Error($"Failed to upload to {bucket}/{key}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken)
|
||||
public async Task UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new PutObjectRequest
|
||||
{
|
||||
@@ -153,17 +146,16 @@ namespace Musify.Infrastructure.Storage
|
||||
{
|
||||
logger.LogError(exception, "Failed to upload file to S3 {Bucket}/{Key}", bucket, key);
|
||||
upload.State = UploadState.Failed;
|
||||
return Result.Error($"Failed to upload to {bucket}/{key}");
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
|
||||
upload.State = UploadState.Successful;
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogDebug("Saved upload record for {Bucket}/{Key}", bucket, key);
|
||||
|
||||
return Result.Success(key);
|
||||
}
|
||||
|
||||
public async Task<Result> CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey,
|
||||
public async Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new CopyObjectRequest
|
||||
@@ -179,13 +171,12 @@ namespace Musify.Infrastructure.Storage
|
||||
var response = await amazonS3.CopyObjectAsync(request, cancellationToken);
|
||||
logger.LogInformation("Copied file from {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}",
|
||||
sourceBucket, sourceKey, destinationBucket, destinationKey);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to copy file from {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}",
|
||||
sourceBucket, sourceKey, destinationBucket, destinationKey);
|
||||
return Result.Error($"Failed to copy from {sourceBucket}/{sourceKey} to {destinationBucket}/{destinationKey}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Amazon.S3;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Application.Contracts.Infrastructure;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Storage
|
||||
{
|
||||
public static class StorageDependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddStorageHandler(this IServiceCollection serviceDescriptors)
|
||||
{
|
||||
serviceDescriptors.AddScoped<IAmazonS3>(serviceProvider =>
|
||||
{
|
||||
var configuration = serviceProvider.GetRequiredService<InfrastructureStorageConfiguration>();
|
||||
var s3Configuration = new AmazonS3Config
|
||||
{
|
||||
ServiceURL = configuration.ServiceUrl,
|
||||
ForcePathStyle = configuration.ForcePathStyle,
|
||||
UseHttp = configuration.UseHttp,
|
||||
};
|
||||
return new AmazonS3Client(configuration.AccessKey, configuration.SecretAccessKey, s3Configuration);
|
||||
} );
|
||||
|
||||
serviceDescriptors.AddScoped<IStorageHandler, StorageHandler>();
|
||||
return serviceDescriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user