diff --git a/Application/Application.csproj b/Application/Application.csproj
index abf9700..f9b7155 100644
--- a/Application/Application.csproj
+++ b/Application/Application.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/Application/Contracts/Application/IPictureService.cs b/Application/Contracts/Application/IPictureService.cs
deleted file mode 100644
index b8bab3a..0000000
--- a/Application/Contracts/Application/IPictureService.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Ardalis.Result;
-using Musify.Application.Pictures.Contracts;
-using Musify.Domain.Entities;
-
-namespace Musify.Application.Contracts.Application
-{
- public interface IPictureService
- {
- Task> ResizePictureAsync(EntityType entityType, Guid EntityId, string keyName, string contentType, Stream pictureStream,
- PictureResize[] pictureResizes, CancellationToken cancellationToken);
- }
-}
\ No newline at end of file
diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs
index 172bfd8..69ea4eb 100644
--- a/Application/Contracts/Infrastructure/IDatabase.cs
+++ b/Application/Contracts/Infrastructure/IDatabase.cs
@@ -15,6 +15,8 @@ namespace Musify.Application.Contracts.Infrastructure
DbSet Uploads { get; }
+ DbSet Jobs { get; }
+
Task SaveChangesAsync(CancellationToken cancellationToken);
}
}
\ No newline at end of file
diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs
new file mode 100644
index 0000000..76fc108
--- /dev/null
+++ b/Application/Events/RemoveFileEvent.cs
@@ -0,0 +1,6 @@
+namespace Musify.Application.Events
+{
+ public record RemoveFileEvent(
+ string BucketName,
+ string KeyName);
+}
\ No newline at end of file
diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs
new file mode 100644
index 0000000..00ccfce
--- /dev/null
+++ b/Application/Events/ResizePictureEvent.cs
@@ -0,0 +1,7 @@
+namespace Musify.Application.Events
+{
+ public record ResizePictureEvent(
+ string BucketName,
+ string KeyName,
+ ResizePictureItems[] Items);
+}
\ No newline at end of file
diff --git a/Application/Events/ResizePictureItems.cs b/Application/Events/ResizePictureItems.cs
new file mode 100644
index 0000000..6af2dae
--- /dev/null
+++ b/Application/Events/ResizePictureItems.cs
@@ -0,0 +1,7 @@
+namespace Musify.Application.Events
+{
+ public record ResizePictureItems(
+ int Width,
+ int Height,
+ string SaveOnRoute);
+}
\ No newline at end of file
diff --git a/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs
deleted file mode 100644
index 707ed8d..0000000
--- a/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Ardalis.Result;
-using DispatchR.Abstractions.Send;
-using Musify.Application.Pictures.Contracts;
-using Musify.Domain.Entities;
-
-namespace Musify.Application.Pictures.Commands.ResizePicture
-{
- public record ResizeCommand(
- EntityType EntityType,
- Guid EntityId,
- string KeyName,
- string ContentType,
- Stream PictureStream,
- PictureResize[] PictureResizes)
- : IRequest>;
-}
\ No newline at end of file
diff --git a/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs
deleted file mode 100644
index bd61a9c..0000000
--- a/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Ardalis.Result;
-using DispatchR.Abstractions.Send;
-using Musify.Application.Contracts.Application;
-
-namespace Musify.Application.Pictures.Commands.ResizePicture
-{
- public class ResizeCommandHandler(IPictureService pictureService)
- : IRequestHandler >>
- {
- public async Task> Handle(ResizeCommand request, CancellationToken cancellationToken)
- {
- var result = await pictureService.ResizePictureAsync(request.EntityType, request.EntityId, request.KeyName, request.ContentType,
- request.PictureStream, request.PictureResizes, cancellationToken);
-
- return result;
- }
- }
-}
\ No newline at end of file
diff --git a/Application/Pictures/Contracts/PictureResize.cs b/Application/Pictures/Contracts/PictureResize.cs
deleted file mode 100644
index f66021a..0000000
--- a/Application/Pictures/Contracts/PictureResize.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Musify.Application.Pictures.Contracts
-{
- public record PictureResize(
- int Width,
- int Height,
- string SaveOnRoute);
-}
\ No newline at end of file
diff --git a/Application/Pictures/Events/PictureResizeEvent.cs b/Application/Pictures/Events/PictureResizeEvent.cs
deleted file mode 100644
index 8439e14..0000000
--- a/Application/Pictures/Events/PictureResizeEvent.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Musify.Application.Pictures.Contracts;
-
-namespace Musify.Application.Pictures.Events
-{
- public record PictureResizeEvent(
- Guid UploadId,
- PictureResize[] ImageResizes);
-}
\ No newline at end of file
diff --git a/Application/Pictures/PictureDependencyInjection.cs b/Application/Pictures/PictureDependencyInjection.cs
deleted file mode 100644
index 914897f..0000000
--- a/Application/Pictures/PictureDependencyInjection.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Musify.Application.Contracts.Application;
-
-namespace Musify.Application.Pictures
-{
- public static class PictureDependencyInjection
- {
- public static void AddPictureService(this IServiceCollection serviceDescriptors)
- {
- serviceDescriptors.AddScoped();
- }
- }
-}
\ No newline at end of file
diff --git a/Application/Pictures/PictureService.cs b/Application/Pictures/PictureService.cs
deleted file mode 100644
index 0dfcedc..0000000
--- a/Application/Pictures/PictureService.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Ardalis.Result;
-using MassTransit;
-using Microsoft.Extensions.Logging;
-using Musify.Application.Configuration;
-using Musify.Application.Contracts.Application;
-using Musify.Application.Contracts.Infrastructure;
-using Musify.Application.Pictures.Commands.ResizePicture;
-using Musify.Application.Pictures.Contracts;
-using Musify.Application.Pictures.Events;
-using Musify.Domain.Entities;
-
-namespace Musify.Application.Pictures
-{
- public class PictureService(IStorageHandler storageHandler, IDatabase database, IPublishEndpoint publishEndpoint,
- ILogger logger, StorageConfiguration storageConfiguration)
- : IPictureService
- {
- public async Task> ResizePictureAsync(EntityType entityType, Guid EntityId, string keyName, string contentType, Stream pictureStream,
- PictureResize[] pictureResizes, CancellationToken cancellationToken)
- {
- var pictureId = Guid.NewGuid();
-
- var upload = new Upload
- {
- Id = pictureId,
- KeyName = keyName,
- ContentType = contentType,
- BucketName = storageConfiguration.BucketName,
- EntityType = entityType,
- EntityId = EntityId,
- State = UploadState.Completed
- };
-
- await database.Uploads.AddAsync(upload, cancellationToken);
-
- await database.SaveChangesAsync(cancellationToken);
-
- var uploadResult = await storageHandler.UploadFileAsync(
- pictureStream,
- contentType,
- storageConfiguration.BucketName,
- keyName,
- cancellationToken);
-
- if (!uploadResult.IsSuccess)
- {
- logger.LogError("Failed to upload the picture with ID {PictureId} to bucket {BucketName}.", pictureId, storageConfiguration.BucketName);
-
- upload.State = UploadState.Failed;
- database.Uploads.Update(upload);
- await database.SaveChangesAsync(cancellationToken);
-
- return Result.Error("Failed to upload the picture.");
- }
-
- await publishEndpoint.Publish(new PictureResizeEvent(pictureId, pictureResizes));
-
- return Result.Success();
- }
- }
-}
\ No newline at end of file
diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs
index b55d20a..d813000 100644
--- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs
+++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs
@@ -9,6 +9,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList
string Name,
string Description,
Stream PictureStream,
+ string PictureFileType,
string PictureContentType)
: IRequest>;
}
\ No newline at end of file
diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs
index 5127c3b..d47cd82 100644
--- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs
+++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs
@@ -1,18 +1,18 @@
using Ardalis.Result;
using DispatchR.Abstractions.Send;
+using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Configuration;
-using Musify.Application.Contracts.Application;
using Musify.Application.Contracts.Infrastructure;
-using Musify.Application.Pictures.Contracts;
+using Musify.Application.Events;
using Musify.Application.PlayLists.Contracts;
using Musify.Domain.Entities;
namespace Musify.Application.PlayLists.Commands.CreatePlayList
{
- public class CreatePlayListCommandHandler(IPictureService pictureService, IDatabase database, ILogger logger,
- PlayListConfiguration playListConfiguration)
+ public class CreatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler,
+ ILogger logger, StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration)
: IRequestHandler> >
{
public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
@@ -35,34 +35,45 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList
MediumPictureKeyName = playListConfiguration.Routes.MediumPictures,
LargePictureKeyName = playListConfiguration.Routes.LargePictures,
};
-
await database.PlayLists.AddAsync(playList, cancellationToken);
- await database.SaveChangesAsync(cancellationToken);
+ var imageId = Guid.NewGuid();
+
+ var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}.{request.PictureFileType}";
+
+ var uploadResult = await storageHandler.UploadFileAsync(
+ request.PictureStream,
+ request.PictureContentType,
+ storageConfiguration.BucketName,
+ imageKeyName,
+ cancellationToken);
+
+ if (!uploadResult.IsSuccess)
+ {
+ logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error: {ErrorMessage}",
+ playList.Id, string.Join("; ", uploadResult.Errors));
+ return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage.");
+ }
logger.LogInformation("PlayList with Id={PlayListId} created successfully for UserId={UserId}.", playList.Id, request.UserId);
- await pictureService.ResizePictureAsync(
- EntityType.PlayList,
- playList.Id,
- playList.Id.ToString(),
- request.PictureContentType,
- request.PictureStream,
- [
- new PictureResize(
+ await publishEndpoint.Publish(new ResizePictureEvent(
+ storageConfiguration.BucketName,
+ imageKeyName,
+ [
+ new ResizePictureItems(
playListConfiguration.PicturesSizes.SmallPictureWidth,
playListConfiguration.PicturesSizes.SmallPictureHeight,
playListConfiguration.Routes.SmallPictures),
- new PictureResize(
+ new ResizePictureItems(
playListConfiguration.PicturesSizes.MediumPictureWidth,
playListConfiguration.PicturesSizes.MediumPictureHeight,
playListConfiguration.Routes.MediumPictures),
- new PictureResize(
+ new ResizePictureItems(
playListConfiguration.PicturesSizes.LargePictureWidth,
playListConfiguration.PicturesSizes.LargePictureHeight,
playListConfiguration.Routes.LargePictures)
- ],
- cancellationToken);
+ ] ), cancellationToken);
logger.LogInformation("Picture for PlayList with Id={PlayListId} ", playList.Id);
diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs
new file mode 100644
index 0000000..50d4cad
--- /dev/null
+++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs
@@ -0,0 +1,8 @@
+using Ardalis.Result;
+using DispatchR.Abstractions.Send;
+
+namespace Musify.Application.PlayLists.Commands.DeletePlayList
+{
+ public record DeletePlayListCommand(Guid UserId, Guid PlayListId)
+ : IRequest;
+}
\ No newline at end of file
diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs
new file mode 100644
index 0000000..e7ca9b7
--- /dev/null
+++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs
@@ -0,0 +1,36 @@
+using Ardalis.Result;
+using DispatchR.Abstractions.Send;
+using Microsoft.Extensions.Logging;
+using Musify.Application.Configuration;
+using Musify.Application.Contracts.Infrastructure;
+
+namespace Musify.Application.PlayLists.Commands.DeletePlayList
+{
+ public class DeletePlayListCommandHandler(IDatabase database, IStorageHandler storageHandler,
+ StorageConfiguration storageConfiguration, ILogger logger)
+ : IRequestHandler>
+ {
+ public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
+ {
+ var playList = await database.PlayLists.FindAsync(request.PlayListId, cancellationToken);
+
+ if (playList is null)
+ {
+ logger.LogDebug("PlayList with id={PlayListId} not found", request.PlayListId);
+ return Result.NotFound();
+ }
+
+ if (playList.UserId != request.UserId)
+ {
+ logger.LogInformation("PlayList with id={PlayListId} does not belong to user with id={UserId}", request.PlayListId, request.UserId);
+ return Result.Unauthorized();
+ }
+
+ database.PlayLists.Remove(playList);
+
+ await database.SaveChangesAsync(cancellationToken);
+
+ return Result.NoContent();
+ }
+ }
+}
diff --git a/Domain/Entities/EntityType.cs b/Domain/Entities/EntityType.cs
deleted file mode 100644
index 24c2cd9..0000000
--- a/Domain/Entities/EntityType.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Musify.Domain.Entities
-{
- public enum EntityType
- {
- User,
- Track,
- PlayList
- }
-}
\ No newline at end of file
diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs
new file mode 100644
index 0000000..6c20331
--- /dev/null
+++ b/Domain/Entities/Job.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Musify.Domain.Entities
+{
+ [Table("Job")]
+ public class Job
+ {
+ [Key]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ [Required]
+ public required JobType JobType { get; set; }
+
+ [Required]
+ public JobState JobState { get; set; } = JobState.Pending;
+
+ [Required]
+ public required string Payload { get; set; }
+
+ [Required]
+ public DateTime CreateAt { get; set; } = DateTime.UtcNow;
+
+ [Required]
+ public int RetryCount { get; set; } = 0;
+
+ public DateTime CompletedAt { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Domain/Entities/JobState.cs b/Domain/Entities/JobState.cs
new file mode 100644
index 0000000..9cc6cde
--- /dev/null
+++ b/Domain/Entities/JobState.cs
@@ -0,0 +1,10 @@
+namespace Musify.Domain.Entities
+{
+ public enum JobState
+ {
+ Pending,
+ Failed,
+ InProgress,
+ Completed
+ }
+}
\ No newline at end of file
diff --git a/Domain/Entities/JobType.cs b/Domain/Entities/JobType.cs
new file mode 100644
index 0000000..c0ded2f
--- /dev/null
+++ b/Domain/Entities/JobType.cs
@@ -0,0 +1,8 @@
+namespace Musify.Domain.Entities
+{
+ public enum JobType
+ {
+ ResizePicture,
+ RemoveFile,
+ }
+}
\ No newline at end of file
diff --git a/Domain/Entities/Upload.cs b/Domain/Entities/Upload.cs
index 0bb9ca4..a74db25 100644
--- a/Domain/Entities/Upload.cs
+++ b/Domain/Entities/Upload.cs
@@ -19,15 +19,9 @@ namespace Musify.Domain.Entities
public required string ContentType { get; set; }
[Required]
- public required EntityType EntityType { get; set; }
-
- [Required]
- public required Guid EntityId { get; set; }
+ public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
[Required]
public UploadState State { get; set; } = UploadState.Pending;
-
- [Required]
- public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
}
}
\ No newline at end of file
diff --git a/Domain/Entities/UploadState.cs b/Domain/Entities/UploadState.cs
index 738b5d4..50a4dfc 100644
--- a/Domain/Entities/UploadState.cs
+++ b/Domain/Entities/UploadState.cs
@@ -3,8 +3,7 @@
public enum UploadState
{
Pending,
- Processing,
- Completed,
- Failed
+ Failed,
+ Successful,
}
}
\ No newline at end of file
diff --git a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs
deleted file mode 100644
index 28b6bdb..0000000
--- a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using Ardalis.Result;
-using MassTransit;
-using Microsoft.Extensions.Logging;
-using Musify.Application.Contracts.Infrastructure;
-using Musify.Application.Pictures.Contracts;
-using Musify.Application.Pictures.Events;
-using Musify.Domain.Entities;
-
-namespace Musify.Infrastructure.Messaging.Consumers
-{
- public class PictureResizerConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger)
- : IConsumer
- {
- public const string QueueName = "picture-resize-queue";
-
- public async Task Consume(ConsumeContext consumeContext)
- {
- try
- {
- var upload = await database.Uploads.FindAsync(consumeContext.Message.UploadId);
-
- if (upload is null)
- {
- logger.LogWarning("Upload not found in database: UploadId={UploadId}", consumeContext.Message.UploadId);
- return;
- }
-
- if (upload.State != UploadState.Completed)
- {
- logger.LogInformation("Upload is not in a Completed state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State);
- return;
- }
-
- await UpdateState(database, upload, UploadState.Processing, consumeContext.CancellationToken);
-
- var fileResult = await storageHandler.GetFileAsync(
- upload.BucketName,
- upload.KeyName,
- consumeContext.CancellationToken);
-
- if (fileResult.IsNotFound())
- {
- logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", upload.BucketName, upload.KeyName);
- await UpdateState(database, upload, UploadState.Failed, consumeContext.CancellationToken);
- return;
- }
-
- using var fileStream = fileResult.Value;
-
- using var memoryStream = new MemoryStream();
- await fileStream.CopyToAsync(memoryStream, consumeContext.CancellationToken);
-
- foreach (var resize in consumeContext.Message.ImageResizes)
- {
- var newUploadId = Guid.NewGuid();
-
- var newUpload = new Upload
- {
- Id = newUploadId,
- BucketName = upload.BucketName,
- KeyName = $"{resize.SaveOnRoute}/{newUploadId}.webp",
- ContentType = "image/webp",
- State = UploadState.Processing,
- EntityId = upload.EntityId,
- EntityType = upload.EntityType
- };
-
- await database.Uploads.AddAsync(newUpload, consumeContext.CancellationToken);
-
- await database.SaveChangesAsync(consumeContext.CancellationToken);
-
- try
- {
- await ResizePicture(upload, newUpload, memoryStream, resize, consumeContext.CancellationToken);
- }
- catch (Exception exception)
- {
- logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", upload.Id, resize);
- await UpdateState(database, newUpload, UploadState.Failed, consumeContext.CancellationToken);
- continue;
- }
- }
-
- await UpdateState(database, upload, UploadState.Completed, consumeContext.CancellationToken);
- }
- catch (Exception exception)
- {
- logger.LogError(exception, "An error occurred while consuming the ImageResizedConsumer message.");
- throw;
- }
- finally
- {
- logger.LogInformation("Saving changes to database after processing ImageResizedConsumer message.");
- await database.SaveChangesAsync(consumeContext.CancellationToken);
- }
- }
-
- async Task UpdateState(IDatabase database, Upload upload, UploadState uploadState, CancellationToken cancellationToken)
- {
- upload.State = uploadState;
- database.Uploads.Update(upload);
- }
-
- async Task ResizePicture(Upload originalUpload, Upload newUpload, MemoryStream memoryStream, PictureResize pictureResize, CancellationToken cancellationToken)
- {
- memoryStream.Position = 0;
-
- var pictureResult = await pictureHandler.ResizePictureAsync(
- memoryStream,
- pictureResize.Width,
- pictureResize.Height,
- cancellationToken);
-
- if (!pictureResult.IsSuccess)
- {
- logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors));
- await UpdateState(database, newUpload, UploadState.Failed, cancellationToken);
- return;
- }
-
- var saveResult = await storageHandler.UploadFileAsync(
- pictureResult.Value,
- "image/webp",
- originalUpload.BucketName,
- $"{pictureResize.SaveOnRoute}/{newUpload.Id}.webp",
- cancellationToken);
-
- if (!saveResult.IsSuccess)
- {
- logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors));
- await UpdateState(database, newUpload, UploadState.Failed, cancellationToken);
- return;
- }
-
- await UpdateState(database, newUpload, UploadState.Completed, cancellationToken);
- }
- }
-}
\ No newline at end of file
diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs
new file mode 100644
index 0000000..29b4244
--- /dev/null
+++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs
@@ -0,0 +1,14 @@
+using MassTransit;
+using Musify.Application.Events;
+
+namespace Musify.Infrastructure.Messaging.Consumers
+{
+ public class RemoveFileConsumer
+ : IConsumer
+ {
+ public Task Consume(ConsumeContext context)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs
new file mode 100644
index 0000000..3c5ed0f
--- /dev/null
+++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs
@@ -0,0 +1,138 @@
+using Ardalis.Result;
+using MassTransit;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Musify.Application.Contracts.Infrastructure;
+using Musify.Application.Events;
+using Musify.Domain.Entities;
+using System.Text.Json;
+
+namespace Musify.Infrastructure.Messaging.Consumers
+{
+ public class ResizePictureConsumer(IStorageHandler storageHandler, IPictureHandler pictureHandler, IDatabase database, ILogger logger)
+ : IConsumer
+ {
+ public const string QueueName = "picture-resize-queue";
+
+ public async Task Consume(ConsumeContext consumeContext)
+ {
+ try
+ {
+ Dictionary jobs = new Dictionary();
+
+ foreach (var item in consumeContext.Message.Items)
+ {
+ var payloadJson = JsonSerializer.Serialize(item);
+
+ var job = await database.Jobs
+ .SingleOrDefaultAsync(job => job.JobType == JobType.ResizePicture && job.Payload == payloadJson, consumeContext.CancellationToken);
+
+ if (job is null)
+ {
+ job = new Job
+ {
+ JobType = JobType.ResizePicture,
+ Payload = payloadJson,
+ };
+ await database.Jobs.AddAsync(job, consumeContext.CancellationToken);
+ }
+ else
+ {
+ job.RetryCount++;
+ job.JobState = JobState.InProgress;
+ database.Jobs.Update(job);
+ }
+
+ jobs.Add(item, job);
+ }
+
+ var fileResult = await storageHandler.GetFileAsync(
+ consumeContext.Message.BucketName,
+ consumeContext.Message.KeyName,
+ consumeContext.CancellationToken);
+
+ if (fileResult.IsNotFound())
+ {
+ logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}",
+ consumeContext.Message.BucketName, consumeContext.Message.KeyName);
+
+ foreach (var job in jobs)
+ {
+ job.Value.JobState = JobState.Failed;
+ }
+ await database.SaveChangesAsync(consumeContext.CancellationToken);
+ return;
+ }
+
+ using var fileStream = fileResult.Value;
+ using var memoryStream = new MemoryStream();
+ await fileStream.CopyToAsync(memoryStream, consumeContext.CancellationToken);
+
+ foreach (var item in consumeContext.Message.Items)
+ {
+ var job = jobs[item];
+
+ try
+ {
+ await ResizePicture(
+ job,
+ consumeContext.Message.BucketName,
+ memoryStream,
+ item,
+ consumeContext.CancellationToken);
+ }
+ catch (Exception exception)
+ {
+ logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", job.Id, item);
+ continue;
+ }
+ }
+ }
+ catch (Exception exception)
+ {
+ logger.LogError(exception, "An error occurred while consuming the ImageResizedConsumer message.");
+ throw;
+ }
+ finally
+ {
+ logger.LogInformation("Saving changes to database after processing ImageResizedConsumer message.");
+ await database.SaveChangesAsync(consumeContext.CancellationToken);
+ }
+ }
+
+ async Task ResizePicture(Job job, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken)
+ {
+ memoryStream.Position = 0;
+ job.JobState = JobState.InProgress;
+
+ var pictureResult = await pictureHandler.ResizePictureAsync(
+ memoryStream,
+ items.Width,
+ items.Height,
+ cancellationToken);
+
+ if (!pictureResult.IsSuccess)
+ {
+ logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors));
+ job.JobState = JobState.Failed;
+ return;
+ }
+
+ var saveResult = await storageHandler.UploadFileAsync(
+ pictureResult.Value,
+ "image/webp",
+ bucketName,
+ $"{items.SaveOnRoute}/{Guid.NewGuid() }.webp",
+ cancellationToken);
+
+ if (!saveResult.IsSuccess)
+ {
+ logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors));
+ job.JobState = JobState.Failed;
+ return;
+ }
+
+ job.JobState = JobState.Completed;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs
index c7a37f0..a38f593 100644
--- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs
+++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs
@@ -37,7 +37,7 @@ namespace Musify.Infrastructure.Messaging
{
serviceDescriptors.AddMassTransit(options =>
{
- options.AddConsumer();
+ options.AddConsumer();
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
{
@@ -58,9 +58,9 @@ namespace Musify.Infrastructure.Messaging
options.Password(password);
} );
- busFactoryConfigurator.ReceiveEndpoint(PictureResizerConsumer.QueueName, endpointConfigurator =>
+ busFactoryConfigurator.ReceiveEndpoint(ResizePictureConsumer.QueueName, endpointConfigurator =>
{
- endpointConfigurator.ConfigureConsumer(busRegistrationContext);
+ endpointConfigurator.ConfigureConsumer(busRegistrationContext);
} );
} );
} );
diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs
index 5a1e8da..5148f5a 100644
--- a/Infrastructure/Persistence/Database.cs
+++ b/Infrastructure/Persistence/Database.cs
@@ -22,5 +22,7 @@ namespace Musify.Infrastructure.Persistence
public DbSet PlayListHasTracks => Set();
public DbSet Uploads => Set();
+
+ public DbSet Jobs => Set();
}
}
\ No newline at end of file
diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs
index ff6a7ac..c324220 100644
--- a/Infrastructure/Storage/StorageHandler.cs
+++ b/Infrastructure/Storage/StorageHandler.cs
@@ -4,10 +4,11 @@ using Amazon.S3.Transfer;
using Ardalis.Result;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts.Infrastructure;
+using Musify.Domain.Entities;
namespace Musify.Infrastructure.Storage
{
- public class StorageHandler(IAmazonS3 amazonS3, ILogger logger)
+ public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, ILogger logger)
: IStorageHandler
{
public async Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken)
@@ -75,18 +76,31 @@ namespace Musify.Infrastructure.Storage
string fileName = Path.GetFileName(file);
string filePath = Path.Combine(sourceDirectory, file);
+ var upload = new Upload
+ {
+ Id = Guid.NewGuid(),
+ BucketName = bucketName,
+ KeyName = $"{route}/{fileName}",
+ ContentType = "Application/Octet-Stream"
+ };
+ await database.Uploads.AddAsync(upload, cancellationToken);
+
try
{
await trasnsferUtility.UploadAsync(filePath, bucketName, fileName, cancellationToken);
+ upload.State = UploadState.Successful;
logger.LogInformation("Successfully transferred file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route);
}
catch
{
logger.LogError("Failed to transfer file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route);
- return Result.Error($"Failed to transfer file {fileName} to S3 with bucket name {bucketName} and route {route}");
+ upload.State = UploadState.Failed;
}
}
+ await database.SaveChangesAsync(cancellationToken);
+ logger.LogInformation("Successfully saved upload records to database for all files transferred to S3 with bucket name {BucketName} and route {Route}", bucketName, route);
+
return Result.Success();
}
@@ -100,6 +114,15 @@ namespace Musify.Infrastructure.Storage
Key = keyName
};
+ var upload = new Upload
+ {
+ Id = Guid.NewGuid(),
+ BucketName = bucketName,
+ KeyName = keyName,
+ ContentType = contentType
+ };
+ await database.Uploads.AddAsync(upload, cancellationToken);
+
try
{
var response = await amazonS3.PutObjectAsync(request, cancellationToken);
@@ -108,9 +131,14 @@ namespace Musify.Infrastructure.Storage
catch
{
logger.LogError("Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
+ upload.State = UploadState.Failed;
return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}");
}
+ upload.State = UploadState.Successful;
+ await database.SaveChangesAsync(cancellationToken);
+ logger.LogInformation("Successfully saved upload record to database with bucket name {BucketName} and key name {KeyName}", bucketName, keyName);
+
return Result.Success(keyName);
}
}