From 306248b11cde0597307ac1b536d38957f0e6126c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 01:14:49 +0100 Subject: [PATCH 001/172] First Commit --- .gitignore | 428 ++++++++++++++++++ Application/Application.csproj | 30 ++ Application/ApplicationDependencyInjection.cs | 6 + .../Contracts/Infrastructure/IDatabase.cs | 18 + .../Infrastructure/IKeycloakUserClient.cs | 22 + .../Infrastructure/IStorageHandler.cs | 17 + .../Contracts/PaginatedPlayListResponse.cs | 12 + .../PlayLists/Contracts/PlayListResponse.cs | 10 + .../GetPlayListById/GetPlayListByIdQuery.cs | 9 + .../GetPlayListByIdQueryHandler.cs | 29 ++ .../Queries/GetPlayLists/GetPlayListsQuery.cs | 9 + .../GetPlayLists/GetPlayListsQueryHandler.cs | 44 ++ .../GetPlayListsByUserIdQuery.cs | 13 + .../GetPlayListsByUserIdQueryHandler.cs | 51 +++ .../Commands/CreateUser/CreateUserCommand.cs | 9 + .../CreateUser/CreateUserCommandHandler.cs | 46 ++ .../Commands/DeleteUser/DeleteUserCommand.cs | 8 + .../DeleteUser/DeleteUserCommandHandler.cs | 24 + .../Users/Contracts/KeycloakUserResponse.cs | 8 + .../Users/Contracts/PaginatedUserResponse.cs | 12 + Application/Users/Contracts/UserResponse.cs | 10 + .../Queries/GetUserById/GetUserByIdQuery.cs | 9 + .../GetUserById/GetUserByIdQueryHandler.cs | 35 ++ .../GetUserByKeycloakCommand.cs | 9 + .../GetUserByKeycloakCommandHandler.cs | 35 ++ .../Users/Queries/GetUsers/GetUsersCommand.cs | 9 + .../GetUsers/GetUsersCommandHandler.cs | 59 +++ .../Identity/KeycloakConfiguration.cs | 15 + .../Identity/KeycloakIdentityService.cs | 61 +++ Infrastructure/Identity/KeycloakUserMapper.cs | 17 + Infrastructure/Infrastructure.csproj | 21 + Infrastructure/Persistence/Database.cs | 18 + Infrastructure/Storage/StorageHandler.cs | 115 +++++ Musify.Backend/Domain.csproj | 10 + Musify.Backend/Entities/PlayList.cs | 43 ++ Musify.Backend/Entities/PlayListHasTrack.cs | 30 ++ Musify.Backend/Entities/Track.cs | 33 ++ Musify.Backend/Entities/User.cs | 22 + MusifyBackend.slnx | 5 + 39 files changed, 1361 insertions(+) create mode 100644 .gitignore create mode 100644 Application/Application.csproj create mode 100644 Application/ApplicationDependencyInjection.cs create mode 100644 Application/Contracts/Infrastructure/IDatabase.cs create mode 100644 Application/Contracts/Infrastructure/IKeycloakUserClient.cs create mode 100644 Application/Contracts/Infrastructure/IStorageHandler.cs create mode 100644 Application/PlayLists/Contracts/PaginatedPlayListResponse.cs create mode 100644 Application/PlayLists/Contracts/PlayListResponse.cs create mode 100644 Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs create mode 100644 Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs create mode 100644 Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs create mode 100644 Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs create mode 100644 Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs create mode 100644 Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs create mode 100644 Application/Users/Commands/CreateUser/CreateUserCommand.cs create mode 100644 Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs create mode 100644 Application/Users/Commands/DeleteUser/DeleteUserCommand.cs create mode 100644 Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs create mode 100644 Application/Users/Contracts/KeycloakUserResponse.cs create mode 100644 Application/Users/Contracts/PaginatedUserResponse.cs create mode 100644 Application/Users/Contracts/UserResponse.cs create mode 100644 Application/Users/Queries/GetUserById/GetUserByIdQuery.cs create mode 100644 Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs create mode 100644 Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs create mode 100644 Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs create mode 100644 Application/Users/Queries/GetUsers/GetUsersCommand.cs create mode 100644 Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs create mode 100644 Infrastructure/Identity/KeycloakConfiguration.cs create mode 100644 Infrastructure/Identity/KeycloakIdentityService.cs create mode 100644 Infrastructure/Identity/KeycloakUserMapper.cs create mode 100644 Infrastructure/Infrastructure.csproj create mode 100644 Infrastructure/Persistence/Database.cs create mode 100644 Infrastructure/Storage/StorageHandler.cs create mode 100644 Musify.Backend/Domain.csproj create mode 100644 Musify.Backend/Entities/PlayList.cs create mode 100644 Musify.Backend/Entities/PlayListHasTrack.cs create mode 100644 Musify.Backend/Entities/Track.cs create mode 100644 Musify.Backend/Entities/User.cs create mode 100644 MusifyBackend.slnx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba7a514 --- /dev/null +++ b/.gitignore @@ -0,0 +1,428 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp \ No newline at end of file diff --git a/Application/Application.csproj b/Application/Application.csproj new file mode 100644 index 0000000..d063a70 --- /dev/null +++ b/Application/Application.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + Musify.Application + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/ApplicationDependencyInjection.cs b/Application/ApplicationDependencyInjection.cs new file mode 100644 index 0000000..620f081 --- /dev/null +++ b/Application/ApplicationDependencyInjection.cs @@ -0,0 +1,6 @@ +namespace Musify.Application +{ + public static class ApplicationDependencyInjection + { + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs new file mode 100644 index 0000000..0d3a871 --- /dev/null +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Musify.Domain.Entities; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IDatabase + { + DbSet Users { get; } + + DbSet PlayLists { get; } + + DbSet Tracks { get; } + + DbSet PlayListHasTracks { get; } + + Task SaveChangesAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IKeycloakUserClient.cs b/Application/Contracts/Infrastructure/IKeycloakUserClient.cs new file mode 100644 index 0000000..688ef1d --- /dev/null +++ b/Application/Contracts/Infrastructure/IKeycloakUserClient.cs @@ -0,0 +1,22 @@ +using Ardalis.Result; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IKeycloakUserClient + { + Task> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken); + + Task> GetUsersAsync( + CancellationToken cancellationToken, + string search = "", + int first = 0, + int max = 20, + string username = ""); + + Task GetUsersCountAsync( + CancellationToken cancellationToken, + string search = "", + string username = ""); + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs new file mode 100644 index 0000000..e3af5f1 --- /dev/null +++ b/Application/Contracts/Infrastructure/IStorageHandler.cs @@ -0,0 +1,17 @@ +using Ardalis.Result; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IStorageHandler + { + Task> GetFile(string bucketName, string keyName, CancellationToken cancellationToken); + + Task GetUrl(string bucketName, string keyName, TimeSpan ExpirationTime, CancellationToken cancellationToken); + + Task> UploadFile(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken); + + Task TransferFiles(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); + + Task RemoveFile(string bucketName, string keyName, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/PlayLists/Contracts/PaginatedPlayListResponse.cs b/Application/PlayLists/Contracts/PaginatedPlayListResponse.cs new file mode 100644 index 0000000..1431574 --- /dev/null +++ b/Application/PlayLists/Contracts/PaginatedPlayListResponse.cs @@ -0,0 +1,12 @@ +namespace Musify.Application.PlayLists.Contracts +{ + public record PaginatedPlayListResponse( + IReadOnlyCollection PlayLists, + int PageNumber, + int PageSize, + int PageCount, + int TotalItemCount, + bool HasNextPage, + bool HasPreviousPage + ); +} \ No newline at end of file diff --git a/Application/PlayLists/Contracts/PlayListResponse.cs b/Application/PlayLists/Contracts/PlayListResponse.cs new file mode 100644 index 0000000..4d4e582 --- /dev/null +++ b/Application/PlayLists/Contracts/PlayListResponse.cs @@ -0,0 +1,10 @@ +namespace Musify.Application.PlayLists.Contracts +{ + public record PlayListResponse( + Guid Id, + string Name, + string Description, + string ImageName, + DateTime CreatedAt, + DateTime UpdatedAt); +} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs new file mode 100644 index 0000000..b57ebbc --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Queries.GetPlayListById +{ + public record GetPlayListByIdQuery(Guid Id) + : IRequest>; +} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs new file mode 100644 index 0000000..f2bd780 --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs @@ -0,0 +1,29 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Queries.GetPlayListById +{ + public class GetPlayListByIdQueryHandler(IDatabase database) + : IRequestHandler> > + { + public async Task> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken) + { + var playList = await database.PlayLists + .AsNoTracking() + .SingleOrDefaultAsync(p => p.Id == request.Id, cancellationToken); + + return playList is not null + ? Result.Success(new PlayListResponse( + playList.Id, + playList.Name, + playList.Description, + playList.ImageName, + playList.CreatedDate, + playList.UpdatedDate)) + : Result.NotFound("Playlist not found."); + } + } +} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs new file mode 100644 index 0000000..c01cceb --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Queries.GetPlayLists +{ + public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10) + : IRequest>; +} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs new file mode 100644 index 0000000..78ce41a --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs @@ -0,0 +1,44 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.PlayLists.Contracts; +using X.PagedList.EF; + +namespace Musify.Application.PlayLists.Queries.GetPlayLists +{ + public class GetPlayListsQueryHandler(IDatabase database) + : IRequestHandler> > + { + public async Task> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) + { + var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; + var pageSize = request.PageSize < 1 ? 10 : request.PageSize; + + var totalCount = await database.PlayLists.CountAsync(cancellationToken); + + var pagedPlayLists = await database.PlayLists + .AsNoTracking() + .OrderBy(p => p.Id) + .Select(p => new PlayListResponse( + p.Id, + p.Name, + p.Description, + p.ImageName, + p.CreatedDate, + p.UpdatedDate)) + .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + + var response = new PaginatedPlayListResponse( + pagedPlayLists.ToArray(), + pagedPlayLists.PageNumber, + pagedPlayLists.PageSize, + pagedPlayLists.PageCount, + pagedPlayLists.TotalItemCount, + pagedPlayLists.HasNextPage, + pagedPlayLists.HasPreviousPage); + + return Result.Success(response); + } + } +} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs new file mode 100644 index 0000000..0479ccb --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs @@ -0,0 +1,13 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId +{ + public record GetPlayListsByUserIdQuery( + Guid UserId, + string Name = "", + int PageNumber = 1, + int PageSize = 10) + : IRequest>; +} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs new file mode 100644 index 0000000..848802b --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs @@ -0,0 +1,51 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.PlayLists.Contracts; +using X.PagedList.EF; + +namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId +{ + public class GetPlayListsByUserIdQueryHandler(IDatabase database) + : IRequestHandler> > + { + public async Task> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) + { + var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; + var pageSize = request.PageSize < 1 ? 10 : request.PageSize; + var normalizedName = request.Name.Trim().ToUpperInvariant(); + + var playListsQuery = database.PlayLists + .AsNoTracking() + .Where(p => p.UserId == request.UserId); + + if (!string.IsNullOrWhiteSpace(normalizedName)) + playListsQuery = playListsQuery.Where(p => p.NormalizedName.Contains(normalizedName)); + + var totalCount = await playListsQuery.CountAsync(cancellationToken); + + var pagedPlayLists = await playListsQuery + .OrderBy(p => p.CreatedDate) + .Select(p => new PlayListResponse( + p.Id, + p.Name, + p.Description, + p.ImageName, + p.CreatedDate, + p.UpdatedDate)) + .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + + var response = new PaginatedPlayListResponse( + pagedPlayLists.ToArray(), + pagedPlayLists.PageNumber, + pagedPlayLists.PageSize, + pagedPlayLists.PageCount, + pagedPlayLists.TotalItemCount, + pagedPlayLists.HasNextPage, + pagedPlayLists.HasPreviousPage); + + return Result.Success(response); + } + } +} \ No newline at end of file diff --git a/Application/Users/Commands/CreateUser/CreateUserCommand.cs b/Application/Users/Commands/CreateUser/CreateUserCommand.cs new file mode 100644 index 0000000..519e26f --- /dev/null +++ b/Application/Users/Commands/CreateUser/CreateUserCommand.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Commands.CreateUser +{ + public record CreateUserCommand(Guid Id) + : IRequest>; +} \ No newline at end of file diff --git a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs b/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs new file mode 100644 index 0000000..f3f7773 --- /dev/null +++ b/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs @@ -0,0 +1,46 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Users.Contracts; +using Musify.Domain.Entities; + +namespace Musify.Application.Users.Commands.CreateUser +{ + public class CreateUserCommandHandler(IDatabase database, IKeycloakUserClient keycloakUserClient, ILogger logger) + : IRequestHandler >> + { + public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) + { + logger.LogDebug("Handling {CommandName} with Id: {UserId}", nameof(CreateUserCommand), request.Id); + + var existingUser = database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); + + if (existingUser is not null) + return Result.Conflict($"A user with Id {request.Id} already exists."); + + var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); + + if (!keycloakUser.IsSuccess) + return Result.NotFound("Keycloak user not found."); + + var newUser = new User + { + Id = request.Id + }; + + await database.Users.AddAsync(newUser, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + + return Result.Created(new UserResponse( + newUser.Id.ToString(), + keycloakUser.Value.Name, + keycloakUser.Value.FirstName, + keycloakUser.Value.SecondName, + newUser.CreatedAt, + newUser.UpdatedAt)); + } + } +} \ No newline at end of file diff --git a/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs b/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs new file mode 100644 index 0000000..571ce4d --- /dev/null +++ b/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs @@ -0,0 +1,8 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; + +namespace Musify.Application.Users.Commands.DeleteUser +{ + public record DeleteUserCommand(Guid Id) + : IRequest; +} \ No newline at end of file diff --git a/Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs b/Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs new file mode 100644 index 0000000..6cb9845 --- /dev/null +++ b/Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs @@ -0,0 +1,24 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Application.Users.Commands.DeleteUser +{ + public class DeleteUserCommandHandler(IDatabase database) + : IRequestHandler> + { + public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) + { + var existingUser = await database.Users.FindAsync(request.Id, cancellationToken); + + if (existingUser is null) + return Result.NotFound(); + + database.Users.Remove(existingUser); + + await database.SaveChangesAsync(cancellationToken); + + return Result.NoContent(); + } + } +} \ No newline at end of file diff --git a/Application/Users/Contracts/KeycloakUserResponse.cs b/Application/Users/Contracts/KeycloakUserResponse.cs new file mode 100644 index 0000000..c4ad490 --- /dev/null +++ b/Application/Users/Contracts/KeycloakUserResponse.cs @@ -0,0 +1,8 @@ +namespace Musify.Application.Users.Contracts +{ + public record KeycloakUserResponse( + Guid Id, + string Name, + string FirstName, + string SecondName); +} \ No newline at end of file diff --git a/Application/Users/Contracts/PaginatedUserResponse.cs b/Application/Users/Contracts/PaginatedUserResponse.cs new file mode 100644 index 0000000..042dd32 --- /dev/null +++ b/Application/Users/Contracts/PaginatedUserResponse.cs @@ -0,0 +1,12 @@ +namespace Musify.Application.Users.Contracts +{ + public record PaginatedUserResponse( + IReadOnlyCollection Users, + int PageNumber, + int PageSize, + int PageCount, + int TotalItemCount, + bool HasNextPage, + bool HasPreviousPage + ); +} \ No newline at end of file diff --git a/Application/Users/Contracts/UserResponse.cs b/Application/Users/Contracts/UserResponse.cs new file mode 100644 index 0000000..435536c --- /dev/null +++ b/Application/Users/Contracts/UserResponse.cs @@ -0,0 +1,10 @@ +namespace Musify.Application.Users.Contracts +{ + public record UserResponse( + string Id, + string Name, + string FirstName, + string SecondName, + DateTime CreatedAt, + DateTime UpdatedAt); +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs b/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs new file mode 100644 index 0000000..adc9340 --- /dev/null +++ b/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Queries.GetUserById +{ + public record GetUserByIdQuery(Guid Id) + : IRequest >>; +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs b/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs new file mode 100644 index 0000000..b6550c0 --- /dev/null +++ b/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs @@ -0,0 +1,35 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Queries.GetUserById +{ + public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) + : IRequestHandler>> + { + public async Task> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) + { + var user = await database.Users + .AsNoTracking() + .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); + + if (user is null) + return Result.NotFound("User not found."); + + var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); + + if (!keycloakUser.IsSuccess) + return Result.NotFound("Keycloak user not found."); + + return Result.Success(new UserResponse( + user.Id.ToString(), + keycloakUser.Value.Name, + keycloakUser.Value.FirstName, + keycloakUser.Value.SecondName, + user.CreatedAt, + user.UpdatedAt)); + } + } +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs new file mode 100644 index 0000000..811578a --- /dev/null +++ b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Queries.GetUserByKeycloakId +{ + public record GetUserByKeycloakQuery(Guid Id) + : IRequest >>; +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs new file mode 100644 index 0000000..a77ed6e --- /dev/null +++ b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs @@ -0,0 +1,35 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Queries.GetUserByKeycloakId +{ + public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) + : IRequestHandler>> + { + public async Task> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) + { + var user = await database.Users + .AsNoTracking() + .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); + + if (user is null) + return Result.NotFound("User not found."); + + var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); + + if (!keycloakUser.IsSuccess) + return Result.NotFound("Keycloak user not found."); + + return Result.Success(new UserResponse( + user.Id.ToString(), + keycloakUser.Value.Name, + keycloakUser.Value.FirstName, + keycloakUser.Value.SecondName, + user.CreatedAt, + user.UpdatedAt)); + } + } +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUsers/GetUsersCommand.cs b/Application/Users/Queries/GetUsers/GetUsersCommand.cs new file mode 100644 index 0000000..77e6969 --- /dev/null +++ b/Application/Users/Queries/GetUsers/GetUsersCommand.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Queries.GetUsers +{ + public record GetUsersCommand(int PageNumber, int PageSize, string UsernameSearch) + : IRequest> >; +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs b/Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs new file mode 100644 index 0000000..f975fff --- /dev/null +++ b/Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs @@ -0,0 +1,59 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Users.Contracts; +using X.PagedList; +using X.PagedList.Extensions; + +namespace Musify.Application.Users.Queries.GetUsers +{ + public class GetUsersQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) + : IRequestHandler >> + { + public async Task> Handle(GetUsersCommand request, CancellationToken cancellationToken) + { + var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; + var pageSize = request.PageSize < 1 ? 10 : request.PageSize; + var first = (pageNumber - 1) * pageSize; + + var keycloakUsers = await keycloakUserClient.GetUsersAsync( + cancellationToken, + first: first, + max: pageSize, + search: request.UsernameSearch); + + var keycloakIds = keycloakUsers.Select(u => u.Id).ToList(); + + var databaseUsers = await database.Users + .Where(u => keycloakIds.Contains(u.Id)) + .ToListAsync(cancellationToken); + + var users = databaseUsers + .Join(keycloakUsers, db => db.Id, kc => kc.Id, + (databaseSource, keycloakSource) => new UserResponse( + keycloakSource.Id.ToString(), + keycloakSource.Name, + keycloakSource.FirstName, + keycloakSource.SecondName, + databaseSource.CreatedAt, + databaseSource.UpdatedAt)) + .ToArray(); + + var totalItemCount = await keycloakUserClient.GetUsersCountAsync(cancellationToken, search: request.UsernameSearch); + + var pagedUsers = new StaticPagedList(users, pageNumber, pageSize, totalItemCount); + + var response = new PaginatedUserResponse( + pagedUsers.ToArray(), + pagedUsers.PageNumber, + pagedUsers.PageSize, + pagedUsers.PageCount, + pagedUsers.TotalItemCount, + pagedUsers.HasNextPage, + pagedUsers.HasPreviousPage); + + return Result.Success(response); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakConfiguration.cs b/Infrastructure/Identity/KeycloakConfiguration.cs new file mode 100644 index 0000000..385fe20 --- /dev/null +++ b/Infrastructure/Identity/KeycloakConfiguration.cs @@ -0,0 +1,15 @@ +namespace Musify.Infrastructure.Identity +{ + public class KeycloakConfiguration + { + public const string SectionName = "Keycloak"; + + public string BaseUrl { get; set; } = string.Empty; + + public string Realm { get; set; } = string.Empty; + + public string Username { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakIdentityService.cs b/Infrastructure/Identity/KeycloakIdentityService.cs new file mode 100644 index 0000000..ea4533d --- /dev/null +++ b/Infrastructure/Identity/KeycloakIdentityService.cs @@ -0,0 +1,61 @@ +using Ardalis.Result; +using Keycloak.Net; +using Keycloak.Net.Models.Users; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Users.Contracts; + +namespace Musify.Infrastructure.Identity +{ + public class KeycloakIdentityService( + KeycloakClient keycloakClient, + KeycloakConfiguration keycloakConfiguration, + ILogger logger) : IKeycloakUserClient + { + public async Task> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken) + { + User keycloakUser; + + try + { + keycloakUser = await keycloakClient.GetUserAsync(keycloakConfiguration.Realm, keycloakId, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + logger.LogWarning(exception, "Keycloak user with id {KeycloakId} could not be retrieved.", keycloakId); + + return Result.NotFound(); + } + + return Result.Success(KeycloakUserMapper.Map(keycloakUser)); + } + + public async Task> GetUsersAsync(CancellationToken cancellationToken, string search = "", int first = 0, int max = 20, string username = "") + { + var users = await keycloakClient.GetUsersAsync( + realm: keycloakConfiguration.Realm, + first: first, + max: max, + search: search, + username: username, + cancellationToken: cancellationToken); + + return users + .Select(KeycloakUserMapper.Map) + .ToArray(); + } + + public Task GetUsersCountAsync(CancellationToken cancellationToken, string search = "", string username = "") + { + return keycloakClient.GetUsersCountAsync( + keycloakConfiguration.Realm, + search: search, + username: username, + cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakUserMapper.cs b/Infrastructure/Identity/KeycloakUserMapper.cs new file mode 100644 index 0000000..768e069 --- /dev/null +++ b/Infrastructure/Identity/KeycloakUserMapper.cs @@ -0,0 +1,17 @@ +using Keycloak.Net.Models.Users; +using Musify.Application.Users.Contracts; + +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.")); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj new file mode 100644 index 0000000..8b630b2 --- /dev/null +++ b/Infrastructure/Infrastructure.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + Musify.Infrastructure + + + + + + + + + + + + + + diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs new file mode 100644 index 0000000..3ee112b --- /dev/null +++ b/Infrastructure/Persistence/Database.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.Persistence +{ + public class Database(DbContextOptions dbContextOptions) + : DbContext(dbContextOptions), IDatabase + { + public DbSet Users => Set(); + + public DbSet Tracks => Set(); + + public DbSet PlayLists => Set(); + + public DbSet PlayListHasTracks => Set(); + } +} \ No newline at end of file diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs new file mode 100644 index 0000000..7ad9d4c --- /dev/null +++ b/Infrastructure/Storage/StorageHandler.cs @@ -0,0 +1,115 @@ +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Transfer; +using Ardalis.Result; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Storage +{ + public class StorageHandler(IAmazonS3 amazonS3, ILogger logger) + : IStorageHandler + { + public async Task> GetFile(string bucketName, string keyName, CancellationToken cancellationToken) + { + var request = new GetObjectRequest + { + BucketName = bucketName, + Key = keyName, + }; + + logger.LogDebug("Getting file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + + try + { + var response = await amazonS3.GetObjectAsync(request, cancellationToken); + + logger.LogDebug("Successfully got file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + return Result.Success(response.ResponseStream); + } + catch + { + logger.LogDebug("Failed to get file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + return Result.NotFound(); + } + } + + public async Task GetUrl(string bucketName, string keyName, TimeSpan expirationTime, CancellationToken cancellationToken) + { + var request = new GetPreSignedUrlRequest + { + BucketName = bucketName, + Key = keyName, + Expires = DateTime.UtcNow + expirationTime, + Protocol = Protocol.HTTP + }; + + logger.LogDebug("Getting pre-signed URL from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + var url = await amazonS3.GetPreSignedURLAsync(request); + + return url; + } + + public async Task RemoveFile(string bucketName, string keyName, CancellationToken cancellationToken) + { + var request = new DeleteObjectRequest + { + BucketName = bucketName, + Key = keyName, + }; + + logger.LogDebug("Removing file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + var response = await amazonS3.DeleteObjectAsync(request, cancellationToken); + + return Result.NoContent(); + } + + public async Task TransferFiles(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken) + { + var trasnsferUtility = new TransferUtility(amazonS3); + + var files = Directory.GetFiles(sourceDirectory, "*"); + + foreach (var file in files) + { + string fileName = Path.GetFileName(file); + string filePath = Path.Combine(sourceDirectory, file); + + try + { + await trasnsferUtility.UploadAsync(filePath, bucketName, fileName, cancellationToken); + } + 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}"); + } + } + + return Result.Success(); + } + + public async Task> UploadFile(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken) + { + var request = new PutObjectRequest + { + BucketName = bucketName, + ContentType = contentType, + InputStream = sourceStream, + Key = keyName + }; + + try + { + var response = await amazonS3.PutObjectAsync(request, cancellationToken); + } + catch + { + logger.LogError("Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}"); + } + + return Result.Success(keyName); + } + } +} \ No newline at end of file diff --git a/Musify.Backend/Domain.csproj b/Musify.Backend/Domain.csproj new file mode 100644 index 0000000..c79d803 --- /dev/null +++ b/Musify.Backend/Domain.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + Musify.Domain + + + diff --git a/Musify.Backend/Entities/PlayList.cs b/Musify.Backend/Entities/PlayList.cs new file mode 100644 index 0000000..4247756 --- /dev/null +++ b/Musify.Backend/Entities/PlayList.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("PlayLists")] + public class PlayList + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required Guid UserId { get; set; } + + [Required] + [MaxLength(50)] + public required string Name { get; set; } + + [Required] + [MaxLength(50)] + public required string NormalizedName { get; set; } + + [Required] + [MaxLength(254)] + public required string Description { get; set; } = "No description was provided."; + + [Required] + [MaxLength(64)] + public required string ImageName { get; set; } + + [Required] + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + + [ForeignKey(nameof(UserId)) ] + public User User { get; set; } + + public ICollection PlayListTracks { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Musify.Backend/Entities/PlayListHasTrack.cs b/Musify.Backend/Entities/PlayListHasTrack.cs new file mode 100644 index 0000000..38d0c86 --- /dev/null +++ b/Musify.Backend/Entities/PlayListHasTrack.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("PlayListHasTrack")] + public class PlayListHasTrack + { + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required Guid PlayListId { get; set; } + + [Required] + public required Guid TrackId { get; set; } + + [Required] + public int Position { get; set; } = 0; + + [Required] + public DateTime AddedAt { get; set; } = DateTime.UtcNow; + + [ForeignKey(nameof(PlayListId)) ] + public PlayList PlayList { get; set; } + + [ForeignKey(nameof(TrackId)) ] + public Track? Track { get; set; } + } +} \ No newline at end of file diff --git a/Musify.Backend/Entities/Track.cs b/Musify.Backend/Entities/Track.cs new file mode 100644 index 0000000..6fae133 --- /dev/null +++ b/Musify.Backend/Entities/Track.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + public class Track + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + [MaxLength(50)] + public required string Title { get; set; } + + [Required] + [MaxLength(50)] + public required string NormalizedTitle { get; set; } + + [Required] + [MaxLength(64)] + public required string ImageName { get; set; } + + [Required] + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + + public ICollection Users { get; set; } = new List(); + + public ICollection PlayListTracks { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Musify.Backend/Entities/User.cs b/Musify.Backend/Entities/User.cs new file mode 100644 index 0000000..08784e3 --- /dev/null +++ b/Musify.Backend/Entities/User.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ + [Table("User")] + public class User + { + [Key] + public required Guid Id { get; set; } + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.Now; + + [Required] + public DateTime UpdatedAt { get; set; } = DateTime.Now; + + public ICollection Tracks { get; set; } = new List(); + + public ICollection PlayLists { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/MusifyBackend.slnx b/MusifyBackend.slnx new file mode 100644 index 0000000..7912f3e --- /dev/null +++ b/MusifyBackend.slnx @@ -0,0 +1,5 @@ + + + + + From a75f2c208745b361ebb2bc195451a77f020fe349 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 01:24:16 +0100 Subject: [PATCH 002/172] PictureHandler --- .../Infrastructure/IPictureHandler.cs | 9 ++++ Infrastructure/Infrastructure.csproj | 1 + Infrastructure/Pictures/PictureHandler.cs | 42 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 Application/Contracts/Infrastructure/IPictureHandler.cs create mode 100644 Infrastructure/Pictures/PictureHandler.cs diff --git a/Application/Contracts/Infrastructure/IPictureHandler.cs b/Application/Contracts/Infrastructure/IPictureHandler.cs new file mode 100644 index 0000000..63ba2e1 --- /dev/null +++ b/Application/Contracts/Infrastructure/IPictureHandler.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IPictureHandler + { + Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 8b630b2..7564525 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -11,6 +11,7 @@ + diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs new file mode 100644 index 0000000..6afa11b --- /dev/null +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -0,0 +1,42 @@ +using Ardalis.Result; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace Musify.Infrastructure.Pictures +{ + public class PictureHandler(ILogger logger) + : IPictureHandler + { + public async Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) + { + try + { + if (pictureStream.CanSeek && pictureStream.Position > 0) + pictureStream.Position = 0; + + using var picture = await Image.LoadAsync(pictureStream, cancellationToken); + + picture.Mutate(options => options.Resize(new ResizeOptions + { + Size = new Size(width, height), + Mode = ResizeMode.Max + } )); + + var memoryStream = new MemoryStream(); + + await picture.SaveAsWebpAsync(memoryStream, cancellationToken); + + memoryStream.Position = 0; + + return Result.Success(memoryStream); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while resizing the picture."); + return Result.Error("An error occurred while resizing the picture."); + } + } + } +} \ No newline at end of file From cd8b8cbd0bbfca963f1adf3353975cd2f6e704b3 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 02:19:36 +0100 Subject: [PATCH 003/172] Uploads --- Application/Application.csproj | 2 +- .../Configuration/PlayListConfiguration.cs | 30 ++++++ .../Configuration/StorageConfiguration.cs | 14 +++ .../Contracts/Infrastructure/IDatabase.cs | 2 + .../Contracts/Infrastructure/IEventBus.cs | 7 ++ .../Infrastructure/IStorageHandler.cs | 10 +- .../Commands/ResizeImage/ResizeCommand.cs | 13 +++ .../ResizeImage/ResizeCommandHandler.cs | 55 +++++++++++ .../Pictures/Events/ImageResizeEvent.cs | 8 ++ Infrastructure/Infrastructure.csproj | 1 + .../Consumers/ImageResizedConsumer.cs | 99 +++++++++++++++++++ .../Messaging/MassTransitEventBus.cs | 16 +++ Infrastructure/Persistence/Database.cs | 2 + Infrastructure/Pictures/PictureHandler.cs | 2 +- Infrastructure/Storage/StorageHandler.cs | 10 +- Musify.Backend/Entities/Upload.cs | 27 +++++ Musify.Backend/Entities/UploadState.cs | 10 ++ 17 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 Application/Configuration/PlayListConfiguration.cs create mode 100644 Application/Configuration/StorageConfiguration.cs create mode 100644 Application/Contracts/Infrastructure/IEventBus.cs create mode 100644 Application/Pictures/Commands/ResizeImage/ResizeCommand.cs create mode 100644 Application/Pictures/Commands/ResizeImage/ResizeCommandHandler.cs create mode 100644 Application/Pictures/Events/ImageResizeEvent.cs create mode 100644 Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs create mode 100644 Infrastructure/Messaging/MassTransitEventBus.cs create mode 100644 Musify.Backend/Entities/Upload.cs create mode 100644 Musify.Backend/Entities/UploadState.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index d063a70..1764b90 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -10,6 +10,7 @@ + @@ -20,7 +21,6 @@ - diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs new file mode 100644 index 0000000..ea14d99 --- /dev/null +++ b/Application/Configuration/PlayListConfiguration.cs @@ -0,0 +1,30 @@ +namespace Musify.Application.Configuration +{ + public class PlayListConfiguration + { + public PlayListRoutes Routes { get; set; } = new PlayListRoutes(); + + public int SmallPictureWidth { get; set; } = 128; + + public int SmallPictureHeight { get; set; } = 128; + + public int MediumPictureWidth { get; private set; } = 256; + + public int MediumPictureHeight { get; private set; } = 256; + + public int LargePictureWidth { get; private set; } = 512; + + public float LargePictureHeight { get; private set; } = 512; + } + + public class PlayListRoutes + { + public string ParentFolder { get; set; } = "PlayLists"; + + public string SmallPictures { get; set; } = "SmallPictures"; + + public string MediumPictures { get; set; } = "MediumPictures"; + + public string LargePictures { get; set; } = "LargePictures"; + } +} \ No newline at end of file diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs new file mode 100644 index 0000000..8cf6c49 --- /dev/null +++ b/Application/Configuration/StorageConfiguration.cs @@ -0,0 +1,14 @@ +namespace Musify.Application.Configuration +{ + public class StorageConfiguration + { + public string BucketName { get; set; } = "musify-S3"; + + public StorageRoutes Routes { get; set; } = new StorageRoutes(); + } + + public class StorageRoutes + { + public string Uploads { get; set; } = "Uploads"; + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 0d3a871..172bfd8 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -13,6 +13,8 @@ namespace Musify.Application.Contracts.Infrastructure DbSet PlayListHasTracks { get; } + DbSet Uploads { get; } + Task SaveChangesAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IEventBus.cs b/Application/Contracts/Infrastructure/IEventBus.cs new file mode 100644 index 0000000..0091321 --- /dev/null +++ b/Application/Contracts/Infrastructure/IEventBus.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IEventBus + { + Task PublishAsync(T message, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs index e3af5f1..613b524 100644 --- a/Application/Contracts/Infrastructure/IStorageHandler.cs +++ b/Application/Contracts/Infrastructure/IStorageHandler.cs @@ -4,14 +4,14 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IStorageHandler { - Task> GetFile(string bucketName, string keyName, CancellationToken cancellationToken); + Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken); - Task GetUrl(string bucketName, string keyName, TimeSpan ExpirationTime, CancellationToken cancellationToken); + Task GetUrlAsync(string bucketName, string keyName, TimeSpan ExpirationTime, CancellationToken cancellationToken); - Task> UploadFile(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken); + Task> UploadFileAsync(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken); - Task TransferFiles(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); + Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); - Task RemoveFile(string bucketName, string keyName, CancellationToken cancellationToken); + Task RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Pictures/Commands/ResizeImage/ResizeCommand.cs b/Application/Pictures/Commands/ResizeImage/ResizeCommand.cs new file mode 100644 index 0000000..c79b5e5 --- /dev/null +++ b/Application/Pictures/Commands/ResizeImage/ResizeCommand.cs @@ -0,0 +1,13 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; + +namespace Musify.Application.Pictures.Commands.ResizeImage +{ + public record ResizeCommand( + Stream PictureStream, + string ContentType, + int Width, + int Height, + string SaveRoute) + : IRequest>; +} \ No newline at end of file diff --git a/Application/Pictures/Commands/ResizeImage/ResizeCommandHandler.cs b/Application/Pictures/Commands/ResizeImage/ResizeCommandHandler.cs new file mode 100644 index 0000000..037992b --- /dev/null +++ b/Application/Pictures/Commands/ResizeImage/ResizeCommandHandler.cs @@ -0,0 +1,55 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Pictures.Events; +using Musify.Domain.Entities; + +namespace Musify.Application.Pictures.Commands.ResizeImage +{ + public class ResizeCommandHandler(IStorageHandler storageHandler, IDatabase database, IPublishEndpoint publishEndpoint, + ILogger logger, StorageConfiguration storageConfiguration) + : IRequestHandler >> + { + public async Task> Handle(ResizeCommand request, CancellationToken cancellationToken) + { + var pictureId = Guid.NewGuid(); + + var upload = new Upload + { + Id = pictureId, + KeyName = $"{storageConfiguration.Routes.Uploads}/{pictureId}", + ContentType = request.ContentType, + BucketName = storageConfiguration.BucketName, + }; + + await database.Uploads.AddAsync(upload, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + + var uploadResult = await storageHandler.UploadFileAsync( + request.PictureStream, + request.ContentType, + storageConfiguration.BucketName, + pictureId.ToString(), + cancellationToken); + + if (!uploadResult.IsSuccess) + { + logger.LogError("Failed to upload the picture with ID {PictureId} to bucket {BucketName}.", pictureId, storageConfiguration.BucketName); + return Result.Error("Failed to upload the picture."); + } + + await publishEndpoint.Publish(new ImageResizeEvent( + pictureId, + request.Width, + request.Height, + request.SaveRoute), + cancellationToken); + + return Result.Success(); + } + } +} \ No newline at end of file diff --git a/Application/Pictures/Events/ImageResizeEvent.cs b/Application/Pictures/Events/ImageResizeEvent.cs new file mode 100644 index 0000000..c08aec6 --- /dev/null +++ b/Application/Pictures/Events/ImageResizeEvent.cs @@ -0,0 +1,8 @@ +namespace Musify.Application.Pictures.Events +{ + public record ImageResizeEvent( + Guid UploadId, + int Width, + int Height, + string SaveRoute); +} \ No newline at end of file diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 7564525..4a82fe5 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -10,6 +10,7 @@ + diff --git a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs new file mode 100644 index 0000000..6e386fa --- /dev/null +++ b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs @@ -0,0 +1,99 @@ +using Ardalis.Result; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Pictures.Events; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class ImageResizedConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) + : IConsumer + { + public async Task Consume(ConsumeContext consumeContext) + { + try + { + var upload = await database.Uploads.AsNoTracking().SingleOrDefaultAsync(u => u.Id == consumeContext.Message.UploadId); + + if (upload is null) + { + logger.LogWarning("Upload not found in database: UploadId={UploadId}", consumeContext.Message.UploadId); + return; + } + + if (upload.State != UploadState.Pending) + { + logger.LogInformation("Upload is not in a failed state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State); + return; + } + + await UpdateState(database, upload, UploadState.Processing, consumeContext.CancellationToken); + + var file = await storageHandler.GetFileAsync( + upload.BucketName, + upload.KeyName, + consumeContext.CancellationToken); + + if (file.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; + } + + var pictureResult = await pictureHandler.ResizePictureAsync( + file.Value, + consumeContext.Message.Width, + consumeContext.Message.Height, + consumeContext.CancellationToken); + + if (!pictureResult.IsSuccess) + { + string errors = string.Join("; ", pictureResult.Errors); + logger.LogError("Failed to resize picture: {ErrorMessage}", errors); + + await UpdateState(database, upload, UploadState.Failed, consumeContext.CancellationToken); + + return; + } + + var saveResult = await storageHandler.UploadFileAsync( + pictureResult.Value, + "image/webp", + upload.BucketName, + $"{consumeContext.Message.SaveRoute}/{upload.Id}.webp", + consumeContext.CancellationToken); + + if (!saveResult.IsSuccess) + { + string errors = string.Join("; ", saveResult.Errors); + logger.LogError("Failed to upload resized picture: {ErrorMessage}", errors); + + await UpdateState(database, upload, UploadState.Failed, consumeContext.CancellationToken); + + return; + } + + await UpdateState(database, upload, UploadState.Completed, consumeContext.CancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while consuming the ImageResizedConsumer message."); + throw; + } + } + + async Task UpdateState(IDatabase database, Upload upload, UploadState uploadState, CancellationToken cancellationToken) + { + upload.State = uploadState; + + database.Uploads.Update(upload); + + await database.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitEventBus.cs b/Infrastructure/Messaging/MassTransitEventBus.cs new file mode 100644 index 0000000..0e15411 --- /dev/null +++ b/Infrastructure/Messaging/MassTransitEventBus.cs @@ -0,0 +1,16 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Messaging +{ + public class MassTransitEventBus(IPublishEndpoint publishEndpoint) + : IEventBus + { + public async Task PublishAsync(T message, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(message, nameof(message)); + + await publishEndpoint.Publish(message, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 3ee112b..cf4b52d 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -14,5 +14,7 @@ namespace Musify.Infrastructure.Persistence public DbSet PlayLists => Set(); public DbSet PlayListHasTracks => Set(); + + public DbSet Uploads => Set(); } } \ No newline at end of file diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index 6afa11b..1e5db72 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -11,7 +11,7 @@ namespace Musify.Infrastructure.Pictures { public async Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) { - try + try { if (pictureStream.CanSeek && pictureStream.Position > 0) pictureStream.Position = 0; diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index 7ad9d4c..b95f616 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -10,7 +10,7 @@ namespace Musify.Infrastructure.Storage public class StorageHandler(IAmazonS3 amazonS3, ILogger logger) : IStorageHandler { - public async Task> GetFile(string bucketName, string keyName, CancellationToken cancellationToken) + public async Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) { var request = new GetObjectRequest { @@ -34,7 +34,7 @@ namespace Musify.Infrastructure.Storage } } - public async Task GetUrl(string bucketName, string keyName, TimeSpan expirationTime, CancellationToken cancellationToken) + public async Task GetUrlAsync(string bucketName, string keyName, TimeSpan expirationTime, CancellationToken cancellationToken) { var request = new GetPreSignedUrlRequest { @@ -50,7 +50,7 @@ namespace Musify.Infrastructure.Storage return url; } - public async Task RemoveFile(string bucketName, string keyName, CancellationToken cancellationToken) + public async Task RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) { var request = new DeleteObjectRequest { @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.Storage return Result.NoContent(); } - public async Task TransferFiles(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken) + public async Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken) { var trasnsferUtility = new TransferUtility(amazonS3); @@ -89,7 +89,7 @@ namespace Musify.Infrastructure.Storage return Result.Success(); } - public async Task> UploadFile(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken) + public async Task> UploadFileAsync(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken) { var request = new PutObjectRequest { diff --git a/Musify.Backend/Entities/Upload.cs b/Musify.Backend/Entities/Upload.cs new file mode 100644 index 0000000..0bb727c --- /dev/null +++ b/Musify.Backend/Entities/Upload.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ + [Table("Upload")] + public class Upload + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required string KeyName { get; set; } + + [Required] + public required string BucketName { get; set; } + + [Required] + public required string ContentType { get; set; } + + [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/Musify.Backend/Entities/UploadState.cs b/Musify.Backend/Entities/UploadState.cs new file mode 100644 index 0000000..738b5d4 --- /dev/null +++ b/Musify.Backend/Entities/UploadState.cs @@ -0,0 +1,10 @@ +namespace Musify.Domain.Entities +{ + public enum UploadState + { + Pending, + Processing, + Completed, + Failed + } +} \ No newline at end of file From 199ff3f104361b1b2b6bf0f1635de54c5cbdc6e1 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 02:22:44 +0100 Subject: [PATCH 004/172] Using --- .../Messaging/Consumers/ImageResizedConsumer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs index 6e386fa..303f268 100644 --- a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs @@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { try { - var upload = await database.Uploads.AsNoTracking().SingleOrDefaultAsync(u => u.Id == consumeContext.Message.UploadId); + var upload = await database.Uploads.SingleOrDefaultAsync(u => u.Id == consumeContext.Message.UploadId); if (upload is null) { @@ -25,7 +25,7 @@ namespace Musify.Infrastructure.Messaging.Consumers if (upload.State != UploadState.Pending) { - logger.LogInformation("Upload is not in a failed state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State); + logger.LogInformation("Upload is not in a pending state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State); return; } @@ -45,8 +45,10 @@ namespace Musify.Infrastructure.Messaging.Consumers return; } + using var fileStream = file.Value; + var pictureResult = await pictureHandler.ResizePictureAsync( - file.Value, + fileStream, consumeContext.Message.Width, consumeContext.Message.Height, consumeContext.CancellationToken); From 2549dc7a784892dbfc4b5ffe380bf584fec4e6ce Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 10:26:40 +0100 Subject: [PATCH 005/172] Upload playlist image --- Application/Application.csproj | 2 - .../Configuration/PlayListConfiguration.cs | 8 ++- .../Contracts/Application/IPictureService.cs | 11 ++++ .../ResizeCommand.cs | 10 +-- .../ResizePicture/ResizeCommandHandler.cs | 17 ++++++ .../Pictures/Contracts/PictureResize.cs | 6 ++ .../Pictures/Events/ImageResizeEvent.cs | 8 --- .../Pictures/Events/PictureResizeEvent.cs | 8 +++ ...izeCommandHandler.cs => PictureService.cs} | 28 ++++----- .../CreatePlayList/CreatePlayListCommand.cs | 14 +++++ .../CreatePlayListCommandHandler.cs | 61 +++++++++++++++++++ .../PlayLists/Contracts/PlayListResponse.cs | 24 +++++++- .../Consumers/ImageResizedConsumer.cs | 4 +- Musify.Backend/Entities/PlayList.cs | 10 ++- 14 files changed, 174 insertions(+), 37 deletions(-) create mode 100644 Application/Contracts/Application/IPictureService.cs rename Application/Pictures/Commands/{ResizeImage => ResizePicture}/ResizeCommand.cs (54%) create mode 100644 Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs create mode 100644 Application/Pictures/Contracts/PictureResize.cs delete mode 100644 Application/Pictures/Events/ImageResizeEvent.cs create mode 100644 Application/Pictures/Events/PictureResizeEvent.cs rename Application/Pictures/{Commands/ResizeImage/ResizeCommandHandler.cs => PictureService.cs} (61%) create mode 100644 Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs create mode 100644 Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index 1764b90..d1a1586 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -21,8 +21,6 @@ - - diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index ea14d99..fea1cf7 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -14,7 +14,7 @@ public int LargePictureWidth { get; private set; } = 512; - public float LargePictureHeight { get; private set; } = 512; + public int LargePictureHeight { get; private set; } = 512; } public class PlayListRoutes @@ -26,5 +26,11 @@ public string MediumPictures { get; set; } = "MediumPictures"; public string LargePictures { get; set; } = "LargePictures"; + + public string DefaultSmallPicture { get; set; } = "DefaultSmallPicture.webp"; + + public string DefaultMediumPicture { get; set; } = "DefaultMediumPicture.webp"; + + public string DefaultLargePicture { get; set; } = "DefaultLargePicture.webp"; } } \ No newline at end of file diff --git a/Application/Contracts/Application/IPictureService.cs b/Application/Contracts/Application/IPictureService.cs new file mode 100644 index 0000000..5adba92 --- /dev/null +++ b/Application/Contracts/Application/IPictureService.cs @@ -0,0 +1,11 @@ +using Ardalis.Result; +using Musify.Application.Pictures.Contracts; + +namespace Musify.Application.Contracts.Application +{ + public interface IPictureService + { + Task> ResizePictureAsync(string keyName, string contentType, Stream pictureStream, PictureResize[] pictureResizes, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/Pictures/Commands/ResizeImage/ResizeCommand.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs similarity index 54% rename from Application/Pictures/Commands/ResizeImage/ResizeCommand.cs rename to Application/Pictures/Commands/ResizePicture/ResizeCommand.cs index c79b5e5..628ad6d 100644 --- a/Application/Pictures/Commands/ResizeImage/ResizeCommand.cs +++ b/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs @@ -1,13 +1,13 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; +using Musify.Application.Pictures.Contracts; -namespace Musify.Application.Pictures.Commands.ResizeImage +namespace Musify.Application.Pictures.Commands.ResizePicture { public record ResizeCommand( - Stream PictureStream, + string KeyName, string ContentType, - int Width, - int Height, - string SaveRoute) + 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 new file mode 100644 index 0000000..95b61fd --- /dev/null +++ b/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs @@ -0,0 +1,17 @@ +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.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 new file mode 100644 index 0000000..44b79fa --- /dev/null +++ b/Application/Pictures/Contracts/PictureResize.cs @@ -0,0 +1,6 @@ +namespace Musify.Application.Pictures.Contracts +{ + public record PictureResize( + int Width, + int Height); +} \ No newline at end of file diff --git a/Application/Pictures/Events/ImageResizeEvent.cs b/Application/Pictures/Events/ImageResizeEvent.cs deleted file mode 100644 index c08aec6..0000000 --- a/Application/Pictures/Events/ImageResizeEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Musify.Application.Pictures.Events -{ - public record ImageResizeEvent( - Guid UploadId, - int Width, - int Height, - string SaveRoute); -} \ No newline at end of file diff --git a/Application/Pictures/Events/PictureResizeEvent.cs b/Application/Pictures/Events/PictureResizeEvent.cs new file mode 100644 index 0000000..8439e14 --- /dev/null +++ b/Application/Pictures/Events/PictureResizeEvent.cs @@ -0,0 +1,8 @@ +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/Commands/ResizeImage/ResizeCommandHandler.cs b/Application/Pictures/PictureService.cs similarity index 61% rename from Application/Pictures/Commands/ResizeImage/ResizeCommandHandler.cs rename to Application/Pictures/PictureService.cs index 037992b..306131f 100644 --- a/Application/Pictures/Commands/ResizeImage/ResizeCommandHandler.cs +++ b/Application/Pictures/PictureService.cs @@ -1,19 +1,22 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; 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.Commands.ResizeImage +namespace Musify.Application.Pictures { - public class ResizeCommandHandler(IStorageHandler storageHandler, IDatabase database, IPublishEndpoint publishEndpoint, + public class PictureService(IStorageHandler storageHandler, IDatabase database, IPublishEndpoint publishEndpoint, ILogger logger, StorageConfiguration storageConfiguration) - : IRequestHandler >> + : IPictureService { - public async Task> Handle(ResizeCommand request, CancellationToken cancellationToken) + public async Task> ResizePictureAsync(string keyName, string contentType, Stream pictureStream, PictureResize[] pictureResizes, + CancellationToken cancellationToken) { var pictureId = Guid.NewGuid(); @@ -21,7 +24,7 @@ namespace Musify.Application.Pictures.Commands.ResizeImage { Id = pictureId, KeyName = $"{storageConfiguration.Routes.Uploads}/{pictureId}", - ContentType = request.ContentType, + ContentType = contentType, BucketName = storageConfiguration.BucketName, }; @@ -30,10 +33,10 @@ namespace Musify.Application.Pictures.Commands.ResizeImage await database.SaveChangesAsync(cancellationToken); var uploadResult = await storageHandler.UploadFileAsync( - request.PictureStream, - request.ContentType, + pictureStream, + contentType, storageConfiguration.BucketName, - pictureId.ToString(), + $"{storageConfiguration.Routes.Uploads}/{pictureId}", cancellationToken); if (!uploadResult.IsSuccess) @@ -42,12 +45,7 @@ namespace Musify.Application.Pictures.Commands.ResizeImage return Result.Error("Failed to upload the picture."); } - await publishEndpoint.Publish(new ImageResizeEvent( - pictureId, - request.Width, - request.Height, - request.SaveRoute), - cancellationToken); + await publishEndpoint.Publish(new PictureResizeEvent(pictureId, pictureResizes)); return Result.Success(); } diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs new file mode 100644 index 0000000..b55d20a --- /dev/null +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs @@ -0,0 +1,14 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Commands.CreatePlayList +{ + public record CreatePlayListCommand( + Guid UserId, + string Name, + string Description, + Stream PictureStream, + 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 new file mode 100644 index 0000000..2ac9fe3 --- /dev/null +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -0,0 +1,61 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +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.PlayLists.Contracts; +using Musify.Domain.Entities; + +namespace Musify.Application.PlayLists.Commands.CreatePlayList +{ + public class CreatePlayListCommandHandler(IPictureService pictureService, IDatabase database, ILogger logger, + PlayListConfiguration playListConfiguration) + : IRequestHandler> > + { + public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) + { + var userExists = await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken); + + if (!userExists) + { + logger.LogWarning("User with Id={UserId} does not exist.", request.UserId); + return Result.NotFound($"User with Id {request.UserId} does not exist."); + } + + var playList = new PlayList + { + UserId = request.UserId, + Name = request.Name, + NormalizedName = request.Name.Trim().ToUpperInvariant(), + Description = request.Description, + SmallPictureKeyName = playListConfiguration.Routes.SmallPictures, + MediumPictureKeyName = playListConfiguration.Routes.MediumPictures, + LargePictureKeyName = playListConfiguration.Routes.LargePictures, + }; + + await database.PlayLists.AddAsync(playList, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("PlayList with Id={PlayListId} created successfully for UserId={UserId}.", playList.Id, request.UserId); + + await pictureService.ResizePictureAsync( + playList.Id.ToString(), + request.PictureContentType, + request.PictureStream, + [ + new PictureResize(playListConfiguration.SmallPictureWidth, playListConfiguration.SmallPictureHeight), + new PictureResize(playListConfiguration.MediumPictureWidth, playListConfiguration.MediumPictureHeight), + new PictureResize(playListConfiguration.LargePictureWidth, playListConfiguration.LargePictureHeight) + ], + cancellationToken); + + logger.LogInformation("Picture for PlayList with Id={PlayListId} ", playList.Id); + + return Result.Created(PlayListResponse.Map(playList)); + } + } +} \ No newline at end of file diff --git a/Application/PlayLists/Contracts/PlayListResponse.cs b/Application/PlayLists/Contracts/PlayListResponse.cs index 4d4e582..ba96c40 100644 --- a/Application/PlayLists/Contracts/PlayListResponse.cs +++ b/Application/PlayLists/Contracts/PlayListResponse.cs @@ -1,10 +1,28 @@ -namespace Musify.Application.PlayLists.Contracts +using Musify.Domain.Entities; + +namespace Musify.Application.PlayLists.Contracts { public record PlayListResponse( Guid Id, string Name, string Description, - string ImageName, + string SmallImageKeyName, + string MediumImageKeyName, + string LargeImageKeyName, DateTime CreatedAt, - DateTime UpdatedAt); + DateTime UpdatedAt) + { + public static PlayListResponse Map(PlayList playList) + { + return new PlayListResponse( + playList.Id, + playList.Name, + playList.Description, + playList.SmallPictureKeyName, + playList.MediumPictureKeyName, + playList.LargePictureKeyName, + playList.CreatedDate, + playList.UpdatedDate); + } + } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs index 303f268..a03800b 100644 --- a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs @@ -9,9 +9,9 @@ using Musify.Domain.Entities; namespace Musify.Infrastructure.Messaging.Consumers { public class ImageResizedConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) - : IConsumer + : IConsumer { - public async Task Consume(ConsumeContext consumeContext) + public async Task Consume(ConsumeContext consumeContext) { try { diff --git a/Musify.Backend/Entities/PlayList.cs b/Musify.Backend/Entities/PlayList.cs index 4247756..44d751f 100644 --- a/Musify.Backend/Entities/PlayList.cs +++ b/Musify.Backend/Entities/PlayList.cs @@ -27,7 +27,15 @@ namespace Musify.Domain.Entities [Required] [MaxLength(64)] - public required string ImageName { get; set; } + public required string SmallPictureKeyName { get; set; } + + [Required] + [MaxLength(64)] + public required string MediumPictureKeyName { get; set; } + + [Required] + [MaxLength(64)] + public required string LargePictureKeyName { get; set; } [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; From 9ba073ec64cbdec66af8d7b3700db6ad9eacfe2f Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 10:52:00 +0100 Subject: [PATCH 006/172] Changes and fixes --- .../Contracts/Application/IPictureService.cs | 5 +- .../Commands/ResizePicture/ResizeCommand.cs | 3 + .../ResizePicture/ResizeCommandHandler.cs | 3 +- .../Pictures/Contracts/PictureResize.cs | 3 +- Application/Pictures/PictureService.cs | 6 +- .../CreatePlayListCommandHandler.cs | 13 +++- .../PlayLists/Contracts/PlayListResponse.cs | 2 +- .../GetPlayListByIdQueryHandler.cs | 8 +- .../GetPlayLists/GetPlayListsQueryHandler.cs | 9 +-- .../GetPlayListsByUserIdQueryHandler.cs | 9 +-- .../Consumers/ImageResizedConsumer.cs | 76 +++++++++++-------- Musify.Backend/Entities/EntityType.cs | 9 +++ Musify.Backend/Entities/Upload.cs | 6 ++ 13 files changed, 87 insertions(+), 65 deletions(-) create mode 100644 Musify.Backend/Entities/EntityType.cs diff --git a/Application/Contracts/Application/IPictureService.cs b/Application/Contracts/Application/IPictureService.cs index 5adba92..b8bab3a 100644 --- a/Application/Contracts/Application/IPictureService.cs +++ b/Application/Contracts/Application/IPictureService.cs @@ -1,11 +1,12 @@ using Ardalis.Result; using Musify.Application.Pictures.Contracts; +using Musify.Domain.Entities; namespace Musify.Application.Contracts.Application { public interface IPictureService { - Task> ResizePictureAsync(string keyName, string contentType, Stream pictureStream, PictureResize[] pictureResizes, - CancellationToken cancellationToken); + 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/Pictures/Commands/ResizePicture/ResizeCommand.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs index 628ad6d..707ed8d 100644 --- a/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs +++ b/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs @@ -1,10 +1,13 @@ 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, diff --git a/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs index 95b61fd..bd61a9c 100644 --- a/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs +++ b/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs @@ -9,7 +9,8 @@ namespace Musify.Application.Pictures.Commands.ResizePicture { public async Task> Handle(ResizeCommand request, CancellationToken cancellationToken) { - var result = await pictureService.ResizePictureAsync(request.KeyName, request.ContentType, request.PictureStream, request.PictureResizes, cancellationToken); + var result = await pictureService.ResizePictureAsync(request.EntityType, request.EntityId, request.KeyName, request.ContentType, + request.PictureStream, request.PictureResizes, cancellationToken); return result; } diff --git a/Application/Pictures/Contracts/PictureResize.cs b/Application/Pictures/Contracts/PictureResize.cs index 44b79fa..f66021a 100644 --- a/Application/Pictures/Contracts/PictureResize.cs +++ b/Application/Pictures/Contracts/PictureResize.cs @@ -2,5 +2,6 @@ { public record PictureResize( int Width, - int Height); + int Height, + string SaveOnRoute); } \ No newline at end of file diff --git a/Application/Pictures/PictureService.cs b/Application/Pictures/PictureService.cs index 306131f..c061995 100644 --- a/Application/Pictures/PictureService.cs +++ b/Application/Pictures/PictureService.cs @@ -15,8 +15,8 @@ namespace Musify.Application.Pictures ILogger logger, StorageConfiguration storageConfiguration) : IPictureService { - public async Task> ResizePictureAsync(string keyName, string contentType, Stream pictureStream, PictureResize[] pictureResizes, - CancellationToken cancellationToken) + public async Task> ResizePictureAsync(EntityType entityType, Guid EntityId, string keyName, string contentType, Stream pictureStream, + PictureResize[] pictureResizes, CancellationToken cancellationToken) { var pictureId = Guid.NewGuid(); @@ -26,6 +26,8 @@ namespace Musify.Application.Pictures KeyName = $"{storageConfiguration.Routes.Uploads}/{pictureId}", ContentType = contentType, BucketName = storageConfiguration.BucketName, + EntityType = entityType, + EntityId = EntityId, }; await database.Uploads.AddAsync(upload, cancellationToken); diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 2ac9fe3..76851f7 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -43,19 +43,24 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList 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(playListConfiguration.SmallPictureWidth, playListConfiguration.SmallPictureHeight), - new PictureResize(playListConfiguration.MediumPictureWidth, playListConfiguration.MediumPictureHeight), - new PictureResize(playListConfiguration.LargePictureWidth, playListConfiguration.LargePictureHeight) + new PictureResize(playListConfiguration.SmallPictureWidth, + playListConfiguration.SmallPictureHeight, playListConfiguration.Routes.SmallPictures), + new PictureResize(playListConfiguration.MediumPictureWidth, + playListConfiguration.MediumPictureHeight, playListConfiguration.Routes.MediumPictures), + new PictureResize(playListConfiguration.LargePictureWidth, + playListConfiguration.LargePictureHeight, playListConfiguration.Routes.LargePictures) ], cancellationToken); logger.LogInformation("Picture for PlayList with Id={PlayListId} ", playList.Id); - return Result.Created(PlayListResponse.Map(playList)); + return Result.Created(PlayListResponse.FromEntity(playList)); } } } \ No newline at end of file diff --git a/Application/PlayLists/Contracts/PlayListResponse.cs b/Application/PlayLists/Contracts/PlayListResponse.cs index ba96c40..8ec1f54 100644 --- a/Application/PlayLists/Contracts/PlayListResponse.cs +++ b/Application/PlayLists/Contracts/PlayListResponse.cs @@ -12,7 +12,7 @@ namespace Musify.Application.PlayLists.Contracts DateTime CreatedAt, DateTime UpdatedAt) { - public static PlayListResponse Map(PlayList playList) + public static PlayListResponse FromEntity(PlayList playList) { return new PlayListResponse( playList.Id, diff --git a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs index f2bd780..f2313f4 100644 --- a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs @@ -16,13 +16,7 @@ namespace Musify.Application.PlayLists.Queries.GetPlayListById .SingleOrDefaultAsync(p => p.Id == request.Id, cancellationToken); return playList is not null - ? Result.Success(new PlayListResponse( - playList.Id, - playList.Name, - playList.Description, - playList.ImageName, - playList.CreatedDate, - playList.UpdatedDate)) + ? Result.Success(PlayListResponse.FromEntity(playList)) : Result.NotFound("Playlist not found."); } } diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs index 78ce41a..ecda141 100644 --- a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs @@ -3,6 +3,7 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Contracts; +using Musify.Domain.Entities; using X.PagedList.EF; namespace Musify.Application.PlayLists.Queries.GetPlayLists @@ -20,13 +21,7 @@ namespace Musify.Application.PlayLists.Queries.GetPlayLists var pagedPlayLists = await database.PlayLists .AsNoTracking() .OrderBy(p => p.Id) - .Select(p => new PlayListResponse( - p.Id, - p.Name, - p.Description, - p.ImageName, - p.CreatedDate, - p.UpdatedDate)) + .Select(p => PlayListResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); var response = new PaginatedPlayListResponse( diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs index 848802b..328895f 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs @@ -3,6 +3,7 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Contracts; +using Musify.Domain.Entities; using X.PagedList.EF; namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId @@ -27,13 +28,7 @@ namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId var pagedPlayLists = await playListsQuery .OrderBy(p => p.CreatedDate) - .Select(p => new PlayListResponse( - p.Id, - p.Name, - p.Description, - p.ImageName, - p.CreatedDate, - p.UpdatedDate)) + .Select(p => PlayListResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); var response = new PaginatedPlayListResponse( diff --git a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs index a03800b..88280bb 100644 --- a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs @@ -1,6 +1,5 @@ using Ardalis.Result; using MassTransit; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Pictures.Events; @@ -15,7 +14,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { try { - var upload = await database.Uploads.SingleOrDefaultAsync(u => u.Id == consumeContext.Message.UploadId); + var upload = await database.Uploads.FindAsync(consumeContext.Message.UploadId); if (upload is null) { @@ -31,53 +30,64 @@ namespace Musify.Infrastructure.Messaging.Consumers await UpdateState(database, upload, UploadState.Processing, consumeContext.CancellationToken); - var file = await storageHandler.GetFileAsync( + var fileResult = await storageHandler.GetFileAsync( upload.BucketName, upload.KeyName, consumeContext.CancellationToken); - if (file.IsNotFound()) + 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 = file.Value; + using var fileStream = fileResult.Value; - var pictureResult = await pictureHandler.ResizePictureAsync( - fileStream, - consumeContext.Message.Width, - consumeContext.Message.Height, - consumeContext.CancellationToken); - - if (!pictureResult.IsSuccess) + foreach (var resize in consumeContext.Message.ImageResizes) { - string errors = string.Join("; ", pictureResult.Errors); - logger.LogError("Failed to resize picture: {ErrorMessage}", errors); - - await UpdateState(database, upload, UploadState.Failed, consumeContext.CancellationToken); + var newUpload = new Upload + { + BucketName = upload.BucketName, + KeyName = $"{resize.SaveOnRoute}/{upload.Id}.webp", + ContentType = "image/webp", + State = UploadState.Processing, + EntityId = upload.EntityId, + EntityType = upload.EntityType + }; - return; - } + await database.Uploads.AddAsync(newUpload, consumeContext.CancellationToken); - var saveResult = await storageHandler.UploadFileAsync( - pictureResult.Value, - "image/webp", - upload.BucketName, - $"{consumeContext.Message.SaveRoute}/{upload.Id}.webp", - consumeContext.CancellationToken); + await database.SaveChangesAsync(consumeContext.CancellationToken); - if (!saveResult.IsSuccess) - { - string errors = string.Join("; ", saveResult.Errors); - logger.LogError("Failed to upload resized picture: {ErrorMessage}", errors); + var pictureResult = await pictureHandler.ResizePictureAsync( + fileStream, + resize.Width, + resize.Height, + consumeContext.CancellationToken); - await UpdateState(database, upload, UploadState.Failed, consumeContext.CancellationToken); + if (!pictureResult.IsSuccess) + { + logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); + await UpdateState(database, newUpload, UploadState.Failed, consumeContext.CancellationToken); + continue; + } - return; + var saveResult = await storageHandler.UploadFileAsync( + pictureResult.Value, + "image/webp", + upload.BucketName, + $"{resize.SaveOnRoute}/{upload.Id}.webp", + consumeContext.CancellationToken); + + if (!saveResult.IsSuccess) + { + logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); + await UpdateState(database, newUpload, UploadState.Failed, consumeContext.CancellationToken); + continue; + } + + await UpdateState(database, newUpload, UploadState.Completed, consumeContext.CancellationToken); } await UpdateState(database, upload, UploadState.Completed, consumeContext.CancellationToken); @@ -88,7 +98,7 @@ namespace Musify.Infrastructure.Messaging.Consumers throw; } } - + async Task UpdateState(IDatabase database, Upload upload, UploadState uploadState, CancellationToken cancellationToken) { upload.State = uploadState; diff --git a/Musify.Backend/Entities/EntityType.cs b/Musify.Backend/Entities/EntityType.cs new file mode 100644 index 0000000..24c2cd9 --- /dev/null +++ b/Musify.Backend/Entities/EntityType.cs @@ -0,0 +1,9 @@ +namespace Musify.Domain.Entities +{ + public enum EntityType + { + User, + Track, + PlayList + } +} \ No newline at end of file diff --git a/Musify.Backend/Entities/Upload.cs b/Musify.Backend/Entities/Upload.cs index 0bb727c..0bb9ca4 100644 --- a/Musify.Backend/Entities/Upload.cs +++ b/Musify.Backend/Entities/Upload.cs @@ -18,6 +18,12 @@ namespace Musify.Domain.Entities [Required] public required string ContentType { get; set; } + [Required] + public required EntityType EntityType { get; set; } + + [Required] + public required Guid EntityId { get; set; } + [Required] public UploadState State { get; set; } = UploadState.Pending; From e95574f8019eac8843a3d3d25418917cf86de68a Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 15:50:18 +0100 Subject: [PATCH 007/172] Config --- Application/Application.csproj | 2 + .../Configuration/PlayListConfiguration.cs | 43 ++++++++++++++++++- .../Configuration/StorageConfiguration.cs | 28 +++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index d1a1586..28753c1 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -12,6 +12,8 @@ + + diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index fea1cf7..e0360c0 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -1,7 +1,11 @@ -namespace Musify.Application.Configuration +using Microsoft.Extensions.Configuration; + +namespace Musify.Application.Configuration { public class PlayListConfiguration { + public const string SectionName = "PlayListConfiguration"; + public PlayListRoutes Routes { get; set; } = new PlayListRoutes(); public int SmallPictureWidth { get; set; } = 128; @@ -15,6 +19,43 @@ public int LargePictureWidth { get; private set; } = 512; public int LargePictureHeight { get; private set; } = 512; + + public PlayListConfiguration Load(IConfiguration configuration) + { + var section = configuration.GetSection(SectionName).Get() + ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); + + Validate(); + + return section; + } + + void Validate() + { + EnsureValue(Routes.ParentFolder, $"{SectionName}:BucketName"); + EnsureValue(Routes.SmallPictures, $"{SectionName}:Routes:SmallPictures"); + EnsureValue(Routes.MediumPictures, $"{SectionName}:Routes:MediumPictures"); + EnsureValue(Routes.LargePictures, $"{SectionName}:Routes:LargePictures"); + + EnsurePositiveValue(SmallPictureWidth, $"{SectionName}:SmallPictureWidth"); + EnsurePositiveValue(SmallPictureHeight, $"{SectionName}:SmallPictureHeight"); + EnsurePositiveValue(MediumPictureWidth, $"{SectionName}:MediumPictureWidth"); + EnsurePositiveValue(MediumPictureHeight, $"{SectionName}:MediumPictureHeight"); + EnsurePositiveValue(LargePictureWidth, $"{SectionName}:LargePictureWidth"); + EnsurePositiveValue(LargePictureHeight, $"{SectionName}:LargePictureHeight"); + } + + void EnsureValue(string value, string name) + { + if (string.IsNullOrEmpty(value)) + throw new InvalidOperationException($"{name} configuration value not found."); + } + + void EnsurePositiveValue(int value, string name) + { + if (value < 1) + throw new InvalidOperationException($"{name} configuration value must be a positive integer."); + } } public class PlayListRoutes diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs index 8cf6c49..e18b5de 100644 --- a/Application/Configuration/StorageConfiguration.cs +++ b/Application/Configuration/StorageConfiguration.cs @@ -1,10 +1,36 @@ -namespace Musify.Application.Configuration +using Microsoft.Extensions.Configuration; + +namespace Musify.Application.Configuration { public class StorageConfiguration { + public const string SectionName = "Storage"; + public string BucketName { get; set; } = "musify-S3"; public StorageRoutes Routes { get; set; } = new StorageRoutes(); + + public StorageConfiguration Load(IConfiguration configuration) + { + var section = configuration.GetSection(SectionName).Get() + ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); + + Validate(); + + return section; + } + + void Validate() + { + EnsureValue(BucketName, nameof(BucketName)); + EnsureValue(Routes.Uploads, $"{SectionName}:Route:Upload"); + } + + void EnsureValue(string value, string name) + { + if (string.IsNullOrEmpty(value)) + throw new InvalidOperationException($"{name} configuration value not found."); + } } public class StorageRoutes From 80c957dec1996b8e8f69774cffbcc93a401682ed Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 16:06:57 +0100 Subject: [PATCH 008/172] Changes --- .../Consumers/ImageResizedConsumer.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs index 88280bb..cb9c24a 100644 --- a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs @@ -44,12 +44,20 @@ namespace Musify.Infrastructure.Messaging.Consumers using var fileStream = fileResult.Value; + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, consumeContext.CancellationToken); + foreach (var resize in consumeContext.Message.ImageResizes) { + memoryStream.Position = 0; + + var newUploadId = Guid.NewGuid(); + var newUpload = new Upload { + Id = newUploadId, BucketName = upload.BucketName, - KeyName = $"{resize.SaveOnRoute}/{upload.Id}.webp", + KeyName = $"{resize.SaveOnRoute}/{newUploadId}.webp", ContentType = "image/webp", State = UploadState.Processing, EntityId = upload.EntityId, @@ -61,7 +69,7 @@ namespace Musify.Infrastructure.Messaging.Consumers await database.SaveChangesAsync(consumeContext.CancellationToken); var pictureResult = await pictureHandler.ResizePictureAsync( - fileStream, + memoryStream, resize.Width, resize.Height, consumeContext.CancellationToken); @@ -77,7 +85,7 @@ namespace Musify.Infrastructure.Messaging.Consumers pictureResult.Value, "image/webp", upload.BucketName, - $"{resize.SaveOnRoute}/{upload.Id}.webp", + $"{resize.SaveOnRoute}/{newUploadId}.webp", consumeContext.CancellationToken); if (!saveResult.IsSuccess) @@ -97,15 +105,17 @@ namespace Musify.Infrastructure.Messaging.Consumers 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); - - await database.SaveChangesAsync(cancellationToken); } } } \ No newline at end of file From b8598d37d72feaee1463c3017ed3ea8e4a9c8032 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 16:15:50 +0100 Subject: [PATCH 009/172] Rename --- .../{ImageResizedConsumer.cs => ImageResizerConsumer.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Infrastructure/Messaging/Consumers/{ImageResizedConsumer.cs => ImageResizerConsumer.cs} (98%) diff --git a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs b/Infrastructure/Messaging/Consumers/ImageResizerConsumer.cs similarity index 98% rename from Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs rename to Infrastructure/Messaging/Consumers/ImageResizerConsumer.cs index cb9c24a..a875a50 100644 --- a/Infrastructure/Messaging/Consumers/ImageResizedConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ImageResizerConsumer.cs @@ -7,7 +7,7 @@ using Musify.Domain.Entities; namespace Musify.Infrastructure.Messaging.Consumers { - public class ImageResizedConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) + public class ImageResizerConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) : IConsumer { public async Task Consume(ConsumeContext consumeContext) From 782fee4ffd92194d935e602ee464169e898929a2 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 16:29:19 +0100 Subject: [PATCH 010/172] Rename to picture --- .../{ImageResizerConsumer.cs => PictureResizerConsumer.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Infrastructure/Messaging/Consumers/{ImageResizerConsumer.cs => PictureResizerConsumer.cs} (96%) diff --git a/Infrastructure/Messaging/Consumers/ImageResizerConsumer.cs b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs similarity index 96% rename from Infrastructure/Messaging/Consumers/ImageResizerConsumer.cs rename to Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs index a875a50..86c5e16 100644 --- a/Infrastructure/Messaging/Consumers/ImageResizerConsumer.cs +++ b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs @@ -7,7 +7,7 @@ using Musify.Domain.Entities; namespace Musify.Infrastructure.Messaging.Consumers { - public class ImageResizerConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) + public class PictureResizerConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) : IConsumer { public async Task Consume(ConsumeContext consumeContext) From 94de61c187e4c4f7eb71c9267fa48c31f84f98aa Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 16:43:34 +0100 Subject: [PATCH 011/172] Rename --- Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs index 86c5e16..04b1dd5 100644 --- a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs +++ b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs @@ -10,6 +10,8 @@ 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 From 582bd9c005ac08e8482b8bd2f5827adc6e81662e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 17:17:37 +0100 Subject: [PATCH 012/172] Dependency injection --- Application/ApplicationDependencyInjection.cs | 6 ---- .../ConfigurationDependencyInjection.cs | 22 +++++++++++++ .../Configuration/PlayListConfiguration.cs | 30 ++++++++--------- .../Configuration/StorageConfiguration.cs | 12 +++---- .../DispatchR/DispatchRDependencyInjection.cs | 13 ++++++++ .../Pictures/PictureDependencyInjection.cs | 13 ++++++++ Infrastructure/Infrastructure.csproj | 2 ++ .../MassTransitDependencyInjection.cs | 33 +++++++++++++++++++ .../DatabaseDependencyInjection.cs | 23 +++++++++++++ .../Pictures/PictureDependencyInjection.cs | 13 ++++++++ .../Storage/StorageDependencyInjection.cs | 13 ++++++++ 11 files changed, 153 insertions(+), 27 deletions(-) delete mode 100644 Application/ApplicationDependencyInjection.cs create mode 100644 Application/Configuration/ConfigurationDependencyInjection.cs create mode 100644 Application/DispatchR/DispatchRDependencyInjection.cs create mode 100644 Application/Pictures/PictureDependencyInjection.cs create mode 100644 Infrastructure/Messaging/MassTransitDependencyInjection.cs create mode 100644 Infrastructure/Persistence/DatabaseDependencyInjection.cs create mode 100644 Infrastructure/Pictures/PictureDependencyInjection.cs create mode 100644 Infrastructure/Storage/StorageDependencyInjection.cs diff --git a/Application/ApplicationDependencyInjection.cs b/Application/ApplicationDependencyInjection.cs deleted file mode 100644 index 620f081..0000000 --- a/Application/ApplicationDependencyInjection.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Musify.Application -{ - public static class ApplicationDependencyInjection - { - } -} \ No newline at end of file diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs new file mode 100644 index 0000000..46d3702 --- /dev/null +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Musify.Application.Configuration +{ + public static class ConfigurationDependencyInjection + { + public static void AddPlayListConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + var playListConfiguration = PlayListConfiguration.Load(configuration); + + serviceDescriptors.AddSingleton(playListConfiguration); + } + + public static void AddStorageConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + var storageConfiguration = StorageConfiguration.Load(configuration); + + serviceDescriptors.AddSingleton(storageConfiguration); + } + } +} \ No newline at end of file diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index e0360c0..fd6260b 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -20,38 +20,38 @@ namespace Musify.Application.Configuration public int LargePictureHeight { get; private set; } = 512; - public PlayListConfiguration Load(IConfiguration configuration) + public static PlayListConfiguration Load(IConfiguration configuration) { var section = configuration.GetSection(SectionName).Get() ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); - Validate(); + Validate(section); return section; } - void Validate() + static void Validate(PlayListConfiguration configuration) { - EnsureValue(Routes.ParentFolder, $"{SectionName}:BucketName"); - EnsureValue(Routes.SmallPictures, $"{SectionName}:Routes:SmallPictures"); - EnsureValue(Routes.MediumPictures, $"{SectionName}:Routes:MediumPictures"); - EnsureValue(Routes.LargePictures, $"{SectionName}:Routes:LargePictures"); + EnsureValue(configuration.Routes.ParentFolder, $"{SectionName}:BucketName"); + EnsureValue(configuration.Routes.SmallPictures, $"{SectionName}:Routes:SmallPictures"); + EnsureValue(configuration.Routes.MediumPictures, $"{SectionName}:Routes:MediumPictures"); + EnsureValue(configuration.Routes.LargePictures, $"{SectionName}:Routes:LargePictures"); - EnsurePositiveValue(SmallPictureWidth, $"{SectionName}:SmallPictureWidth"); - EnsurePositiveValue(SmallPictureHeight, $"{SectionName}:SmallPictureHeight"); - EnsurePositiveValue(MediumPictureWidth, $"{SectionName}:MediumPictureWidth"); - EnsurePositiveValue(MediumPictureHeight, $"{SectionName}:MediumPictureHeight"); - EnsurePositiveValue(LargePictureWidth, $"{SectionName}:LargePictureWidth"); - EnsurePositiveValue(LargePictureHeight, $"{SectionName}:LargePictureHeight"); + EnsurePositiveValue(configuration.SmallPictureWidth, $"{SectionName}:SmallPictureWidth"); + EnsurePositiveValue(configuration.SmallPictureHeight, $"{SectionName}:SmallPictureHeight"); + EnsurePositiveValue(configuration.MediumPictureWidth, $"{SectionName}:MediumPictureWidth"); + EnsurePositiveValue(configuration.MediumPictureHeight, $"{SectionName}:MediumPictureHeight"); + EnsurePositiveValue(configuration.LargePictureWidth, $"{SectionName}:LargePictureWidth"); + EnsurePositiveValue(configuration.LargePictureHeight, $"{SectionName}:LargePictureHeight"); } - void EnsureValue(string value, string name) + static void EnsureValue(string value, string name) { if (string.IsNullOrEmpty(value)) throw new InvalidOperationException($"{name} configuration value not found."); } - void EnsurePositiveValue(int value, string name) + static void EnsurePositiveValue(int value, string name) { if (value < 1) throw new InvalidOperationException($"{name} configuration value must be a positive integer."); diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs index e18b5de..2604ee6 100644 --- a/Application/Configuration/StorageConfiguration.cs +++ b/Application/Configuration/StorageConfiguration.cs @@ -10,23 +10,23 @@ namespace Musify.Application.Configuration public StorageRoutes Routes { get; set; } = new StorageRoutes(); - public StorageConfiguration Load(IConfiguration configuration) + public static StorageConfiguration Load(IConfiguration configuration) { var section = configuration.GetSection(SectionName).Get() ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); - Validate(); + Validate(section); return section; } - void Validate() + static void Validate(StorageConfiguration configuration) { - EnsureValue(BucketName, nameof(BucketName)); - EnsureValue(Routes.Uploads, $"{SectionName}:Route:Upload"); + EnsureValue(configuration.BucketName, nameof(BucketName)); + EnsureValue(configuration.Routes.Uploads, $"{SectionName}:Route:Upload"); } - void EnsureValue(string value, string name) + static void EnsureValue(string value, string name) { if (string.IsNullOrEmpty(value)) throw new InvalidOperationException($"{name} configuration value not found."); diff --git a/Application/DispatchR/DispatchRDependencyInjection.cs b/Application/DispatchR/DispatchRDependencyInjection.cs new file mode 100644 index 0000000..2b058d8 --- /dev/null +++ b/Application/DispatchR/DispatchRDependencyInjection.cs @@ -0,0 +1,13 @@ +using DispatchR.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Musify.Application.DispatchR +{ + public static class DispatchRDependencyInjection + { + public static void AddDispatchR(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddDispatchR(typeof(DispatchRDependencyInjection).Assembly); + } + } +} \ No newline at end of file diff --git a/Application/Pictures/PictureDependencyInjection.cs b/Application/Pictures/PictureDependencyInjection.cs new file mode 100644 index 0000000..914897f --- /dev/null +++ b/Application/Pictures/PictureDependencyInjection.cs @@ -0,0 +1,13 @@ +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/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 4a82fe5..4b67f77 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -11,7 +11,9 @@ + + diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs new file mode 100644 index 0000000..30c31af --- /dev/null +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -0,0 +1,33 @@ +using MassTransit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Musify.Infrastructure.Messaging.Consumers; + +namespace Musify.Infrastructure.Messaging +{ + public static class MassTransitDependencyInjection + { + public static void AddMassTransitHandler(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors.AddMassTransit(options => + { + options.AddConsumer(); + + options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => + { + var section = configuration.GetSection("MassTransit"); + + var host = section["Host"] + ?? throw new InvalidOperationException("MassTransit:Host configuration is missing."); + + busFactoryConfigurator.Host(host); + + busFactoryConfigurator.ReceiveEndpoint(PictureResizerConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + } ); + } ); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs new file mode 100644 index 0000000..f924e98 --- /dev/null +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Persistence +{ + public static class DatabaseDependencyInjection + { + public static void AddDatabase(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("Default") + ?? throw new InvalidOperationException("Default Connection connection string not found."); + + serviceDescriptors.AddDbContext(options => + { + options.UseNpgsql(connectionString); + } ); + + serviceDescriptors.AddScoped(serviceProvider => serviceProvider.GetRequiredService()); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Pictures/PictureDependencyInjection.cs b/Infrastructure/Pictures/PictureDependencyInjection.cs new file mode 100644 index 0000000..2abd860 --- /dev/null +++ b/Infrastructure/Pictures/PictureDependencyInjection.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Pictures +{ + public static class PictureDependencyInjection + { + public static void AddPictureHandler(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddScoped(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs new file mode 100644 index 0000000..54c58a2 --- /dev/null +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Storage +{ + public static class StorageDependencyInjection + { + public static void AddStorageHandler(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddScoped(); + } + } +} \ No newline at end of file From b7299322c0b6fe18d67be1fdf9ec6eb060ed9f82 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 17:42:16 +0100 Subject: [PATCH 013/172] username and password --- .../Messaging/MassTransitDependencyInjection.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index 30c31af..9e5f183 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -18,9 +18,19 @@ namespace Musify.Infrastructure.Messaging var section = configuration.GetSection("MassTransit"); var host = section["Host"] - ?? throw new InvalidOperationException("MassTransit:Host configuration is missing."); + ?? throw new InvalidOperationException("MassTransit:Host configuration value not found."); - busFactoryConfigurator.Host(host); + var username = section["Username"] + ?? throw new InvalidOperationException("MassTransit:Username configuration value not found."); + + var password = section["Password"] + ?? throw new InvalidOperationException("MassTransit:Password configuration value not found."); + + busFactoryConfigurator.Host(host, options => + { + options.Username(username); + options.Password(password); + } ); busFactoryConfigurator.ReceiveEndpoint(PictureResizerConsumer.QueueName, endpointConfigurator => { From fcc1f563e59d0803a4c477899f1d46ea4797afe9 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 18:08:53 +0100 Subject: [PATCH 014/172] Changes --- .../Configuration/StorageConfiguration.cs | 16 ++++++++++- .../Identity/KeycloakConfiguration.cs | 28 ++++++++++++++++++- .../Identity/KeycloakDependencyInjection.cs | 18 ++++++++++++ .../Storage/StorageDependencyInjection.cs | 18 +++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 Infrastructure/Identity/KeycloakDependencyInjection.cs diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs index 2604ee6..0940fc7 100644 --- a/Application/Configuration/StorageConfiguration.cs +++ b/Application/Configuration/StorageConfiguration.cs @@ -6,7 +6,17 @@ namespace Musify.Application.Configuration { public const string SectionName = "Storage"; - public string BucketName { get; set; } = "musify-S3"; + public string ServiceUrl { get; set; } = string.Empty; + + public string AccessKey { get; set; } = string.Empty; + + public string SecretAccessKey { get; set; } = string.Empty; + + public string BucketName { get; set; } = string.Empty; + + public bool ForcePathStyle { get; set; } + + public bool UseHttp { get; set; } public StorageRoutes Routes { get; set; } = new StorageRoutes(); @@ -24,6 +34,10 @@ namespace Musify.Application.Configuration { EnsureValue(configuration.BucketName, nameof(BucketName)); EnsureValue(configuration.Routes.Uploads, $"{SectionName}:Route:Upload"); + + EnsureValue(configuration.ServiceUrl, nameof(ServiceUrl)); + EnsureValue(configuration.AccessKey, nameof(AccessKey)); + EnsureValue(configuration.SecretAccessKey, nameof(SecretAccessKey)); } static void EnsureValue(string value, string name) diff --git a/Infrastructure/Identity/KeycloakConfiguration.cs b/Infrastructure/Identity/KeycloakConfiguration.cs index 385fe20..9b1f146 100644 --- a/Infrastructure/Identity/KeycloakConfiguration.cs +++ b/Infrastructure/Identity/KeycloakConfiguration.cs @@ -1,4 +1,6 @@ -namespace Musify.Infrastructure.Identity +using Microsoft.Extensions.Configuration; + +namespace Musify.Infrastructure.Identity { public class KeycloakConfiguration { @@ -11,5 +13,29 @@ public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; + + public static KeycloakConfiguration Load(IConfiguration configuration) + { + var keycloakConfiguration = configuration.GetSection(SectionName).Get() + ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); + + Validate(keycloakConfiguration); + + return keycloakConfiguration; + } + + static void Validate(KeycloakConfiguration configuration) + { + EnsureValue(configuration.BaseUrl, nameof(configuration.BaseUrl)); + EnsureValue(configuration.Realm, nameof(configuration.Realm)); + EnsureValue(configuration.Username, nameof(configuration.Username)); + EnsureValue(configuration.Password, nameof(configuration.Password)); + } + + static void EnsureValue(string value, string name) + { + if (string.IsNullOrEmpty(value)) + throw new InvalidOperationException($"{name} configuration value not found."); + } } } \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs new file mode 100644 index 0000000..ab2bacf --- /dev/null +++ b/Infrastructure/Identity/KeycloakDependencyInjection.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Infrastructure.Identity +{ + public static class KeycloakDependencyInjection + { + public static void AddKeycloakAuthentication(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + var keycloakConfiguration = KeycloakConfiguration.Load(configuration); + + serviceDescriptors.AddSingleton(keycloakConfiguration); + + serviceDescriptors.AddScoped(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs index 54c58a2..0c13dd2 100644 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +using Amazon.S3; +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; namespace Musify.Infrastructure.Storage @@ -7,6 +9,20 @@ namespace Musify.Infrastructure.Storage { public static void AddStorageHandler(this IServiceCollection serviceDescriptors) { + serviceDescriptors.AddScoped(serviceProvider => + { + var configuration = serviceProvider.GetRequiredService(); + + var s3Configuration = new AmazonS3Config + { + ServiceURL = configuration.ServiceUrl, + ForcePathStyle = configuration.ForcePathStyle, + UseHttp = configuration.UseHttp, + }; + + return new AmazonS3Client(configuration.AccessKey, configuration.SecretAccessKey, s3Configuration); + } ); + serviceDescriptors.AddScoped(); } } From e28532a1d9034537aa746a40d26415083809d353 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 18:48:01 +0100 Subject: [PATCH 015/172] Changes --- Infrastructure/Infrastructure.csproj | 6 ++++ .../MassTransitDependencyInjection.cs | 28 ++++++++++++++++++- .../Persistence/DatabaseDesignTimeFactory.cs | 23 +++++++++++++++ Infrastructure/Storage/StorageHandler.cs | 2 ++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 Infrastructure/Persistence/DatabaseDesignTimeFactory.cs diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 4b67f77..3808e4b 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -13,6 +13,12 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index 9e5f183..c7a37f0 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -7,7 +7,33 @@ namespace Musify.Infrastructure.Messaging { public static class MassTransitDependencyInjection { - public static void AddMassTransitHandler(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static void AddMassTransitClient(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors.AddMassTransit(options => + { + options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => + { + var section = configuration.GetSection("MassTransit"); + + var host = section["Host"] + ?? throw new InvalidOperationException("MassTransit:Host configuration value not found."); + + var username = section["Username"] + ?? throw new InvalidOperationException("MassTransit:Username configuration value not found."); + + var password = section["Password"] + ?? throw new InvalidOperationException("MassTransit:Password configuration value not found."); + + busFactoryConfigurator.Host(host, options => + { + options.Username(username); + options.Password(password); + } ); + } ); + } ); + } + + public static void AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors.AddMassTransit(options => { diff --git a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs new file mode 100644 index 0000000..d776732 --- /dev/null +++ b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace Musify.Infrastructure.Persistence +{ + public class DatabaseDesignTimeFactory : IDesignTimeDbContextFactory + { + public Database CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("AppSettings.json") + .Build(); + var connectionString = configuration.GetConnectionString("Default"); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(connectionString); + + return new Database(optionsBuilder.Options); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index b95f616..ff6a7ac 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -78,6 +78,7 @@ namespace Musify.Infrastructure.Storage try { await trasnsferUtility.UploadAsync(filePath, bucketName, fileName, cancellationToken); + logger.LogInformation("Successfully transferred file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); } catch { @@ -102,6 +103,7 @@ namespace Musify.Infrastructure.Storage try { var response = await amazonS3.PutObjectAsync(request, cancellationToken); + logger.LogInformation("Successfully uploaded file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); } catch { From b77b56859ee54c4ba8b641e20acb4a4c9b815bb0 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 18:59:41 +0100 Subject: [PATCH 016/172] Changes --- .../ConfigurationDependencyInjection.cs | 2 -- .../Configuration/PlayListConfiguration.cs | 1 - .../Configuration/StorageConfiguration.cs | 1 - .../ConfigurationDependencyInjection.cs | 20 ++++++++++++ .../Configuration/DatabaseConfiguration.cs | 31 +++++++++++++++++++ .../KeycloakConfiguration.cs | 3 +- .../Identity/KeycloakDependencyInjection.cs | 3 +- .../Identity/KeycloakIdentityService.cs | 1 + Infrastructure/Persistence/Database.cs | 10 ++++-- .../Persistence/DatabaseDesignTimeFactory.cs | 10 ++++-- .../Storage/StorageDependencyInjection.cs | 2 -- 11 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 Infrastructure/Configuration/ConfigurationDependencyInjection.cs create mode 100644 Infrastructure/Configuration/DatabaseConfiguration.cs rename Infrastructure/{Identity => Configuration}/KeycloakConfiguration.cs (96%) diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs index 46d3702..d1e0fab 100644 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -8,14 +8,12 @@ namespace Musify.Application.Configuration public static void AddPlayListConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) { var playListConfiguration = PlayListConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(playListConfiguration); } public static void AddStorageConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) { var storageConfiguration = StorageConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(storageConfiguration); } } diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index fd6260b..4567cc7 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -26,7 +26,6 @@ namespace Musify.Application.Configuration ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); Validate(section); - return section; } diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs index 0940fc7..b8932aa 100644 --- a/Application/Configuration/StorageConfiguration.cs +++ b/Application/Configuration/StorageConfiguration.cs @@ -26,7 +26,6 @@ namespace Musify.Application.Configuration ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); Validate(section); - return section; } diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs new file mode 100644 index 0000000..815fb0f --- /dev/null +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Musify.Infrastructure.Configuration +{ + public static class ConfigurationDependencyInjection + { + public static void AddKeycloakConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + var keycloakConfiguration = KeycloakConfiguration.Load(configuration); + serviceDescriptors.AddSingleton(keycloakConfiguration); + } + + public static void AddDatabaseConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + var databaseConfiguration = DatabaseConfiguration.Load(configuration); + serviceDescriptors.AddSingleton(databaseConfiguration); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Configuration/DatabaseConfiguration.cs b/Infrastructure/Configuration/DatabaseConfiguration.cs new file mode 100644 index 0000000..aeef73d --- /dev/null +++ b/Infrastructure/Configuration/DatabaseConfiguration.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Configuration; + +namespace Musify.Infrastructure.Configuration +{ + public class DatabaseConfiguration + { + public const string SectionName = "Database"; + + public string ConnectionString { get; set; } = string.Empty; + + public static DatabaseConfiguration Load(IConfiguration configuration) + { + var databaseConfiguration = configuration.GetSection(SectionName).Get() + ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); + + Validate(databaseConfiguration); + return databaseConfiguration; + } + + static void Validate(DatabaseConfiguration configuration) + { + EnsureValue(configuration.ConnectionString, nameof(configuration.ConnectionString)); + } + + static void EnsureValue(string value, string name) + { + if (string.IsNullOrEmpty(value)) + throw new InvalidOperationException($"{name} configuration value not found."); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakConfiguration.cs b/Infrastructure/Configuration/KeycloakConfiguration.cs similarity index 96% rename from Infrastructure/Identity/KeycloakConfiguration.cs rename to Infrastructure/Configuration/KeycloakConfiguration.cs index 9b1f146..23d0198 100644 --- a/Infrastructure/Identity/KeycloakConfiguration.cs +++ b/Infrastructure/Configuration/KeycloakConfiguration.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; -namespace Musify.Infrastructure.Identity +namespace Musify.Infrastructure.Configuration { public class KeycloakConfiguration { @@ -20,7 +20,6 @@ namespace Musify.Infrastructure.Identity ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); Validate(keycloakConfiguration); - return keycloakConfiguration; } diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs index ab2bacf..f23619f 100644 --- a/Infrastructure/Identity/KeycloakDependencyInjection.cs +++ b/Infrastructure/Identity/KeycloakDependencyInjection.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Identity { @@ -9,9 +10,7 @@ namespace Musify.Infrastructure.Identity public static void AddKeycloakAuthentication(this IServiceCollection serviceDescriptors, IConfiguration configuration) { var keycloakConfiguration = KeycloakConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(keycloakConfiguration); - serviceDescriptors.AddScoped(); } } diff --git a/Infrastructure/Identity/KeycloakIdentityService.cs b/Infrastructure/Identity/KeycloakIdentityService.cs index ea4533d..ecb3f13 100644 --- a/Infrastructure/Identity/KeycloakIdentityService.cs +++ b/Infrastructure/Identity/KeycloakIdentityService.cs @@ -4,6 +4,7 @@ using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Contracts; +using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Identity { diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index cf4b52d..5a1e8da 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -1,12 +1,18 @@ using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; +using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Persistence { - public class Database(DbContextOptions dbContextOptions) - : DbContext(dbContextOptions), IDatabase + public class Database(DatabaseConfiguration configuration) + : DbContext, IDatabase { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(configuration.ConnectionString); + } + public DbSet Users => Set(); public DbSet Tracks => Set(); diff --git a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs index d776732..dd6aa29 100644 --- a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs +++ b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; +using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Persistence { @@ -12,12 +13,15 @@ namespace Musify.Infrastructure.Persistence .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("AppSettings.json") .Build(); - var connectionString = configuration.GetConnectionString("Default"); + + var databaseConfiguration = configuration.GetSection(DatabaseConfiguration.SectionName) + .Get() + ?? throw new InvalidOperationException($"Failed to load {DatabaseConfiguration.SectionName} configuration."); var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(connectionString); + optionsBuilder.UseNpgsql(databaseConfiguration.ConnectionString); - return new Database(optionsBuilder.Options); + return new Database(databaseConfiguration); } } } \ No newline at end of file diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs index 0c13dd2..6c54325 100644 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -12,14 +12,12 @@ namespace Musify.Infrastructure.Storage serviceDescriptors.AddScoped(serviceProvider => { var configuration = serviceProvider.GetRequiredService(); - var s3Configuration = new AmazonS3Config { ServiceURL = configuration.ServiceUrl, ForcePathStyle = configuration.ForcePathStyle, UseHttp = configuration.UseHttp, }; - return new AmazonS3Client(configuration.AccessKey, configuration.SecretAccessKey, s3Configuration); } ); From 5f1bcf93c65a0a511cea7270a018dbbfbe7e76be Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 19:07:01 +0100 Subject: [PATCH 017/172] Small changes --- Application/Pictures/PictureService.cs | 6 ++ .../Consumers/PictureResizerConsumer.cs | 71 +++++++++++-------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/Application/Pictures/PictureService.cs b/Application/Pictures/PictureService.cs index c061995..7ee8348 100644 --- a/Application/Pictures/PictureService.cs +++ b/Application/Pictures/PictureService.cs @@ -28,6 +28,7 @@ namespace Musify.Application.Pictures BucketName = storageConfiguration.BucketName, EntityType = entityType, EntityId = EntityId, + State = UploadState.Completed }; await database.Uploads.AddAsync(upload, cancellationToken); @@ -44,6 +45,11 @@ namespace Musify.Application.Pictures 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."); } diff --git a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs index 04b1dd5..28b6bdb 100644 --- a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs +++ b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs @@ -2,6 +2,7 @@ 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; @@ -24,9 +25,9 @@ namespace Musify.Infrastructure.Messaging.Consumers return; } - if (upload.State != UploadState.Pending) + if (upload.State != UploadState.Completed) { - logger.LogInformation("Upload is not in a pending state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State); + logger.LogInformation("Upload is not in a Completed state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State); return; } @@ -51,8 +52,6 @@ namespace Musify.Infrastructure.Messaging.Consumers foreach (var resize in consumeContext.Message.ImageResizes) { - memoryStream.Position = 0; - var newUploadId = Guid.NewGuid(); var newUpload = new Upload @@ -70,34 +69,16 @@ namespace Musify.Infrastructure.Messaging.Consumers await database.SaveChangesAsync(consumeContext.CancellationToken); - var pictureResult = await pictureHandler.ResizePictureAsync( - memoryStream, - resize.Width, - resize.Height, - consumeContext.CancellationToken); - - if (!pictureResult.IsSuccess) + try { - logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); + 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; } - - var saveResult = await storageHandler.UploadFileAsync( - pictureResult.Value, - "image/webp", - upload.BucketName, - $"{resize.SaveOnRoute}/{newUploadId}.webp", - consumeContext.CancellationToken); - - if (!saveResult.IsSuccess) - { - logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); - await UpdateState(database, newUpload, UploadState.Failed, consumeContext.CancellationToken); - continue; - } - - await UpdateState(database, newUpload, UploadState.Completed, consumeContext.CancellationToken); } await UpdateState(database, upload, UploadState.Completed, consumeContext.CancellationToken); @@ -119,5 +100,39 @@ namespace Musify.Infrastructure.Messaging.Consumers 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 From 771bfbb270a4d04ec547c95120327d7c525bdae2 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 20:52:11 +0100 Subject: [PATCH 018/172] Change name --- Application/Application.csproj | 2 +- {Musify.Backend => Domain}/Domain.csproj | 0 {Musify.Backend => Domain}/Entities/EntityType.cs | 0 {Musify.Backend => Domain}/Entities/PlayList.cs | 0 {Musify.Backend => Domain}/Entities/PlayListHasTrack.cs | 0 {Musify.Backend => Domain}/Entities/Track.cs | 0 {Musify.Backend => Domain}/Entities/Upload.cs | 0 {Musify.Backend => Domain}/Entities/UploadState.cs | 0 {Musify.Backend => Domain}/Entities/User.cs | 0 Infrastructure/Infrastructure.csproj | 2 +- MusifyBackend.slnx | 2 +- 11 files changed, 3 insertions(+), 3 deletions(-) rename {Musify.Backend => Domain}/Domain.csproj (100%) rename {Musify.Backend => Domain}/Entities/EntityType.cs (100%) rename {Musify.Backend => Domain}/Entities/PlayList.cs (100%) rename {Musify.Backend => Domain}/Entities/PlayListHasTrack.cs (100%) rename {Musify.Backend => Domain}/Entities/Track.cs (100%) rename {Musify.Backend => Domain}/Entities/Upload.cs (100%) rename {Musify.Backend => Domain}/Entities/UploadState.cs (100%) rename {Musify.Backend => Domain}/Entities/User.cs (100%) diff --git a/Application/Application.csproj b/Application/Application.csproj index 28753c1..abf9700 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -19,7 +19,7 @@ - + diff --git a/Musify.Backend/Domain.csproj b/Domain/Domain.csproj similarity index 100% rename from Musify.Backend/Domain.csproj rename to Domain/Domain.csproj diff --git a/Musify.Backend/Entities/EntityType.cs b/Domain/Entities/EntityType.cs similarity index 100% rename from Musify.Backend/Entities/EntityType.cs rename to Domain/Entities/EntityType.cs diff --git a/Musify.Backend/Entities/PlayList.cs b/Domain/Entities/PlayList.cs similarity index 100% rename from Musify.Backend/Entities/PlayList.cs rename to Domain/Entities/PlayList.cs diff --git a/Musify.Backend/Entities/PlayListHasTrack.cs b/Domain/Entities/PlayListHasTrack.cs similarity index 100% rename from Musify.Backend/Entities/PlayListHasTrack.cs rename to Domain/Entities/PlayListHasTrack.cs diff --git a/Musify.Backend/Entities/Track.cs b/Domain/Entities/Track.cs similarity index 100% rename from Musify.Backend/Entities/Track.cs rename to Domain/Entities/Track.cs diff --git a/Musify.Backend/Entities/Upload.cs b/Domain/Entities/Upload.cs similarity index 100% rename from Musify.Backend/Entities/Upload.cs rename to Domain/Entities/Upload.cs diff --git a/Musify.Backend/Entities/UploadState.cs b/Domain/Entities/UploadState.cs similarity index 100% rename from Musify.Backend/Entities/UploadState.cs rename to Domain/Entities/UploadState.cs diff --git a/Musify.Backend/Entities/User.cs b/Domain/Entities/User.cs similarity index 100% rename from Musify.Backend/Entities/User.cs rename to Domain/Entities/User.cs diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 3808e4b..75091d6 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -25,7 +25,7 @@ - + diff --git a/MusifyBackend.slnx b/MusifyBackend.slnx index 7912f3e..c92b66b 100644 --- a/MusifyBackend.slnx +++ b/MusifyBackend.slnx @@ -1,5 +1,5 @@ - + From 574b0a05e21748c22f420ee768279947a89aea8c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 18 Mar 2026 21:05:06 +0100 Subject: [PATCH 019/172] Some changes --- .../Configuration/PlayListConfiguration.cs | 56 +++++++++++-------- .../Configuration/StorageConfiguration.cs | 13 +---- Application/Pictures/PictureService.cs | 4 +- .../CreatePlayListCommandHandler.cs | 18 ++++-- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 4567cc7..62d2a7f 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -6,19 +6,9 @@ namespace Musify.Application.Configuration { public const string SectionName = "PlayListConfiguration"; - public PlayListRoutes Routes { get; set; } = new PlayListRoutes(); + public Routes Routes { get; set; } = new Routes(); - public int SmallPictureWidth { get; set; } = 128; - - public int SmallPictureHeight { get; set; } = 128; - - public int MediumPictureWidth { get; private set; } = 256; - - public int MediumPictureHeight { get; private set; } = 256; - - public int LargePictureWidth { get; private set; } = 512; - - public int LargePictureHeight { get; private set; } = 512; + public PicturesSizes PicturesSizes { get; set; } = new PicturesSizes(); public static PlayListConfiguration Load(IConfiguration configuration) { @@ -31,17 +21,20 @@ namespace Musify.Application.Configuration static void Validate(PlayListConfiguration configuration) { - EnsureValue(configuration.Routes.ParentFolder, $"{SectionName}:BucketName"); + EnsureValue(configuration.Routes.OriginalPictures, $"{SectionName}:BucketName"); EnsureValue(configuration.Routes.SmallPictures, $"{SectionName}:Routes:SmallPictures"); EnsureValue(configuration.Routes.MediumPictures, $"{SectionName}:Routes:MediumPictures"); EnsureValue(configuration.Routes.LargePictures, $"{SectionName}:Routes:LargePictures"); + EnsureValue(configuration.Routes.PresetSmallPicture, $"{SectionName}:Routes:SmallPictures"); + EnsureValue(configuration.Routes.PresetMediumPicture, $"{SectionName}:Routes:MediumPictures"); + EnsureValue(configuration.Routes.PresetLargePicture, $"{SectionName}:Routes:LargePictures"); - EnsurePositiveValue(configuration.SmallPictureWidth, $"{SectionName}:SmallPictureWidth"); - EnsurePositiveValue(configuration.SmallPictureHeight, $"{SectionName}:SmallPictureHeight"); - EnsurePositiveValue(configuration.MediumPictureWidth, $"{SectionName}:MediumPictureWidth"); - EnsurePositiveValue(configuration.MediumPictureHeight, $"{SectionName}:MediumPictureHeight"); - EnsurePositiveValue(configuration.LargePictureWidth, $"{SectionName}:LargePictureWidth"); - EnsurePositiveValue(configuration.LargePictureHeight, $"{SectionName}:LargePictureHeight"); + EnsurePositiveValue(configuration.PicturesSizes.SmallPictureWidth, $"{SectionName}:SmallPictureWidth"); + EnsurePositiveValue(configuration.PicturesSizes.SmallPictureHeight, $"{SectionName}:SmallPictureHeight"); + EnsurePositiveValue(configuration.PicturesSizes.MediumPictureWidth, $"{SectionName}:MediumPictureWidth"); + EnsurePositiveValue(configuration.PicturesSizes.MediumPictureHeight, $"{SectionName}:MediumPictureHeight"); + EnsurePositiveValue(configuration.PicturesSizes.LargePictureWidth, $"{SectionName}:LargePictureWidth"); + EnsurePositiveValue(configuration.PicturesSizes.LargePictureHeight, $"{SectionName}:LargePictureHeight"); } static void EnsureValue(string value, string name) @@ -57,9 +50,9 @@ namespace Musify.Application.Configuration } } - public class PlayListRoutes + public class Routes { - public string ParentFolder { get; set; } = "PlayLists"; + public string OriginalPictures { get; set; } = "OriginalPictures"; public string SmallPictures { get; set; } = "SmallPictures"; @@ -67,10 +60,25 @@ namespace Musify.Application.Configuration public string LargePictures { get; set; } = "LargePictures"; - public string DefaultSmallPicture { get; set; } = "DefaultSmallPicture.webp"; + public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp"; - public string DefaultMediumPicture { get; set; } = "DefaultMediumPicture.webp"; + public string PresetMediumPicture { get; set; } = "PresettMediumPicture.webp"; - public string DefaultLargePicture { get; set; } = "DefaultLargePicture.webp"; + public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; + } + + public class PicturesSizes + { + public int SmallPictureWidth { get; set; } = 128; + + public int SmallPictureHeight { get; set; } = 128; + + public int MediumPictureWidth { get; private set; } = 256; + + public int MediumPictureHeight { get; private set; } = 256; + + public int LargePictureWidth { get; private set; } = 512; + + public int LargePictureHeight { get; private set; } = 512; } } \ No newline at end of file diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs index b8932aa..d02353f 100644 --- a/Application/Configuration/StorageConfiguration.cs +++ b/Application/Configuration/StorageConfiguration.cs @@ -6,20 +6,18 @@ namespace Musify.Application.Configuration { public const string SectionName = "Storage"; + public string BucketName { get; set; } = string.Empty; + public string ServiceUrl { get; set; } = string.Empty; public string AccessKey { get; set; } = string.Empty; public string SecretAccessKey { get; set; } = string.Empty; - public string BucketName { get; set; } = string.Empty; - public bool ForcePathStyle { get; set; } public bool UseHttp { get; set; } - public StorageRoutes Routes { get; set; } = new StorageRoutes(); - public static StorageConfiguration Load(IConfiguration configuration) { var section = configuration.GetSection(SectionName).Get() @@ -32,8 +30,6 @@ namespace Musify.Application.Configuration static void Validate(StorageConfiguration configuration) { EnsureValue(configuration.BucketName, nameof(BucketName)); - EnsureValue(configuration.Routes.Uploads, $"{SectionName}:Route:Upload"); - EnsureValue(configuration.ServiceUrl, nameof(ServiceUrl)); EnsureValue(configuration.AccessKey, nameof(AccessKey)); EnsureValue(configuration.SecretAccessKey, nameof(SecretAccessKey)); @@ -45,9 +41,4 @@ namespace Musify.Application.Configuration throw new InvalidOperationException($"{name} configuration value not found."); } } - - public class StorageRoutes - { - public string Uploads { get; set; } = "Uploads"; - } } \ No newline at end of file diff --git a/Application/Pictures/PictureService.cs b/Application/Pictures/PictureService.cs index 7ee8348..0dfcedc 100644 --- a/Application/Pictures/PictureService.cs +++ b/Application/Pictures/PictureService.cs @@ -23,7 +23,7 @@ namespace Musify.Application.Pictures var upload = new Upload { Id = pictureId, - KeyName = $"{storageConfiguration.Routes.Uploads}/{pictureId}", + KeyName = keyName, ContentType = contentType, BucketName = storageConfiguration.BucketName, EntityType = entityType, @@ -39,7 +39,7 @@ namespace Musify.Application.Pictures pictureStream, contentType, storageConfiguration.BucketName, - $"{storageConfiguration.Routes.Uploads}/{pictureId}", + keyName, cancellationToken); if (!uploadResult.IsSuccess) diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 76851f7..5127c3b 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -49,12 +49,18 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList request.PictureContentType, request.PictureStream, [ - new PictureResize(playListConfiguration.SmallPictureWidth, - playListConfiguration.SmallPictureHeight, playListConfiguration.Routes.SmallPictures), - new PictureResize(playListConfiguration.MediumPictureWidth, - playListConfiguration.MediumPictureHeight, playListConfiguration.Routes.MediumPictures), - new PictureResize(playListConfiguration.LargePictureWidth, - playListConfiguration.LargePictureHeight, playListConfiguration.Routes.LargePictures) + new PictureResize( + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + playListConfiguration.Routes.SmallPictures), + new PictureResize( + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + playListConfiguration.Routes.MediumPictures), + new PictureResize( + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight, + playListConfiguration.Routes.LargePictures) ], cancellationToken); From 34c213ef5a4b234e17639006d44c8ce1c79096b3 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 01:09:02 +0100 Subject: [PATCH 020/172] jobs and others --- Application/Application.csproj | 2 +- .../Contracts/Application/IPictureService.cs | 12 -- .../Contracts/Infrastructure/IDatabase.cs | 2 + Application/Events/RemoveFileEvent.cs | 6 + Application/Events/ResizePictureEvent.cs | 7 + Application/Events/ResizePictureItems.cs | 7 + .../Commands/ResizePicture/ResizeCommand.cs | 16 -- .../ResizePicture/ResizeCommandHandler.cs | 18 --- .../Pictures/Contracts/PictureResize.cs | 7 - .../Pictures/Events/PictureResizeEvent.cs | 8 - .../Pictures/PictureDependencyInjection.cs | 13 -- Application/Pictures/PictureService.cs | 61 -------- .../CreatePlayList/CreatePlayListCommand.cs | 1 + .../CreatePlayListCommandHandler.cs | 47 +++--- .../DeletePlayList/DeletePlayListCommand.cs | 8 + .../DeletePlayListCommandHandler.cs | 36 +++++ Domain/Entities/EntityType.cs | 9 -- Domain/Entities/Job.cs | 29 ++++ Domain/Entities/JobState.cs | 10 ++ Domain/Entities/JobType.cs | 8 + Domain/Entities/Upload.cs | 8 +- Domain/Entities/UploadState.cs | 5 +- .../Consumers/PictureResizerConsumer.cs | 138 ------------------ .../Messaging/Consumers/RemoveFileConsumer.cs | 14 ++ .../Consumers/ResizePictureConsumer.cs | 138 ++++++++++++++++++ .../MassTransitDependencyInjection.cs | 6 +- Infrastructure/Persistence/Database.cs | 2 + Infrastructure/Storage/StorageHandler.cs | 32 +++- 28 files changed, 334 insertions(+), 316 deletions(-) delete mode 100644 Application/Contracts/Application/IPictureService.cs create mode 100644 Application/Events/RemoveFileEvent.cs create mode 100644 Application/Events/ResizePictureEvent.cs create mode 100644 Application/Events/ResizePictureItems.cs delete mode 100644 Application/Pictures/Commands/ResizePicture/ResizeCommand.cs delete mode 100644 Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs delete mode 100644 Application/Pictures/Contracts/PictureResize.cs delete mode 100644 Application/Pictures/Events/PictureResizeEvent.cs delete mode 100644 Application/Pictures/PictureDependencyInjection.cs delete mode 100644 Application/Pictures/PictureService.cs create mode 100644 Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs create mode 100644 Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs delete mode 100644 Domain/Entities/EntityType.cs create mode 100644 Domain/Entities/Job.cs create mode 100644 Domain/Entities/JobState.cs create mode 100644 Domain/Entities/JobType.cs delete mode 100644 Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs create mode 100644 Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs create mode 100644 Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs 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); } } From eb9bac6cf9f67efe9d19400ebbcbf074daf09367 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 01:20:05 +0100 Subject: [PATCH 021/172] Remove files consumer --- .../CreatePlayListCommandHandler.cs | 11 ++--- .../DeletePlayListCommandHandler.cs | 29 ++++++++++--- Domain/Entities/PlayList.cs | 4 ++ .../Messaging/Consumers/RemoveFileConsumer.cs | 42 +++++++++++++++++-- .../MassTransitDependencyInjection.cs | 5 +++ 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index d47cd82..8ff6c82 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -25,20 +25,21 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList return Result.NotFound($"User with Id {request.UserId} does not exist."); } + var imageId = Guid.NewGuid(); + var playList = new PlayList { UserId = request.UserId, Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, - SmallPictureKeyName = playListConfiguration.Routes.SmallPictures, - MediumPictureKeyName = playListConfiguration.Routes.MediumPictures, - LargePictureKeyName = playListConfiguration.Routes.LargePictures, + OriginalPictureKeyName = $"{imageId}/{request.PictureFileType}", + SmallPictureKeyName = playListConfiguration.Routes.PresetSmallPicture, + MediumPictureKeyName = playListConfiguration.Routes.PresetMediumPicture, + LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, }; await database.PlayLists.AddAsync(playList, cancellationToken); - var imageId = Guid.NewGuid(); - var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}.{request.PictureFileType}"; var uploadResult = await storageHandler.UploadFileAsync( diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index e7ca9b7..5738e15 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -1,13 +1,15 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Events; namespace Musify.Application.PlayLists.Commands.DeletePlayList { - public class DeletePlayListCommandHandler(IDatabase database, IStorageHandler storageHandler, - StorageConfiguration storageConfiguration, ILogger logger) + public class DeletePlayListCommandHandler(IDatabase database, IPublishEndpoint publishEndpoint, + StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) : IRequestHandler> { public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) @@ -26,11 +28,28 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList return Result.Unauthorized(); } + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.OriginalPictureKeyName), cancellationToken); + + if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.SmallPictureKeyName), cancellationToken); + + if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.MediumPictureKeyName), cancellationToken); + + if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.LargePictureKeyName), cancellationToken); + database.PlayLists.Remove(playList); - await database.SaveChangesAsync(cancellationToken); - return Result.NoContent(); } } -} +} \ No newline at end of file diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 44d751f..876854e 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -25,6 +25,10 @@ namespace Musify.Domain.Entities [MaxLength(254)] public required string Description { get; set; } = "No description was provided."; + [Required] + [MaxLength(64)] + public required string OriginalPictureKeyName { get; set; } + [Required] [MaxLength(64)] public required string SmallPictureKeyName { get; set; } diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 29b4244..ce79d4f 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -1,14 +1,50 @@ 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 RemoveFileConsumer + public class RemoveFileConsumer(IDatabase database, IStorageHandler storageHandler, ILogger logger) : IConsumer { - public Task Consume(ConsumeContext context) + public const string QueueName = "file-remove-queue"; + + public async Task Consume(ConsumeContext consumeContext) { - throw new NotImplementedException(); + var payload = JsonSerializer.Serialize(consumeContext.Message); + + var job = await database.Jobs + .SingleOrDefaultAsync(j => j.JobState == JobState.Failed && j.Payload == payload); + + if (job is null) + { + job = new Job + { + JobType = JobType.RemoveFile, + Payload = payload + }; + await database.Jobs.AddAsync(job, consumeContext.CancellationToken); + } + else + { + job.RetryCount++; + job.JobState = JobState.Pending; + database.Jobs.Update(job); + } + + var removeFile = await storageHandler.RemoveFileAsync( + consumeContext.Message.BucketName, + consumeContext.Message.KeyName, + consumeContext.CancellationToken); + + if (!removeFile.IsSuccess) + job.JobState = JobState.Failed; + + await database.SaveChangesAsync(consumeContext.CancellationToken); } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index a38f593..3d7cf60 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -38,6 +38,7 @@ namespace Musify.Infrastructure.Messaging serviceDescriptors.AddMassTransit(options => { options.AddConsumer(); + options.AddConsumer(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -62,6 +63,10 @@ namespace Musify.Infrastructure.Messaging { endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); + busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); } ); } ); } From 3632c0d278f066b0689e80ba6f9f9c8257bb7762 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 09:27:22 +0100 Subject: [PATCH 022/172] . --- .../Commands/CreatePlayList/CreatePlayListCommandHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 8ff6c82..67c0a81 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -33,7 +33,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, - OriginalPictureKeyName = $"{imageId}/{request.PictureFileType}", + OriginalPictureKeyName = $"{imageId}.{request.PictureFileType}", SmallPictureKeyName = playListConfiguration.Routes.PresetSmallPicture, MediumPictureKeyName = playListConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, From 69f7cbeb05dc25c45b268d3e518bd9a672bf743e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 09:33:39 +0100 Subject: [PATCH 023/172] Fix --- .../PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs | 2 +- .../PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs index d813000..c00a97b 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs @@ -11,5 +11,5 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList Stream PictureStream, string PictureFileType, string PictureContentType) - : IRequest>; + : IRequest> >; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs index 50d4cad..4057db9 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs @@ -4,5 +4,5 @@ using DispatchR.Abstractions.Send; namespace Musify.Application.PlayLists.Commands.DeletePlayList { public record DeletePlayListCommand(Guid UserId, Guid PlayListId) - : IRequest; + : IRequest >; } \ No newline at end of file From 34b2a30738682785e69d1ae3b63997d222972430 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 09:41:48 +0100 Subject: [PATCH 024/172] Add DesignSettings --- Infrastructure/DesignSettings.json | 5 +++++ Infrastructure/Persistence/DatabaseDesignTimeFactory.cs | 7 ++----- 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 Infrastructure/DesignSettings.json diff --git a/Infrastructure/DesignSettings.json b/Infrastructure/DesignSettings.json new file mode 100644 index 0000000..afb3092 --- /dev/null +++ b/Infrastructure/DesignSettings.json @@ -0,0 +1,5 @@ +{ + "Database": { + "ConnectionString": "" + } +} \ No newline at end of file diff --git a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs index dd6aa29..b1a3695 100644 --- a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs +++ b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs @@ -11,12 +11,9 @@ namespace Musify.Infrastructure.Persistence { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("AppSettings.json") + .AddJsonFile("DesignSettings.json") .Build(); - - var databaseConfiguration = configuration.GetSection(DatabaseConfiguration.SectionName) - .Get() - ?? throw new InvalidOperationException($"Failed to load {DatabaseConfiguration.SectionName} configuration."); + var databaseConfiguration = DatabaseConfiguration.Load(configuration); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql(databaseConfiguration.ConnectionString); From 67186476ce1abbc0e524f1e9176da90b9cffc4fa Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 10:41:32 +0100 Subject: [PATCH 025/172] Changes --- Application/Configuration/PlayListConfiguration.cs | 2 +- .../CreatePlayList/CreatePlayListCommandHandler.cs | 4 ++-- .../Identity/KeycloakDependencyInjection.cs | 10 +++++++++- Infrastructure/Infrastructure.csproj | 2 ++ .../Messaging/Consumers/RemoveFileConsumer.cs | 10 ++++++++++ .../Persistence/DatabaseDependencyInjection.cs | 12 ++---------- .../Persistence/DatabaseDesignTimeFactory.cs | 1 + 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 62d2a7f..abaaeb0 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -62,7 +62,7 @@ namespace Musify.Application.Configuration public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp"; - public string PresetMediumPicture { get; set; } = "PresettMediumPicture.webp"; + public string PresetMediumPicture { get; set; } = "PresetMediumPicture.webp"; public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; } diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 67c0a81..f122f2f 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -33,14 +33,14 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, - OriginalPictureKeyName = $"{imageId}.{request.PictureFileType}", + OriginalPictureKeyName = $"{imageId}{request.PictureFileType}", SmallPictureKeyName = playListConfiguration.Routes.PresetSmallPicture, MediumPictureKeyName = playListConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, }; await database.PlayLists.AddAsync(playList, cancellationToken); - var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}.{request.PictureFileType}"; + var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}{request.PictureFileType}"; var uploadResult = await storageHandler.UploadFileAsync( request.PictureStream, diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs index f23619f..6ff5c3b 100644 --- a/Infrastructure/Identity/KeycloakDependencyInjection.cs +++ b/Infrastructure/Identity/KeycloakDependencyInjection.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using Keycloak.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; @@ -11,6 +12,13 @@ namespace Musify.Infrastructure.Identity { var keycloakConfiguration = KeycloakConfiguration.Load(configuration); serviceDescriptors.AddSingleton(keycloakConfiguration); + serviceDescriptors.AddScoped(serviceProvider => + { + return new KeycloakClient( + keycloakConfiguration.BaseUrl, + keycloakConfiguration.Username, + keycloakConfiguration.Password); + } ); serviceDescriptors.AddScoped(); } } diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 75091d6..3bc091b 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -5,6 +5,7 @@ enable enable Musify.Infrastructure + 07d419fc-9ade-471a-bc0e-aebece785b30 @@ -19,6 +20,7 @@ + diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index ce79d4f..3c86b06 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -28,12 +28,14 @@ namespace Musify.Infrastructure.Messaging.Consumers Payload = payload }; await database.Jobs.AddAsync(job, consumeContext.CancellationToken); + logger.LogInformation("Created new job with id {JobId} for removing file {BucketName}/{KeyName}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName); } else { job.RetryCount++; job.JobState = JobState.Pending; database.Jobs.Update(job); + logger.LogInformation("Retrying job with id {JobId} for removing file {BucketName}/{KeyName}. Retry count: {RetryCount}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.RetryCount); } var removeFile = await storageHandler.RemoveFileAsync( @@ -42,7 +44,15 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.CancellationToken); if (!removeFile.IsSuccess) + { job.JobState = JobState.Failed; + logger.LogInformation("Failed to remove file {BucketName}/{KeyName} for job with id {JobId}. Error: {ErrorMessage}", consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, string.Join("; ", removeFile.Errors)); + } + else + { + job.CompletedAt = DateTime.UtcNow; + logger.LogInformation("Successfully removed file {BucketName}/{KeyName} for job with id {JobId}.", consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id); + } await database.SaveChangesAsync(consumeContext.CancellationToken); } diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index f924e98..349e91c 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Musify.Application.Contracts.Infrastructure; @@ -9,14 +8,7 @@ namespace Musify.Infrastructure.Persistence { public static void AddDatabase(this IServiceCollection serviceDescriptors, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString("Default") - ?? throw new InvalidOperationException("Default Connection connection string not found."); - - serviceDescriptors.AddDbContext(options => - { - options.UseNpgsql(connectionString); - } ); - + serviceDescriptors.AddDbContext(); serviceDescriptors.AddScoped(serviceProvider => serviceProvider.GetRequiredService()); } } diff --git a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs index b1a3695..5568178 100644 --- a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs +++ b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs @@ -12,6 +12,7 @@ namespace Musify.Infrastructure.Persistence var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("DesignSettings.json") + .AddUserSecrets() .Build(); var databaseConfiguration = DatabaseConfiguration.Load(configuration); From d1fd6fb9a0c0116fbff49a5746258ff9484c1ad4 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 17:12:08 +0100 Subject: [PATCH 026/172] Config changes --- Application/Application.csproj | 2 + .../ConfigurationDependencyInjection.cs | 20 ------- .../Configuration/PlayListConfiguration.cs | 56 ++++++------------- .../Configuration/StorageConfiguration.cs | 40 ++++--------- .../DispatchR/DispatchRDependencyInjection.cs | 13 ----- .../ConfigurationDependencyInjection.cs | 20 ------- .../Configuration/DatabaseConfiguration.cs | 26 ++------- .../Configuration/KeycloakConfiguration.cs | 38 ++++--------- .../Identity/KeycloakDependencyInjection.cs | 8 ++- .../Identity/KeycloakIdentityService.cs | 5 +- 10 files changed, 52 insertions(+), 176 deletions(-) delete mode 100644 Application/Configuration/ConfigurationDependencyInjection.cs delete mode 100644 Application/DispatchR/DispatchRDependencyInjection.cs delete mode 100644 Infrastructure/Configuration/ConfigurationDependencyInjection.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index f9b7155..0ae48f6 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -14,6 +14,8 @@ + + diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs deleted file mode 100644 index d1e0fab..0000000 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Musify.Application.Configuration -{ - public static class ConfigurationDependencyInjection - { - public static void AddPlayListConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - var playListConfiguration = PlayListConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(playListConfiguration); - } - - public static void AddStorageConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - var storageConfiguration = StorageConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(storageConfiguration); - } - } -} \ No newline at end of file diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index abaaeb0..18707cd 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; namespace Musify.Application.Configuration { @@ -6,79 +6,55 @@ namespace Musify.Application.Configuration { public const string SectionName = "PlayListConfiguration"; + [Required] public Routes Routes { get; set; } = new Routes(); + [Required] public PicturesSizes PicturesSizes { get; set; } = new PicturesSizes(); - - public static PlayListConfiguration Load(IConfiguration configuration) - { - var section = configuration.GetSection(SectionName).Get() - ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); - - Validate(section); - return section; - } - - static void Validate(PlayListConfiguration configuration) - { - EnsureValue(configuration.Routes.OriginalPictures, $"{SectionName}:BucketName"); - EnsureValue(configuration.Routes.SmallPictures, $"{SectionName}:Routes:SmallPictures"); - EnsureValue(configuration.Routes.MediumPictures, $"{SectionName}:Routes:MediumPictures"); - EnsureValue(configuration.Routes.LargePictures, $"{SectionName}:Routes:LargePictures"); - EnsureValue(configuration.Routes.PresetSmallPicture, $"{SectionName}:Routes:SmallPictures"); - EnsureValue(configuration.Routes.PresetMediumPicture, $"{SectionName}:Routes:MediumPictures"); - EnsureValue(configuration.Routes.PresetLargePicture, $"{SectionName}:Routes:LargePictures"); - - EnsurePositiveValue(configuration.PicturesSizes.SmallPictureWidth, $"{SectionName}:SmallPictureWidth"); - EnsurePositiveValue(configuration.PicturesSizes.SmallPictureHeight, $"{SectionName}:SmallPictureHeight"); - EnsurePositiveValue(configuration.PicturesSizes.MediumPictureWidth, $"{SectionName}:MediumPictureWidth"); - EnsurePositiveValue(configuration.PicturesSizes.MediumPictureHeight, $"{SectionName}:MediumPictureHeight"); - EnsurePositiveValue(configuration.PicturesSizes.LargePictureWidth, $"{SectionName}:LargePictureWidth"); - EnsurePositiveValue(configuration.PicturesSizes.LargePictureHeight, $"{SectionName}:LargePictureHeight"); - } - - static void EnsureValue(string value, string name) - { - if (string.IsNullOrEmpty(value)) - throw new InvalidOperationException($"{name} configuration value not found."); - } - - static void EnsurePositiveValue(int value, string name) - { - if (value < 1) - throw new InvalidOperationException($"{name} configuration value must be a positive integer."); - } } public class Routes { + [Required] public string OriginalPictures { get; set; } = "OriginalPictures"; + [Required] public string SmallPictures { get; set; } = "SmallPictures"; + [Required] public string MediumPictures { get; set; } = "MediumPictures"; + [Required] public string LargePictures { get; set; } = "LargePictures"; + [Required] public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp"; + [Required] public string PresetMediumPicture { get; set; } = "PresetMediumPicture.webp"; + [Required] public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; } public class PicturesSizes { + [Range(1, 1024)] public int SmallPictureWidth { get; set; } = 128; + [Range(1, 1024)] public int SmallPictureHeight { get; set; } = 128; + [Range(1, 1024)] public int MediumPictureWidth { get; private set; } = 256; + [Range(1, 1024)] public int MediumPictureHeight { get; private set; } = 256; + [Range(1, 1024)] public int LargePictureWidth { get; private set; } = 512; + [Range(1, 1024)] public int LargePictureHeight { get; private set; } = 512; } } \ No newline at end of file diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs index d02353f..70ea2b6 100644 --- a/Application/Configuration/StorageConfiguration.cs +++ b/Application/Configuration/StorageConfiguration.cs @@ -1,44 +1,28 @@ -using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; namespace Musify.Application.Configuration { +#pragma warning disable CS8618 public class StorageConfiguration { public const string SectionName = "Storage"; - public string BucketName { get; set; } = string.Empty; + [Required] + public string BucketName { get; set; } - public string ServiceUrl { get; set; } = string.Empty; + [Required] + public string ServiceUrl { get; set; } - public string AccessKey { get; set; } = string.Empty; + [Required] + public string AccessKey { get; set; } - public string SecretAccessKey { get; set; } = string.Empty; + [Required] + public string SecretAccessKey { get; set; } + [Required] public bool ForcePathStyle { get; set; } + [Required] public bool UseHttp { get; set; } - - public static StorageConfiguration Load(IConfiguration configuration) - { - var section = configuration.GetSection(SectionName).Get() - ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); - - Validate(section); - return section; - } - - static void Validate(StorageConfiguration configuration) - { - EnsureValue(configuration.BucketName, nameof(BucketName)); - EnsureValue(configuration.ServiceUrl, nameof(ServiceUrl)); - EnsureValue(configuration.AccessKey, nameof(AccessKey)); - EnsureValue(configuration.SecretAccessKey, nameof(SecretAccessKey)); - } - - static void EnsureValue(string value, string name) - { - if (string.IsNullOrEmpty(value)) - throw new InvalidOperationException($"{name} configuration value not found."); - } } } \ No newline at end of file diff --git a/Application/DispatchR/DispatchRDependencyInjection.cs b/Application/DispatchR/DispatchRDependencyInjection.cs deleted file mode 100644 index 2b058d8..0000000 --- a/Application/DispatchR/DispatchRDependencyInjection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using DispatchR.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Musify.Application.DispatchR -{ - public static class DispatchRDependencyInjection - { - public static void AddDispatchR(this IServiceCollection serviceDescriptors) - { - serviceDescriptors.AddDispatchR(typeof(DispatchRDependencyInjection).Assembly); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs deleted file mode 100644 index 815fb0f..0000000 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Musify.Infrastructure.Configuration -{ - public static class ConfigurationDependencyInjection - { - public static void AddKeycloakConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - var keycloakConfiguration = KeycloakConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(keycloakConfiguration); - } - - public static void AddDatabaseConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - var databaseConfiguration = DatabaseConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(databaseConfiguration); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Configuration/DatabaseConfiguration.cs b/Infrastructure/Configuration/DatabaseConfiguration.cs index aeef73d..5996e49 100644 --- a/Infrastructure/Configuration/DatabaseConfiguration.cs +++ b/Infrastructure/Configuration/DatabaseConfiguration.cs @@ -1,31 +1,13 @@ -using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; namespace Musify.Infrastructure.Configuration { +#pragma warning disable CS8618 public class DatabaseConfiguration { public const string SectionName = "Database"; - public string ConnectionString { get; set; } = string.Empty; - - public static DatabaseConfiguration Load(IConfiguration configuration) - { - var databaseConfiguration = configuration.GetSection(SectionName).Get() - ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); - - Validate(databaseConfiguration); - return databaseConfiguration; - } - - static void Validate(DatabaseConfiguration configuration) - { - EnsureValue(configuration.ConnectionString, nameof(configuration.ConnectionString)); - } - - static void EnsureValue(string value, string name) - { - if (string.IsNullOrEmpty(value)) - throw new InvalidOperationException($"{name} configuration value not found."); - } + [Required] + public string ConnectionString { get; set; } } } \ No newline at end of file diff --git a/Infrastructure/Configuration/KeycloakConfiguration.cs b/Infrastructure/Configuration/KeycloakConfiguration.cs index 23d0198..635dc07 100644 --- a/Infrastructure/Configuration/KeycloakConfiguration.cs +++ b/Infrastructure/Configuration/KeycloakConfiguration.cs @@ -1,40 +1,22 @@ -using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; namespace Musify.Infrastructure.Configuration { +#pragma warning disable CS8618 public class KeycloakConfiguration { public const string SectionName = "Keycloak"; - public string BaseUrl { get; set; } = string.Empty; + [Required] + public string BaseUrl { get; set; } - public string Realm { get; set; } = string.Empty; + [Required] + public string Realm { get; set; } - public string Username { get; set; } = string.Empty; + [Required] + public string Username { get; set; } - public string Password { get; set; } = string.Empty; - - public static KeycloakConfiguration Load(IConfiguration configuration) - { - var keycloakConfiguration = configuration.GetSection(SectionName).Get() - ?? throw new InvalidOperationException($"{SectionName} configuration section not found."); - - Validate(keycloakConfiguration); - return keycloakConfiguration; - } - - static void Validate(KeycloakConfiguration configuration) - { - EnsureValue(configuration.BaseUrl, nameof(configuration.BaseUrl)); - EnsureValue(configuration.Realm, nameof(configuration.Realm)); - EnsureValue(configuration.Username, nameof(configuration.Username)); - EnsureValue(configuration.Password, nameof(configuration.Password)); - } - - static void EnsureValue(string value, string name) - { - if (string.IsNullOrEmpty(value)) - throw new InvalidOperationException($"{name} configuration value not found."); - } + [Required] + public string Password { get; set; } } } \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs index 6ff5c3b..af34c25 100644 --- a/Infrastructure/Identity/KeycloakDependencyInjection.cs +++ b/Infrastructure/Identity/KeycloakDependencyInjection.cs @@ -10,10 +10,14 @@ namespace Musify.Infrastructure.Identity { public static void AddKeycloakAuthentication(this IServiceCollection serviceDescriptors, IConfiguration configuration) { - var keycloakConfiguration = KeycloakConfiguration.Load(configuration); - serviceDescriptors.AddSingleton(keycloakConfiguration); + var keycloakConfiguration = serviceDescriptors.AddOptionsWithValidateOnStart() + .Bind(configuration) + .ValidateDataAnnotations(); + serviceDescriptors.AddScoped(serviceProvider => { + var keycloakConfiguration = serviceProvider.GetRequiredService(); + return new KeycloakClient( keycloakConfiguration.BaseUrl, keycloakConfiguration.Username, diff --git a/Infrastructure/Identity/KeycloakIdentityService.cs b/Infrastructure/Identity/KeycloakIdentityService.cs index ecb3f13..a2d1d8f 100644 --- a/Infrastructure/Identity/KeycloakIdentityService.cs +++ b/Infrastructure/Identity/KeycloakIdentityService.cs @@ -23,15 +23,14 @@ namespace Musify.Infrastructure.Identity } catch (OperationCanceledException) { - throw; + logger.LogError("Operation to retrieve Keycloak user with id {KeycloakId} was cancelled.", keycloakId); + return Result.Error("Operation was cancelled."); } catch (Exception exception) { logger.LogWarning(exception, "Keycloak user with id {KeycloakId} could not be retrieved.", keycloakId); - return Result.NotFound(); } - return Result.Success(KeycloakUserMapper.Map(keycloakUser)); } From 23852709cb3a89e6e14be1c584996feebe2a4a98 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 17:22:27 +0100 Subject: [PATCH 027/172] Changes on config and DI --- .../ConfigurationDependencyInjection.cs | 35 ++++++++++++++++ .../ConfigurationDependencyInjection.cs | 35 ++++++++++++++++ .../Identity/KeycloakDependencyInjection.cs | 7 ++-- .../InfrastructureDependencyInjection.cs | 42 +++++++++++++++++++ .../MassTransitDependencyInjection.cs | 7 +++- .../DatabaseDependencyInjection.cs | 4 +- .../Persistence/DatabaseDesignTimeFactory.cs | 6 ++- .../Pictures/PictureDependencyInjection.cs | 3 +- .../Storage/StorageDependencyInjection.cs | 4 +- 9 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 Application/Configuration/ConfigurationDependencyInjection.cs create mode 100644 Infrastructure/Configuration/ConfigurationDependencyInjection.cs create mode 100644 Infrastructure/InfrastructureDependencyInjection.cs diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs new file mode 100644 index 0000000..903d489 --- /dev/null +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Musify.Application.Configuration +{ + public static class ConfigurationDependencyInjection + { + public static IServiceCollection AddApplicationConfigurations(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddValidatedConfiguration(configuration, StorageConfiguration.SectionName) + .AddValidatedConfiguration(configuration, PlayListConfiguration.SectionName); + + return serviceDescriptors; + } + + static IServiceCollection AddValidatedConfiguration( + this IServiceCollection serviceDescriptors, + IConfiguration configuration, + string sectionName) + where TConfiguration : class + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(sectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } + } +} diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs new file mode 100644 index 0000000..edd3a70 --- /dev/null +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -0,0 +1,35 @@ +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(configuration, DatabaseConfiguration.SectionName) + .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName); + + return serviceDescriptors; + } + + static IServiceCollection AddValidatedConfiguration( + this IServiceCollection serviceDescriptors, + IConfiguration configuration, + string sectionName) + where TConfiguration : class + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(sectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } + } +} diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs index af34c25..6e5accb 100644 --- a/Infrastructure/Identity/KeycloakDependencyInjection.cs +++ b/Infrastructure/Identity/KeycloakDependencyInjection.cs @@ -8,10 +8,10 @@ namespace Musify.Infrastructure.Identity { public static class KeycloakDependencyInjection { - public static void AddKeycloakAuthentication(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddKeycloakAuthentication(this IServiceCollection serviceDescriptors, IConfiguration configuration) { - var keycloakConfiguration = serviceDescriptors.AddOptionsWithValidateOnStart() - .Bind(configuration) + serviceDescriptors.AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName)) .ValidateDataAnnotations(); serviceDescriptors.AddScoped(serviceProvider => @@ -24,6 +24,7 @@ namespace Musify.Infrastructure.Identity keycloakConfiguration.Password); } ); serviceDescriptors.AddScoped(); + return serviceDescriptors; } } } \ No newline at end of file diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs new file mode 100644 index 0000000..3d52f0f --- /dev/null +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Identity; +using Musify.Infrastructure.Messaging; +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) + .AddKeycloakAuthentication(configuration) + .AddStorageHandler() + .AddPictureHandler() + .AddMassTransitClient(configuration) + .AddScoped(); + + return serviceDescriptors; + } + + public static IServiceCollection AddInfrastructureWorkerModules(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddInfrastructureConfigurations(configuration) + .AddDatabase(configuration) + .AddStorageHandler() + .AddPictureHandler() + .AddMassTransitConsumers(configuration) + .AddScoped(); + + return serviceDescriptors; + } + } +} diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index 3d7cf60..def4d94 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -7,7 +7,7 @@ namespace Musify.Infrastructure.Messaging { public static class MassTransitDependencyInjection { - public static void AddMassTransitClient(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors.AddMassTransit(options => { @@ -31,9 +31,10 @@ namespace Musify.Infrastructure.Messaging } ); } ); } ); + return serviceDescriptors; } - public static void AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors.AddMassTransit(options => { @@ -69,6 +70,8 @@ namespace Musify.Infrastructure.Messaging } ); } ); } ); + + return serviceDescriptors; } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index 349e91c..4152976 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -6,10 +6,12 @@ namespace Musify.Infrastructure.Persistence { public static class DatabaseDependencyInjection { - public static void AddDatabase(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddDatabase(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors.AddDbContext(); serviceDescriptors.AddScoped(serviceProvider => serviceProvider.GetRequiredService()); + + return serviceDescriptors; } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs index 5568178..18f38a9 100644 --- a/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs +++ b/Infrastructure/Persistence/DatabaseDesignTimeFactory.cs @@ -14,7 +14,11 @@ namespace Musify.Infrastructure.Persistence .AddJsonFile("DesignSettings.json") .AddUserSecrets() .Build(); - var databaseConfiguration = DatabaseConfiguration.Load(configuration); + + var databaseConfiguration = configuration + .GetRequiredSection(DatabaseConfiguration.SectionName) + .Get() + ?? throw new InvalidOperationException($"{DatabaseConfiguration.SectionName} configuration section not found."); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql(databaseConfiguration.ConnectionString); diff --git a/Infrastructure/Pictures/PictureDependencyInjection.cs b/Infrastructure/Pictures/PictureDependencyInjection.cs index 2abd860..4d955c7 100644 --- a/Infrastructure/Pictures/PictureDependencyInjection.cs +++ b/Infrastructure/Pictures/PictureDependencyInjection.cs @@ -5,9 +5,10 @@ namespace Musify.Infrastructure.Pictures { public static class PictureDependencyInjection { - public static void AddPictureHandler(this IServiceCollection serviceDescriptors) + public static IServiceCollection AddPictureHandler(this IServiceCollection serviceDescriptors) { serviceDescriptors.AddScoped(); + return serviceDescriptors; } } } \ No newline at end of file diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs index 6c54325..061f13c 100644 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -7,7 +7,7 @@ namespace Musify.Infrastructure.Storage { public static class StorageDependencyInjection { - public static void AddStorageHandler(this IServiceCollection serviceDescriptors) + public static IServiceCollection AddStorageHandler(this IServiceCollection serviceDescriptors) { serviceDescriptors.AddScoped(serviceProvider => { @@ -22,6 +22,8 @@ namespace Musify.Infrastructure.Storage } ); serviceDescriptors.AddScoped(); + + return serviceDescriptors; } } } \ No newline at end of file From 954101548a102a20216cb2f0b0df9294f4d3e449 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 19:50:15 +0100 Subject: [PATCH 028/172] Update playlist and some changes --- Application/Application.csproj | 5 - .../Contracts/Application/IFileData.cs | 13 ++ .../CreatePlayList/CreatePlayListCommand.cs | 5 +- .../CreatePlayListCommandHandler.cs | 56 +++++---- .../DeletePlayListCommandHandler.cs | 33 ++--- .../UpdatePlayList/UpdatePlayListCommand.cs | 15 +++ .../UpdatePlayListCommandHandler.cs | 115 ++++++++++++++++++ Domain/Entities/PlayList.cs | 3 +- 8 files changed, 196 insertions(+), 49 deletions(-) create mode 100644 Application/Contracts/Application/IFileData.cs create mode 100644 Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs create mode 100644 Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index 0ae48f6..cde91d6 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -24,9 +24,4 @@ - - - - - diff --git a/Application/Contracts/Application/IFileData.cs b/Application/Contracts/Application/IFileData.cs new file mode 100644 index 0000000..96a9347 --- /dev/null +++ b/Application/Contracts/Application/IFileData.cs @@ -0,0 +1,13 @@ +namespace Musify.Application.Contracts.Application +{ + public interface IFileData + { + Stream FileStream { get; } + + string FileName { get; } + + string FileType { get; } + + string ContentType { get; } + } +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs index c00a97b..b59d8ec 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Commands.CreatePlayList @@ -8,8 +9,6 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList Guid UserId, string Name, string Description, - Stream PictureStream, - string PictureFileType, - string PictureContentType) + IFileData? Picture) : IRequest> >; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index f122f2f..32c3ee8 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -33,36 +33,37 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, - OriginalPictureKeyName = $"{imageId}{request.PictureFileType}", SmallPictureKeyName = playListConfiguration.Routes.PresetSmallPicture, MediumPictureKeyName = playListConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, }; - await database.PlayLists.AddAsync(playList, cancellationToken); - var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}{request.PictureFileType}"; - - var uploadResult = await storageHandler.UploadFileAsync( - request.PictureStream, - request.PictureContentType, - storageConfiguration.BucketName, - imageKeyName, - cancellationToken); - - if (!uploadResult.IsSuccess) + if (request.Picture is not null) { - 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."); - } + playList.OriginalPictureKeyName = $"{imageId}{request.Picture.FileType}"; + var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}{request.Picture.FileType}"; - logger.LogInformation("PlayList with Id={PlayListId} created successfully for UserId={UserId}.", playList.Id, request.UserId); + var uploadResult = await storageHandler.UploadFileAsync( + request.Picture.FileStream, + request.Picture.ContentType, + storageConfiguration.BucketName, + imageKeyName, + cancellationToken); - await publishEndpoint.Publish(new ResizePictureEvent( - storageConfiguration.BucketName, - imageKeyName, - [ - new ResizePictureItems( + 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 publishEndpoint.Publish(new ResizePictureEvent( + storageConfiguration.BucketName, + imageKeyName, + [ + new ResizePictureItems( playListConfiguration.PicturesSizes.SmallPictureWidth, playListConfiguration.PicturesSizes.SmallPictureHeight, playListConfiguration.Routes.SmallPictures), @@ -74,10 +75,17 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ] ), cancellationToken); + ] ), cancellationToken); + } + else + { + logger.LogInformation("No picture provided for PlayList with Id={PlayListId}. Using Preset pictures.", playList.Id); + } + + await database.PlayLists.AddAsync(playList, cancellationToken); + await database.SaveChangesAsync(cancellationToken); logger.LogInformation("Picture for PlayList with Id={PlayListId} ", playList.Id); - return Result.Created(PlayListResponse.FromEntity(playList)); } } diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index 5738e15..ca066f3 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -28,24 +28,27 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList return Result.Unauthorized(); } - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.OriginalPictureKeyName), cancellationToken); - - if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) + { await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + storageConfiguration.BucketName, + playList.OriginalPictureKeyName), cancellationToken); - if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.SmallPictureKeyName), cancellationToken); - if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.MediumPictureKeyName), cancellationToken); + + if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.LargePictureKeyName), cancellationToken); + } database.PlayLists.Remove(playList); await database.SaveChangesAsync(cancellationToken); diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs new file mode 100644 index 0000000..1ef0e49 --- /dev/null +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs @@ -0,0 +1,15 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Commands.UpdatePlayList +{ + public record UpdatePlayListCommand( + Guid UserId, + Guid PlayListId, + string? NewName, + string? NewDescription, + IFileData? NewPicture) + : IRequest> >; +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs new file mode 100644 index 0000000..4a7b7de --- /dev/null +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -0,0 +1,115 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Events; +using Musify.Application.PlayLists.Contracts; + +namespace Musify.Application.PlayLists.Commands.UpdatePlayList +{ + public class UpdatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler, + ILogger logger, StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) + : IRequestHandler> > + { + public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) + { + var playList = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); + + if (playList is null) + { + logger.LogInformation("PlayList with id {PlayListId} not found", request.PlayListId); + return Result.NotFound(); + } + + if (playList.UserId != request.UserId) + { + logger.LogWarning("User with id {UserId} is not the owner of the PlayList with id {PlayListId}", request.UserId, request.PlayListId); + return Result.Unauthorized(); + } + + if (!string.IsNullOrWhiteSpace(request.NewName)) + playList.Name = request.NewName; + + if (!string.IsNullOrWhiteSpace(request.NewDescription)) + playList.Description = request.NewDescription; + + if (request.NewPicture is null) + { + database.PlayLists.Update(playList); + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Updated PlayList with id={PlayListId}.", playList.Id); + return Result.Success(PlayListResponse.FromEntity(playList)); + } + + if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.OriginalPictureKeyName), cancellationToken); + logger.LogInformation("Published event to remove old picture with key name {KeyName} from bucket {BucketName}", + playList.OriginalPictureKeyName, storageConfiguration.BucketName); + + if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.SmallPictureKeyName), cancellationToken); + + if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.MediumPictureKeyName), cancellationToken); + + if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.LargePictureKeyName), cancellationToken); + } + + var newImageId = Guid.NewGuid(); + var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{newImageId}{request.NewPicture.FileType}"; + + var uploadResult = await storageHandler.UploadFileAsync( + request.NewPicture.FileStream, + request.NewPicture.ContentType, + 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."); + } + + playList.OriginalPictureKeyName = newImageId.ToString(); + await publishEndpoint.Publish(new ResizePictureEvent( + storageConfiguration.BucketName, + imageKeyName, + [ + new ResizePictureItems( + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + playListConfiguration.Routes.SmallPictures), + new ResizePictureItems( + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + playListConfiguration.Routes.MediumPictures), + new ResizePictureItems( + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight, + playListConfiguration.Routes.LargePictures) + ] + ), cancellationToken); + + database.PlayLists.Update(playList); + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("Updated PlayList with id={PlayListId}.", playList.Id); + return Result.Success(PlayListResponse.FromEntity(playList)); + } + } +} \ No newline at end of file diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 876854e..7975558 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -25,9 +25,8 @@ namespace Musify.Domain.Entities [MaxLength(254)] public required string Description { get; set; } = "No description was provided."; - [Required] [MaxLength(64)] - public required string OriginalPictureKeyName { get; set; } + public string? OriginalPictureKeyName { get; set; } [Required] [MaxLength(64)] From d0a39b28244588be762cdc017ce4c8ef098b2e5c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 20:21:02 +0100 Subject: [PATCH 029/172] A lot of changes --- .github/copilot-instructions.md | 4 ++ AGENTS.md | 41 +++++++++++++++++++ .../ConfigurationDependencyInjection.cs | 2 +- .../Configuration/StorageConfiguration.cs | 28 ------------- Application/Configuration/StorageSettings.cs | 12 ++++++ .../CreatePlayListCommandHandler.cs | 6 +-- .../DeletePlayListCommandHandler.cs | 2 +- .../UpdatePlayListCommandHandler.cs | 8 ++-- .../CreateUser/CreateUserCommandHandler.cs | 10 ++++- ...akCommand.cs => GetUserByKeycloakQuery.cs} | 4 +- ...er.cs => GetUserByKeycloakQueryHandler.cs} | 4 +- .../Users/Queries/GetUsers/GetUsersCommand.cs | 9 ---- .../Users/Queries/GetUsers/GetUsersQuery.cs | 9 ++++ ...mandHandler.cs => GetUsersQueryHandler.cs} | 8 ++-- .../ConfigurationDependencyInjection.cs | 3 +- .../StorageClientConfiguration.cs | 24 +++++++++++ .../Storage/StorageDependencyInjection.cs | 4 +- 17 files changed, 119 insertions(+), 59 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md delete mode 100644 Application/Configuration/StorageConfiguration.cs create mode 100644 Application/Configuration/StorageSettings.cs rename Application/Users/Queries/GetUserByKeycloakId/{GetUserByKeycloakCommand.cs => GetUserByKeycloakQuery.cs} (91%) rename Application/Users/Queries/GetUserByKeycloakId/{GetUserByKeycloakCommandHandler.cs => GetUserByKeycloakQueryHandler.cs} (98%) delete mode 100644 Application/Users/Queries/GetUsers/GetUsersCommand.cs create mode 100644 Application/Users/Queries/GetUsers/GetUsersQuery.cs rename Application/Users/Queries/GetUsers/{GetUsersCommandHandler.cs => GetUsersQueryHandler.cs} (92%) create mode 100644 Infrastructure/Configuration/StorageClientConfiguration.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c13be9d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,4 @@ +# Copilot Instructions + +## Project Guidelines +- No crear pruebas/tests si el usuario no lo solicita explícitamente; evitar cambios no pedidos. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..41a2ff6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Contexto del proyecto para agentes de IA + +## Resumen rápido +- Proyecto backend en `.NET 10`. +- Estructura principal en capas: `Domain`, `Application`, `Infrastructure`. +- Patrón arquitectónico: `Clean Architecture` con enfoque `CQRS`. + +## Arquitectura +- **Domain (`Domain`)**: entidades de negocio puras. +- **Application (`Application`)**: + - Casos de uso (comandos/queries y handlers). + - Contratos (`Application.Contracts`). + - Configuración de aplicación (`Application.Configuration`). + - Mensajería orientada a eventos a través de `IEventBus`. +- **Infrastructure (`Infrastructure`)**: + - Persistencia y acceso a datos. + - Integraciones externas (Keycloak, almacenamiento, procesamiento de imágenes). + - Implementación de mensajería y consumidores. + +## CQRS / Mediación +- Se usa **DispatchR** (paquete `DispatchR.Mediator`) como variante de MediatR para orquestar comandos y consultas. +- Los casos de uso están organizados por feature con handlers dedicados (`Commands`, `Queries`). + +## Mensajería y workers +- Se usa **MassTransit** para publicación/consumo de eventos. +- Broker actual: **RabbitMQ** (`MassTransit.RabbitMQ`). +- Hay modo de registro para workers con consumidores en `Infrastructure.Messaging.Consumers`. +- Actualmente existen consumidores como: + - `ResizePictureConsumer` + - `RemoveFileConsumer` + +## Persistencia e integraciones +- `Entity Framework Core` + proveedor `Npgsql` (PostgreSQL). +- Integración de identidad con `Keycloak`. +- Almacenamiento con `AWS S3`. + +## Notas para agentes +- Mantener separación de responsabilidades por capa. +- Evitar lógica de infraestructura dentro de `Application` y `Domain`. +- Agregar nuevos casos de uso siguiendo el patrón CQRS existente (comando/query + handler). +- Para nuevas integraciones asíncronas, reutilizar el pipeline de `MassTransit` y la abstracción `IEventBus`. diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs index 903d489..4630946 100644 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -9,7 +9,7 @@ namespace Musify.Application.Configuration public static IServiceCollection AddApplicationConfigurations(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors - .AddValidatedConfiguration(configuration, StorageConfiguration.SectionName) + .AddValidatedConfiguration(configuration, StorageSettings.SectionName) .AddValidatedConfiguration(configuration, PlayListConfiguration.SectionName); return serviceDescriptors; diff --git a/Application/Configuration/StorageConfiguration.cs b/Application/Configuration/StorageConfiguration.cs deleted file mode 100644 index 70ea2b6..0000000 --- a/Application/Configuration/StorageConfiguration.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Musify.Application.Configuration -{ -#pragma warning disable CS8618 - public class StorageConfiguration - { - public const string SectionName = "Storage"; - - [Required] - public string BucketName { get; set; } - - [Required] - public string ServiceUrl { get; set; } - - [Required] - public string AccessKey { get; set; } - - [Required] - public string SecretAccessKey { get; set; } - - [Required] - public bool ForcePathStyle { get; set; } - - [Required] - public bool UseHttp { get; set; } - } -} \ No newline at end of file diff --git a/Application/Configuration/StorageSettings.cs b/Application/Configuration/StorageSettings.cs new file mode 100644 index 0000000..fa912b5 --- /dev/null +++ b/Application/Configuration/StorageSettings.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Application.Configuration +{ + public class StorageSettings + { + public const string SectionName = "Storage"; + + [Required] + public required string BucketName { get; set; } + } +} diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 32c3ee8..3951ffd 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -12,7 +12,7 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands.CreatePlayList { public class CreatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler, - ILogger logger, StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) + ILogger logger, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) @@ -57,7 +57,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList 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); + logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); await publishEndpoint.Publish(new ResizePictureEvent( storageConfiguration.BucketName, @@ -85,7 +85,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList await database.PlayLists.AddAsync(playList, cancellationToken); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("Picture for PlayList with Id={PlayListId} ", playList.Id); + logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playList.Id, request.UserId); return Result.Created(PlayListResponse.FromEntity(playList)); } } diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index ca066f3..0b73404 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -9,7 +9,7 @@ using Musify.Application.Events; namespace Musify.Application.PlayLists.Commands.DeletePlayList { public class DeletePlayListCommandHandler(IDatabase database, IPublishEndpoint publishEndpoint, - StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) + StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) : IRequestHandler> { public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 4a7b7de..337b122 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -11,7 +11,7 @@ using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Commands.UpdatePlayList { public class UpdatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler, - ILogger logger, StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) + ILogger logger, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) @@ -40,7 +40,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { database.PlayLists.Update(playList); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("Updated PlayList with id={PlayListId}.", playList.Id); + logger.LogInformation("Updated PlayList with id={PlayListId} without picture changes.", playList.Id); return Result.Success(PlayListResponse.FromEntity(playList)); } @@ -85,7 +85,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } - playList.OriginalPictureKeyName = newImageId.ToString(); + playList.OriginalPictureKeyName = imageKeyName; await publishEndpoint.Publish(new ResizePictureEvent( storageConfiguration.BucketName, imageKeyName, @@ -108,7 +108,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList database.PlayLists.Update(playList); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("Updated PlayList with id={PlayListId}.", playList.Id); + logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKeyName}.", playList.Id, imageKeyName); return Result.Success(PlayListResponse.FromEntity(playList)); } } diff --git a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs b/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs index f3f7773..2e72d70 100644 --- a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs +++ b/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs @@ -15,15 +15,21 @@ namespace Musify.Application.Users.Commands.CreateUser { logger.LogDebug("Handling {CommandName} with Id: {UserId}", nameof(CreateUserCommand), request.Id); - var existingUser = database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); + var existingUser = await database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); if (existingUser is not null) + { + logger.LogInformation("User with Id={UserId} already exists.", request.Id); return Result.Conflict($"A user with Id {request.Id} already exists."); + } var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); if (!keycloakUser.IsSuccess) + { + logger.LogWarning("Keycloak user with Id={UserId} was not found.", request.Id); return Result.NotFound("Keycloak user not found."); + } var newUser = new User { @@ -31,9 +37,9 @@ namespace Musify.Application.Users.Commands.CreateUser }; await database.Users.AddAsync(newUser, cancellationToken); - await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("User with Id={UserId} created successfully.", newUser.Id); return Result.Created(new UserResponse( newUser.Id.ToString(), keycloakUser.Value.Name, diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQuery.cs similarity index 91% rename from Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs rename to Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQuery.cs index 811578a..55910b2 100644 --- a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommand.cs +++ b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQuery.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Users.Contracts; @@ -6,4 +6,4 @@ namespace Musify.Application.Users.Queries.GetUserByKeycloakId { public record GetUserByKeycloakQuery(Guid Id) : IRequest >>; -} \ No newline at end of file +} diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs similarity index 98% rename from Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs rename to Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs index a77ed6e..bca8ee5 100644 --- a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakCommandHandler.cs +++ b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; @@ -32,4 +32,4 @@ namespace Musify.Application.Users.Queries.GetUserByKeycloakId user.UpdatedAt)); } } -} \ No newline at end of file +} diff --git a/Application/Users/Queries/GetUsers/GetUsersCommand.cs b/Application/Users/Queries/GetUsers/GetUsersCommand.cs deleted file mode 100644 index 77e6969..0000000 --- a/Application/Users/Queries/GetUsers/GetUsersCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ardalis.Result; -using DispatchR.Abstractions.Send; -using Musify.Application.Users.Contracts; - -namespace Musify.Application.Users.Queries.GetUsers -{ - public record GetUsersCommand(int PageNumber, int PageSize, string UsernameSearch) - : IRequest> >; -} \ No newline at end of file diff --git a/Application/Users/Queries/GetUsers/GetUsersQuery.cs b/Application/Users/Queries/GetUsers/GetUsersQuery.cs new file mode 100644 index 0000000..7d3aeb2 --- /dev/null +++ b/Application/Users/Queries/GetUsers/GetUsersQuery.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Users.Contracts; + +namespace Musify.Application.Users.Queries.GetUsers +{ + public record GetUsersQuery(int PageNumber, int PageSize, string UsernameSearch) + : IRequest> >; +} diff --git a/Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs b/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs similarity index 92% rename from Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs rename to Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs index f975fff..dc4d704 100644 --- a/Application/Users/Queries/GetUsers/GetUsersCommandHandler.cs +++ b/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; @@ -9,9 +9,9 @@ using X.PagedList.Extensions; namespace Musify.Application.Users.Queries.GetUsers { public class GetUsersQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) - : IRequestHandler >> + : IRequestHandler >> { - public async Task> Handle(GetUsersCommand request, CancellationToken cancellationToken) + public async Task> Handle(GetUsersQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -56,4 +56,4 @@ namespace Musify.Application.Users.Queries.GetUsers return Result.Success(response); } } -} \ No newline at end of file +} diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index edd3a70..a357138 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -10,7 +10,8 @@ namespace Musify.Infrastructure.Configuration { serviceDescriptors .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) - .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName); + .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) + .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/StorageClientConfiguration.cs b/Infrastructure/Configuration/StorageClientConfiguration.cs new file mode 100644 index 0000000..92e3caa --- /dev/null +++ b/Infrastructure/Configuration/StorageClientConfiguration.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class StorageClientConfiguration + { + public const string SectionName = "Storage"; + + [Required] + public required string ServiceUrl { get; set; } + + [Required] + public required string AccessKey { get; set; } + + [Required] + public required string SecretAccessKey { get; set; } + + [Required] + public bool ForcePathStyle { get; set; } + + [Required] + public bool UseHttp { get; set; } + } +} diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs index 061f13c..f6c71bd 100644 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -1,7 +1,7 @@ using Amazon.S3; using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Storage { @@ -11,7 +11,7 @@ namespace Musify.Infrastructure.Storage { serviceDescriptors.AddScoped(serviceProvider => { - var configuration = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetRequiredService(); var s3Configuration = new AmazonS3Config { ServiceURL = configuration.ServiceUrl, From 54e82f407f25a2f263159d6581d5199671e6b10d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:35:12 +0100 Subject: [PATCH 030/172] QOL Changes --- Application/Configuration/StorageSettings.cs | 2 + .../CreatePlayListCommandHandler.cs | 50 ++++--- .../DeletePlayListCommandHandler.cs | 67 ++++++++-- .../UpdatePlayListCommandHandler.cs | 124 +++++++++++++----- .../GetPlayListById/GetPlayListByIdQuery.cs | 2 +- .../Queries/GetPlayLists/GetPlayListsQuery.cs | 2 +- .../GetPlayListsByUserIdQuery.cs | 2 +- .../GetPlayListsByUserIdQueryHandler.cs | 3 +- .../Commands/CreateUser/CreateUserCommand.cs | 2 +- .../Commands/DeleteUser/DeleteUserCommand.cs | 2 +- .../StorageClientConfiguration.cs | 5 +- .../Storage/StorageDependencyInjection.cs | 1 - 12 files changed, 183 insertions(+), 79 deletions(-) diff --git a/Application/Configuration/StorageSettings.cs b/Application/Configuration/StorageSettings.cs index fa912b5..f2c0af1 100644 --- a/Application/Configuration/StorageSettings.cs +++ b/Application/Configuration/StorageSettings.cs @@ -7,6 +7,8 @@ namespace Musify.Application.Configuration public const string SectionName = "Storage"; [Required] + [MinLength(3)] + [MaxLength(63)] public required string BucketName { get; set; } } } diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 3951ffd..db4f02e 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -25,8 +25,6 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList return Result.NotFound($"User with Id {request.UserId} does not exist."); } - var imageId = Guid.NewGuid(); - var playList = new PlayList { UserId = request.UserId, @@ -40,14 +38,15 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList if (request.Picture is not null) { - playList.OriginalPictureKeyName = $"{imageId}{request.Picture.FileType}"; - var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}{request.Picture.FileType}"; + var imageId = Guid.NewGuid(); + var pictureName = $"{imageId}{request.Picture.FileType}"; + var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); var uploadResult = await storageHandler.UploadFileAsync( request.Picture.FileStream, request.Picture.ContentType, storageConfiguration.BucketName, - imageKeyName, + originalImageStorageKey, cancellationToken); if (!uploadResult.IsSuccess) @@ -57,25 +56,34 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } + playList.OriginalPictureKeyName = pictureName; logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); - await publishEndpoint.Publish(new ResizePictureEvent( - storageConfiguration.BucketName, - imageKeyName, - [ + try + { + await publishEndpoint.Publish(new ResizePictureEvent( + storageConfiguration.BucketName, + originalImageStorageKey, + [ new ResizePictureItems( - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.SmallPictures), - new ResizePictureItems( - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.MediumPictures), - new ResizePictureItems( - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - playListConfiguration.Routes.LargePictures) - ] ), cancellationToken); + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + playListConfiguration.Routes.SmallPictures), + new ResizePictureItems( + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + playListConfiguration.Routes.MediumPictures), + new ResizePictureItems( + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight, + playListConfiguration.Routes.LargePictures) + ] ), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish resize picture event for PlayList with id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); + } } else { diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index 0b73404..a92e092 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Application.PlayLists; namespace Musify.Application.PlayLists.Commands.DeletePlayList { @@ -28,26 +29,66 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList return Result.Unauthorized(); } - if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) + if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) { - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.OriginalPictureKeyName), cancellationToken); + var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, playList.OriginalPictureKeyName); + + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + originalImageStorageKey), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for original picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + { + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.SmallPictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for small picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + } if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + { + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.MediumPictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for medium picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + } if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + { + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.LargePictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for large picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + } } database.PlayLists.Remove(playList); diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 337b122..0092d1f 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -31,7 +31,10 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList } if (!string.IsNullOrWhiteSpace(request.NewName)) + { playList.Name = request.NewName; + playList.NormalizedName = request.NewName.Trim().ToUpperInvariant(); + } if (!string.IsNullOrWhiteSpace(request.NewDescription)) playList.Description = request.NewDescription; @@ -46,36 +49,78 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) { - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.OriginalPictureKeyName), cancellationToken); + var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, playList.OriginalPictureKeyName); + + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + originalImageStorageKey), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for original picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + logger.LogInformation("Published event to remove old picture with key name {KeyName} from bucket {BucketName}", - playList.OriginalPictureKeyName, storageConfiguration.BucketName); + originalImageStorageKey, storageConfiguration.BucketName); if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + { + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.SmallPictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for small picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + } if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + { + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.MediumPictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for medium picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + } if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - await publishEndpoint.Publish(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + { + try + { + await publishEndpoint.Publish(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.LargePictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for large picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + } + } } var newImageId = Guid.NewGuid(); - var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{newImageId}{request.NewPicture.FileType}"; + var pictureName = $"{newImageId}{request.NewPicture.FileType}"; + var newImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); var uploadResult = await storageHandler.UploadFileAsync( request.NewPicture.FileStream, request.NewPicture.ContentType, storageConfiguration.BucketName, - imageKeyName, + newImageStorageKey, cancellationToken); if (!uploadResult.IsSuccess) @@ -85,30 +130,39 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } - playList.OriginalPictureKeyName = imageKeyName; - await publishEndpoint.Publish(new ResizePictureEvent( - storageConfiguration.BucketName, - imageKeyName, - [ - new ResizePictureItems( - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.SmallPictures), - new ResizePictureItems( - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.MediumPictures), - new ResizePictureItems( - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - playListConfiguration.Routes.LargePictures) - ] - ), cancellationToken); + playList.OriginalPictureKeyName = pictureName; + + try + { + await publishEndpoint.Publish(new ResizePictureEvent( + storageConfiguration.BucketName, + newImageStorageKey, + [ + new ResizePictureItems( + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + playListConfiguration.Routes.SmallPictures), + new ResizePictureItems( + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + playListConfiguration.Routes.MediumPictures), + new ResizePictureItems( + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight, + playListConfiguration.Routes.LargePictures) + ] + ), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish resize picture event for PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); + } database.PlayLists.Update(playList); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKeyName}.", playList.Id, imageKeyName); + logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKeyName}.", playList.Id, newImageStorageKey); return Result.Success(PlayListResponse.FromEntity(playList)); } } diff --git a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs index b57ebbc..031af87 100644 --- a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs @@ -5,5 +5,5 @@ using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Queries.GetPlayListById { public record GetPlayListByIdQuery(Guid Id) - : IRequest>; + : IRequest>>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs index c01cceb..ba213f7 100644 --- a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs +++ b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs @@ -5,5 +5,5 @@ using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Queries.GetPlayLists { public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10) - : IRequest>; + : IRequest>>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs index 0479ccb..efeb755 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs @@ -9,5 +9,5 @@ namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId string Name = "", int PageNumber = 1, int PageSize = 10) - : IRequest>; + : IRequest>>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs index 328895f..58403b4 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs @@ -3,7 +3,6 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Contracts; -using Musify.Domain.Entities; using X.PagedList.EF; namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId @@ -15,7 +14,7 @@ namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var normalizedName = request.Name.Trim().ToUpperInvariant(); + var normalizedName = request.Name?.Trim().ToUpperInvariant() ?? string.Empty; var playListsQuery = database.PlayLists .AsNoTracking() diff --git a/Application/Users/Commands/CreateUser/CreateUserCommand.cs b/Application/Users/Commands/CreateUser/CreateUserCommand.cs index 519e26f..ebb068b 100644 --- a/Application/Users/Commands/CreateUser/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUser/CreateUserCommand.cs @@ -5,5 +5,5 @@ using Musify.Application.Users.Contracts; namespace Musify.Application.Users.Commands.CreateUser { public record CreateUserCommand(Guid Id) - : IRequest>; + : IRequest>>; } \ No newline at end of file diff --git a/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs b/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs index 571ce4d..720fc53 100644 --- a/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs +++ b/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs @@ -4,5 +4,5 @@ using DispatchR.Abstractions.Send; namespace Musify.Application.Users.Commands.DeleteUser { public record DeleteUserCommand(Guid Id) - : IRequest; + : IRequest>; } \ No newline at end of file diff --git a/Infrastructure/Configuration/StorageClientConfiguration.cs b/Infrastructure/Configuration/StorageClientConfiguration.cs index 92e3caa..7e7fc62 100644 --- a/Infrastructure/Configuration/StorageClientConfiguration.cs +++ b/Infrastructure/Configuration/StorageClientConfiguration.cs @@ -7,18 +7,19 @@ namespace Musify.Infrastructure.Configuration public const string SectionName = "Storage"; [Required] + [Url] public required string ServiceUrl { get; set; } [Required] + [MinLength(3)] public required string AccessKey { get; set; } [Required] + [MinLength(8)] public required string SecretAccessKey { get; set; } - [Required] public bool ForcePathStyle { get; set; } - [Required] public bool UseHttp { get; set; } } } diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs index f6c71bd..d0f46ad 100644 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -22,7 +22,6 @@ namespace Musify.Infrastructure.Storage } ); serviceDescriptors.AddScoped(); - return serviceDescriptors; } } From 93c7c372830b5a7fdafa424b3208ac3ad0876536 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:36:22 +0100 Subject: [PATCH 031/172] Missing if --- Infrastructure/Storage/StorageHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index c324220..a7b9e28 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -5,10 +5,11 @@ using Ardalis.Result; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; +using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Storage { - public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, ILogger logger) + public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, StorageClientConfiguration storageClientConfiguration, ILogger logger) : IStorageHandler { public async Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) @@ -42,7 +43,7 @@ namespace Musify.Infrastructure.Storage BucketName = bucketName, Key = keyName, Expires = DateTime.UtcNow + expirationTime, - Protocol = Protocol.HTTP + Protocol = storageClientConfiguration.UseHttp ? Protocol.HTTP : Protocol.HTTPS }; logger.LogDebug("Getting pre-signed URL from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); From 5775fcf908e8deb543f2a536e79e4dbcfcb56de6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:37:34 +0100 Subject: [PATCH 032/172] States --- Domain/Entities/Job.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs index 6c20331..4b2f8c9 100644 --- a/Domain/Entities/Job.cs +++ b/Domain/Entities/Job.cs @@ -15,6 +15,12 @@ namespace Musify.Domain.Entities [Required] public JobState JobState { get; set; } = JobState.Pending; + public bool IsCompleted => JobState == JobState.Completed; + + public bool IsFailed => JobState == JobState.Failed; + + public bool IsPending => JobState == JobState.Pending; + [Required] public required string Payload { get; set; } From 7a6819d9233dad71da3d571e2be7f75813d0332f Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:37:51 +0100 Subject: [PATCH 033/172] [NotMapped] --- Domain/Entities/Job.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs index 4b2f8c9..4560443 100644 --- a/Domain/Entities/Job.cs +++ b/Domain/Entities/Job.cs @@ -15,10 +15,13 @@ namespace Musify.Domain.Entities [Required] public JobState JobState { get; set; } = JobState.Pending; + [NotMapped] public bool IsCompleted => JobState == JobState.Completed; + [NotMapped] public bool IsFailed => JobState == JobState.Failed; + [NotMapped] public bool IsPending => JobState == JobState.Pending; [Required] From d7a24d2704b732b4301e3d487d80dab075d6e9c5 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:58:45 +0100 Subject: [PATCH 034/172] JobExecution --- .../Contracts/Infrastructure/IDatabase.cs | 2 + Application/Events/RemoveFileEvent.cs | 3 +- Application/Events/ResizePictureEvent.cs | 3 +- .../CreatePlayListCommandHandler.cs | 3 +- .../DeletePlayListCommandHandler.cs | 12 ++++-- .../UpdatePlayListCommandHandler.cs | 15 +++++--- Domain/Entities/Job.cs | 17 --------- Domain/Entities/JobExecution.cs | 37 +++++++++++++++++++ .../Messaging/Consumers/RemoveFileConsumer.cs | 23 ++++++------ .../Consumers/ResizePictureConsumer.cs | 37 ++++++++++--------- Infrastructure/Persistence/Database.cs | 2 + 11 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 Domain/Entities/JobExecution.cs diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 69ea4eb..16534e2 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -17,6 +17,8 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Jobs { get; } + DbSet JobExecutions { get; } + Task SaveChangesAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs index 76fc108..ff89abb 100644 --- a/Application/Events/RemoveFileEvent.cs +++ b/Application/Events/RemoveFileEvent.cs @@ -2,5 +2,6 @@ { public record RemoveFileEvent( string BucketName, - string KeyName); + string KeyName, + Guid JobId); } \ No newline at end of file diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs index 00ccfce..df4f3d9 100644 --- a/Application/Events/ResizePictureEvent.cs +++ b/Application/Events/ResizePictureEvent.cs @@ -3,5 +3,6 @@ public record ResizePictureEvent( string BucketName, string KeyName, - ResizePictureItems[] Items); + ResizePictureItems[] Items, + Guid JobId); } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index db4f02e..f57b028 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -77,7 +77,8 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ] ), cancellationToken); + ], + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index a92e092..d980748 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -37,7 +37,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - originalImageStorageKey), cancellationToken); + originalImageStorageKey, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -51,7 +52,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + playList.SmallPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -66,7 +68,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + playList.MediumPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -81,7 +84,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + playList.LargePictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 0092d1f..52e4550 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -55,7 +55,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - originalImageStorageKey), cancellationToken); + originalImageStorageKey, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -72,7 +73,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + playList.SmallPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -87,7 +89,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + playList.MediumPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -102,7 +105,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + playList.LargePictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -150,7 +154,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ] + ], + Guid.NewGuid() ), cancellationToken); } catch (Exception exception) diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs index 4560443..a766585 100644 --- a/Domain/Entities/Job.cs +++ b/Domain/Entities/Job.cs @@ -12,27 +12,10 @@ namespace Musify.Domain.Entities [Required] public required JobType JobType { get; set; } - [Required] - public JobState JobState { get; set; } = JobState.Pending; - - [NotMapped] - public bool IsCompleted => JobState == JobState.Completed; - - [NotMapped] - public bool IsFailed => JobState == JobState.Failed; - - [NotMapped] - public bool IsPending => JobState == 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/JobExecution.cs b/Domain/Entities/JobExecution.cs new file mode 100644 index 0000000..ad20805 --- /dev/null +++ b/Domain/Entities/JobExecution.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("JobExecution")] + public class JobExecution + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required Guid JobId { get; set; } + + [Required] + public JobState JobState { get; set; } = JobState.Pending; + + [NotMapped] + public bool IsCompleted => JobState == JobState.Completed; + + [NotMapped] + public bool IsFailed => JobState == JobState.Failed; + + [NotMapped] + public bool IsPending => JobState == JobState.Pending; + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime FinishedAt { get; set; } + + [ForeignKey(nameof(JobId)) ] + public Job Job { get; set; } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 3c86b06..8d42353 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Messaging.Consumers var payload = JsonSerializer.Serialize(consumeContext.Message); var job = await database.Jobs - .SingleOrDefaultAsync(j => j.JobState == JobState.Failed && j.Payload == payload); + .SingleOrDefaultAsync(j => j.Id == consumeContext.Message.JobId); if (job is null) { @@ -30,13 +30,12 @@ namespace Musify.Infrastructure.Messaging.Consumers await database.Jobs.AddAsync(job, consumeContext.CancellationToken); logger.LogInformation("Created new job with id {JobId} for removing file {BucketName}/{KeyName}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName); } - else + + var jobExecution = new JobExecution { - job.RetryCount++; - job.JobState = JobState.Pending; - database.Jobs.Update(job); - logger.LogInformation("Retrying job with id {JobId} for removing file {BucketName}/{KeyName}. Retry count: {RetryCount}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.RetryCount); - } + JobId = job.Id + }; + await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); var removeFile = await storageHandler.RemoveFileAsync( consumeContext.Message.BucketName, @@ -45,15 +44,17 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!removeFile.IsSuccess) { - job.JobState = JobState.Failed; - logger.LogInformation("Failed to remove file {BucketName}/{KeyName} for job with id {JobId}. Error: {ErrorMessage}", consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, string.Join("; ", removeFile.Errors)); + jobExecution.JobState = JobState.Failed; + logger.LogInformation("Failed to remove file {BucketName}/{KeyName} for jobId={JobId}, JobExecutionId={jobExecutionId}. Error: {ErrorMessage}", + consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, jobExecution.Id, string.Join("; ", removeFile.Errors)); } else { - job.CompletedAt = DateTime.UtcNow; - logger.LogInformation("Successfully removed file {BucketName}/{KeyName} for job with id {JobId}.", consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id); + logger.LogInformation("Successfully removed file {BucketName}/{KeyName} for jobId={JobId}, JobExecutionId={jobExecutionId}.", + consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, jobExecution.Id); } + jobExecution.FinishedAt = DateTime.UtcNow; await database.SaveChangesAsync(consumeContext.CancellationToken); } } diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index 3c5ed0f..bdbb334 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -18,14 +18,14 @@ namespace Musify.Infrastructure.Messaging.Consumers { try { - Dictionary jobs = new Dictionary(); + Dictionary jobsExecutions = 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); + .SingleOrDefaultAsync(job => job.Id == consumeContext.Message.JobId); if (job is null) { @@ -36,14 +36,14 @@ namespace Musify.Infrastructure.Messaging.Consumers }; await database.Jobs.AddAsync(job, consumeContext.CancellationToken); } - else - { - job.RetryCount++; - job.JobState = JobState.InProgress; - database.Jobs.Update(job); - } - jobs.Add(item, job); + var jobExecution = new JobExecution + { + JobId = consumeContext.Message.JobId + }; + await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); + + jobsExecutions.Add(item, jobExecution); } var fileResult = await storageHandler.GetFileAsync( @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.Messaging.Consumers logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", consumeContext.Message.BucketName, consumeContext.Message.KeyName); - foreach (var job in jobs) + foreach (var job in jobsExecutions) { job.Value.JobState = JobState.Failed; } @@ -70,12 +70,12 @@ namespace Musify.Infrastructure.Messaging.Consumers foreach (var item in consumeContext.Message.Items) { - var job = jobs[item]; + var jobExecution = jobsExecutions[item]; try { await ResizePicture( - job, + jobExecution, consumeContext.Message.BucketName, memoryStream, item, @@ -83,7 +83,7 @@ namespace Musify.Infrastructure.Messaging.Consumers } catch (Exception exception) { - logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", job.Id, item); + logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", jobExecution.Id, item); continue; } } @@ -100,10 +100,10 @@ namespace Musify.Infrastructure.Messaging.Consumers } } - async Task ResizePicture(Job job, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken) + async Task ResizePicture(JobExecution jobExecution, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken) { memoryStream.Position = 0; - job.JobState = JobState.InProgress; + jobExecution.JobState = JobState.InProgress; var pictureResult = await pictureHandler.ResizePictureAsync( memoryStream, @@ -114,7 +114,7 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!pictureResult.IsSuccess) { logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); - job.JobState = JobState.Failed; + jobExecution.JobState = JobState.Failed; return; } @@ -128,11 +128,12 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!saveResult.IsSuccess) { logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); - job.JobState = JobState.Failed; + jobExecution.JobState = JobState.Failed; return; } - job.JobState = JobState.Completed; + jobExecution.JobState = JobState.Completed; + jobExecution.FinishedAt = DateTime.UtcNow; } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 5148f5a..be2c9d1 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -24,5 +24,7 @@ namespace Musify.Infrastructure.Persistence public DbSet Uploads => Set(); public DbSet Jobs => Set(); + + public DbSet JobExecutions => Set(); } } \ No newline at end of file From 9800fb29bf371d91ce95b43177a1dfdfc44916d4 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:59:48 +0100 Subject: [PATCH 035/172] Spaces --- Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 8d42353..ed0741a 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -20,8 +20,8 @@ namespace Musify.Infrastructure.Messaging.Consumers var job = await database.Jobs .SingleOrDefaultAsync(j => j.Id == consumeContext.Message.JobId); - if (job is null) - { + if (job is null) + { job = new Job { JobType = JobType.RemoveFile, From e0a48da5b5aede4c991104607ab8aea38b632803 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 09:32:42 +0100 Subject: [PATCH 036/172] MaxLength --- Domain/Entities/Job.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs index a766585..45f07dc 100644 --- a/Domain/Entities/Job.cs +++ b/Domain/Entities/Job.cs @@ -13,6 +13,7 @@ namespace Musify.Domain.Entities public required JobType JobType { get; set; } [Required] + [MaxLength(512)] public required string Payload { get; set; } [Required] From 2720ea30dffcff6e3aea2233966506ade05b9cab Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 09:41:43 +0100 Subject: [PATCH 037/172] Rename --- .../{IKeycloakUserClient.cs => IKeycloakUserService.cs} | 2 +- .../Users/Commands/CreateUser/CreateUserCommandHandler.cs | 2 +- .../Users/Queries/GetUserById/GetUserByIdQueryHandler.cs | 2 +- .../GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs | 2 +- Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs | 2 +- Infrastructure/Identity/KeycloakDependencyInjection.cs | 4 ++-- .../{KeycloakIdentityService.cs => KeycloakUserService.cs} | 4 ++-- Infrastructure/InfrastructureDependencyInjection.cs | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) rename Application/Contracts/Infrastructure/{IKeycloakUserClient.cs => IKeycloakUserService.cs} (93%) rename Infrastructure/Identity/{KeycloakIdentityService.cs => KeycloakUserService.cs} (95%) diff --git a/Application/Contracts/Infrastructure/IKeycloakUserClient.cs b/Application/Contracts/Infrastructure/IKeycloakUserService.cs similarity index 93% rename from Application/Contracts/Infrastructure/IKeycloakUserClient.cs rename to Application/Contracts/Infrastructure/IKeycloakUserService.cs index 688ef1d..055f7f4 100644 --- a/Application/Contracts/Infrastructure/IKeycloakUserClient.cs +++ b/Application/Contracts/Infrastructure/IKeycloakUserService.cs @@ -3,7 +3,7 @@ using Musify.Application.Users.Contracts; namespace Musify.Application.Contracts.Infrastructure { - public interface IKeycloakUserClient + public interface IKeycloakUserService { Task> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken); diff --git a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs b/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs index 2e72d70..add9aef 100644 --- a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs +++ b/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs @@ -8,7 +8,7 @@ using Musify.Domain.Entities; namespace Musify.Application.Users.Commands.CreateUser { - public class CreateUserCommandHandler(IDatabase database, IKeycloakUserClient keycloakUserClient, ILogger logger) + public class CreateUserCommandHandler(IDatabase database, IKeycloakUserService keycloakUserClient, ILogger logger) : IRequestHandler >> { public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) diff --git a/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs b/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs index b6550c0..f57b93b 100644 --- a/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs +++ b/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs @@ -6,7 +6,7 @@ using Musify.Application.Users.Contracts; namespace Musify.Application.Users.Queries.GetUserById { - public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) + public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) : IRequestHandler>> { public async Task> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs index bca8ee5..2f7c897 100644 --- a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs @@ -6,7 +6,7 @@ using Musify.Application.Users.Contracts; namespace Musify.Application.Users.Queries.GetUserByKeycloakId { - public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) + public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) : IRequestHandler>> { public async Task> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) diff --git a/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs b/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs index dc4d704..9e7ecf3 100644 --- a/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs +++ b/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs @@ -8,7 +8,7 @@ using X.PagedList.Extensions; namespace Musify.Application.Users.Queries.GetUsers { - public class GetUsersQueryHandler(IDatabase database, IKeycloakUserClient keycloakUserClient) + public class GetUsersQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) : IRequestHandler >> { public async Task> Handle(GetUsersQuery request, CancellationToken cancellationToken) diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs index 6e5accb..ff03f43 100644 --- a/Infrastructure/Identity/KeycloakDependencyInjection.cs +++ b/Infrastructure/Identity/KeycloakDependencyInjection.cs @@ -8,7 +8,7 @@ namespace Musify.Infrastructure.Identity { public static class KeycloakDependencyInjection { - public static IServiceCollection AddKeycloakAuthentication(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddKeycloakService(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors.AddOptionsWithValidateOnStart() .Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName)) @@ -23,7 +23,7 @@ namespace Musify.Infrastructure.Identity keycloakConfiguration.Username, keycloakConfiguration.Password); } ); - serviceDescriptors.AddScoped(); + serviceDescriptors.AddScoped(); return serviceDescriptors; } } diff --git a/Infrastructure/Identity/KeycloakIdentityService.cs b/Infrastructure/Identity/KeycloakUserService.cs similarity index 95% rename from Infrastructure/Identity/KeycloakIdentityService.cs rename to Infrastructure/Identity/KeycloakUserService.cs index a2d1d8f..175fc3e 100644 --- a/Infrastructure/Identity/KeycloakIdentityService.cs +++ b/Infrastructure/Identity/KeycloakUserService.cs @@ -8,10 +8,10 @@ using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Identity { - public class KeycloakIdentityService( + public class KeycloakUserService( KeycloakClient keycloakClient, KeycloakConfiguration keycloakConfiguration, - ILogger logger) : IKeycloakUserClient + ILogger logger) : IKeycloakUserService { public async Task> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken) { diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 3d52f0f..8e5e072 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -17,7 +17,7 @@ namespace Musify.Infrastructure serviceDescriptors .AddInfrastructureConfigurations(configuration) .AddDatabase(configuration) - .AddKeycloakAuthentication(configuration) + .AddKeycloakClient(configuration) .AddStorageHandler() .AddPictureHandler() .AddMassTransitClient(configuration) From 1e1ea989e9c116cacade9f1ab773712c30f8b20c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 09:42:05 +0100 Subject: [PATCH 038/172] Keycloak service --- Infrastructure/InfrastructureDependencyInjection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 8e5e072..5eddee6 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -17,7 +17,7 @@ namespace Musify.Infrastructure serviceDescriptors .AddInfrastructureConfigurations(configuration) .AddDatabase(configuration) - .AddKeycloakClient(configuration) + .AddKeycloakService(configuration) .AddStorageHandler() .AddPictureHandler() .AddMassTransitClient(configuration) From 7ce9a8371cd341b85c680b4c393ff6f6f709fd00 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 09:53:54 +0100 Subject: [PATCH 039/172] IEventBus --- .../CreatePlayList/CreatePlayListCommandHandler.cs | 4 ++-- .../DeletePlayList/DeletePlayListCommandHandler.cs | 10 +++++----- .../UpdatePlayList/UpdatePlayListCommandHandler.cs | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index f57b028..793fd87 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -11,7 +11,7 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands.CreatePlayList { - public class CreatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler, + public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, ILogger logger, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { @@ -61,7 +61,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList try { - await publishEndpoint.Publish(new ResizePictureEvent( + await eventBus.PublishAsync(new ResizePictureEvent( storageConfiguration.BucketName, originalImageStorageKey, [ diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index d980748..674a0fc 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -9,7 +9,7 @@ using Musify.Application.PlayLists; namespace Musify.Application.PlayLists.Commands.DeletePlayList { - public class DeletePlayListCommandHandler(IDatabase database, IPublishEndpoint publishEndpoint, + public class DeletePlayListCommandHandler(IDatabase database, IEventBus eventBus, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) : IRequestHandler> { @@ -35,7 +35,7 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, originalImageStorageKey, Guid.NewGuid()), cancellationToken); @@ -50,7 +50,7 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, playList.SmallPictureKeyName, Guid.NewGuid()), cancellationToken); @@ -66,7 +66,7 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, playList.MediumPictureKeyName, Guid.NewGuid()), cancellationToken); @@ -82,7 +82,7 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, playList.LargePictureKeyName, Guid.NewGuid()), cancellationToken); diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 52e4550..8d82457 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -10,7 +10,7 @@ using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Commands.UpdatePlayList { - public class UpdatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler, + public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, ILogger logger, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { @@ -53,7 +53,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, originalImageStorageKey, Guid.NewGuid()), cancellationToken); @@ -71,7 +71,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, playList.SmallPictureKeyName, Guid.NewGuid()), cancellationToken); @@ -87,7 +87,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, playList.MediumPictureKeyName, Guid.NewGuid()), cancellationToken); @@ -103,7 +103,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { try { - await publishEndpoint.Publish(new RemoveFileEvent( + await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, playList.LargePictureKeyName, Guid.NewGuid()), cancellationToken); @@ -138,7 +138,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { - await publishEndpoint.Publish(new ResizePictureEvent( + await eventBus.PublishAsync(new ResizePictureEvent( storageConfiguration.BucketName, newImageStorageKey, [ From 61709736bcb4baae813db9a00d4db2bbe069ba17 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 10:43:52 +0100 Subject: [PATCH 040/172] Fix job --- .../CreatePlayListCommandHandler.cs | 1 - .../Consumers/ResizePictureConsumer.cs | 35 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 793fd87..5ba20da 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -1,6 +1,5 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index bdbb334..d164b55 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -19,30 +19,29 @@ namespace Musify.Infrastructure.Messaging.Consumers try { Dictionary jobsExecutions = new Dictionary(); + var job = await database.Jobs + .SingleOrDefaultAsync(job => job.Id == consumeContext.Message.JobId); + + if (job is null) + { + var payloadJson = JsonSerializer.Serialize(consumeContext.Message.Items); + job = new Job + { + Id = consumeContext.Message.JobId, + JobType = JobType.ResizePicture, + Payload = payloadJson, + }; + await database.Jobs.AddAsync(job, consumeContext.CancellationToken); + await database.SaveChangesAsync(consumeContext.CancellationToken); + } foreach (var item in consumeContext.Message.Items) { - var payloadJson = JsonSerializer.Serialize(item); - - var job = await database.Jobs - .SingleOrDefaultAsync(job => job.Id == consumeContext.Message.JobId); - - if (job is null) - { - job = new Job - { - JobType = JobType.ResizePicture, - Payload = payloadJson, - }; - await database.Jobs.AddAsync(job, consumeContext.CancellationToken); - } - var jobExecution = new JobExecution { JobId = consumeContext.Message.JobId }; await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); - jobsExecutions.Add(item, jobExecution); } @@ -56,9 +55,9 @@ namespace Musify.Infrastructure.Messaging.Consumers logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", consumeContext.Message.BucketName, consumeContext.Message.KeyName); - foreach (var job in jobsExecutions) + foreach (var jobExecution in jobsExecutions.Values) { - job.Value.JobState = JobState.Failed; + jobExecution.JobState = JobState.Failed; } await database.SaveChangesAsync(consumeContext.CancellationToken); return; From a10c8238f83550688c645593c34d02fcde11f923 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 11:11:45 +0100 Subject: [PATCH 041/172] Refactor --- .../Consumers/ResizePictureConsumer.cs | 247 +++++++++++------- 1 file changed, 157 insertions(+), 90 deletions(-) diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index d164b55..da3f850 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -12,126 +12,193 @@ namespace Musify.Infrastructure.Messaging.Consumers public class ResizePictureConsumer(IStorageHandler storageHandler, IPictureHandler pictureHandler, IDatabase database, ILogger logger) : IConsumer { + private record JobExecutionItem(ResizePictureItems Item, JobExecution JobExecution); + public const string QueueName = "picture-resize-queue"; public async Task Consume(ConsumeContext consumeContext) { try { - Dictionary jobsExecutions = new Dictionary(); - var job = await database.Jobs - .SingleOrDefaultAsync(job => job.Id == consumeContext.Message.JobId); + var message = consumeContext.Message; + var cancellationToken = consumeContext.CancellationToken; - if (job is null) + var job = await GetOrCreateJobAsync(message, cancellationToken); + var jobExecutions = await CreateJobExecutionsAsync(message.Items, job.Id, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + + var sourceImageResult = await GetSourceImageAsync(message.BucketName, message.KeyName, cancellationToken); + if (!sourceImageResult.IsSuccess) { - var payloadJson = JsonSerializer.Serialize(consumeContext.Message.Items); - job = new Job - { - Id = consumeContext.Message.JobId, - JobType = JobType.ResizePicture, - Payload = payloadJson, - }; - await database.Jobs.AddAsync(job, consumeContext.CancellationToken); - await database.SaveChangesAsync(consumeContext.CancellationToken); - } - - foreach (var item in consumeContext.Message.Items) - { - var jobExecution = new JobExecution - { - JobId = consumeContext.Message.JobId - }; - await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); - jobsExecutions.Add(item, jobExecution); - } - - 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 jobExecution in jobsExecutions.Values) - { - jobExecution.JobState = JobState.Failed; - } - await database.SaveChangesAsync(consumeContext.CancellationToken); + MarkAllAsFailed(jobExecutions); + await database.SaveChangesAsync(cancellationToken); return; } - using var fileStream = fileResult.Value; - using var memoryStream = new MemoryStream(); - await fileStream.CopyToAsync(memoryStream, consumeContext.CancellationToken); + using var sourceImage = sourceImageResult.Value; - foreach (var item in consumeContext.Message.Items) + foreach (var executionItem in jobExecutions) { - var jobExecution = jobsExecutions[item]; + await ProcessExecutionAsync( + executionItem, + message.BucketName, + sourceImage, + cancellationToken); - try - { - await ResizePicture( - jobExecution, - 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}", jobExecution.Id, item); - continue; - } + await database.SaveChangesAsync(cancellationToken); } } catch (Exception exception) { - logger.LogError(exception, "An error occurred while consuming the ImageResizedConsumer message."); + logger.LogError(exception, "An error occurred while consuming the ResizePictureConsumer message."); throw; } - finally - { - logger.LogInformation("Saving changes to database after processing ImageResizedConsumer message."); - await database.SaveChangesAsync(consumeContext.CancellationToken); - } } - async Task ResizePicture(JobExecution jobExecution, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken) + private async Task GetOrCreateJobAsync(ResizePictureEvent message, CancellationToken cancellationToken) { + var job = await database.Jobs + .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); + + if (job is not null) + { + return job; + } + + var payloadJson = JsonSerializer.Serialize(message.Items); + job = new Job + { + Id = message.JobId, + JobType = JobType.ResizePicture, + Payload = payloadJson + }; + + await database.Jobs.AddAsync(job, cancellationToken); + return job; + } + + private async Task> CreateJobExecutionsAsync( + IReadOnlyCollection items, + Guid jobId, + CancellationToken cancellationToken) + { + var jobExecutions = new List(items.Count); + + foreach (var item in items) + { + var jobExecution = new JobExecution + { + JobId = jobId + }; + + await database.JobExecutions.AddAsync(jobExecution, cancellationToken); + jobExecutions.Add(new JobExecutionItem(item, jobExecution)); + } + + return jobExecutions; + } + + private async Task> GetSourceImageAsync(string bucketName, string keyName, CancellationToken cancellationToken) + { + var fileResult = await storageHandler.GetFileAsync(bucketName, keyName, cancellationToken); + + if (fileResult.IsNotFound()) + { + logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", bucketName, keyName); + return Result.NotFound(); + } + + if (!fileResult.IsSuccess) + { + logger.LogError("Failed to read source image from storage: BucketName={BucketName}, KeyName={KeyName}. Errors: {Errors}", + bucketName, + keyName, + string.Join("; ", fileResult.Errors)); + return Result.Error(string.Join("; ", fileResult.Errors)); + } + + using var fileStream = fileResult.Value; + var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); memoryStream.Position = 0; - jobExecution.JobState = JobState.InProgress; - var pictureResult = await pictureHandler.ResizePictureAsync( - memoryStream, - items.Width, - items.Height, - cancellationToken); + return Result.Success(memoryStream); + } - if (!pictureResult.IsSuccess) + private async Task ProcessExecutionAsync( + JobExecutionItem executionItem, + string bucketName, + MemoryStream sourceImage, + CancellationToken cancellationToken) + { + var jobExecution = executionItem.JobExecution; + + try { - logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); - jobExecution.JobState = JobState.Failed; - return; + jobExecution.JobState = JobState.InProgress; + sourceImage.Position = 0; + + var resizedPictureResult = await pictureHandler.ResizePictureAsync( + sourceImage, + executionItem.Item.Width, + executionItem.Item.Height, + cancellationToken); + + if (!resizedPictureResult.IsSuccess) + { + logger.LogError("Failed to resize picture for JobExecutionId={JobExecutionId}. Error: {ErrorMessage}", + jobExecution.Id, + string.Join("; ", resizedPictureResult.Errors)); + + MarkAsFailed(jobExecution); + return; + } + + var saveResult = await storageHandler.UploadFileAsync( + resizedPictureResult.Value, + "image/webp", + bucketName, + $"{executionItem.Item.SaveOnRoute}/{Guid.NewGuid()}.webp", + cancellationToken); + + if (!saveResult.IsSuccess) + { + logger.LogError("Failed to upload resized picture for JobExecutionId={JobExecutionId}. Error: {ErrorMessage}", + jobExecution.Id, + string.Join("; ", saveResult.Errors)); + + MarkAsFailed(jobExecution); + return; + } + + jobExecution.JobState = JobState.Completed; + jobExecution.FinishedAt = DateTime.UtcNow; } - - var saveResult = await storageHandler.UploadFileAsync( - pictureResult.Value, - "image/webp", - bucketName, - $"{items.SaveOnRoute}/{Guid.NewGuid() }.webp", - cancellationToken); - - if (!saveResult.IsSuccess) + catch (Exception exception) { - logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); - jobExecution.JobState = JobState.Failed; - return; - } + logger.LogError(exception, + "An error occurred while processing resize for JobExecutionId={JobExecutionId}, Width={Width}, Height={Height}, SaveOnRoute={SaveOnRoute}", + jobExecution.Id, + executionItem.Item.Width, + executionItem.Item.Height, + executionItem.Item.SaveOnRoute); - jobExecution.JobState = JobState.Completed; + MarkAsFailed(jobExecution); + } + } + + private static void MarkAllAsFailed(IEnumerable jobExecutions) + { + foreach (var executionItem in jobExecutions) + { + MarkAsFailed(executionItem.JobExecution); + } + } + + private static void MarkAsFailed(JobExecution jobExecution) + { + jobExecution.JobState = JobState.Failed; jobExecution.FinishedAt = DateTime.UtcNow; } } From e02e1c5bc38440c14e9991e4b1e5113732deb62c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 11:57:16 +0100 Subject: [PATCH 042/172] Rename --- .../Contracts/Infrastructure/IDatabase.cs | 2 +- .../{JobExecution.cs => JobOperation.cs} | 2 +- .../Messaging/Consumers/RemoveFileConsumer.cs | 4 +- .../Consumers/ResizePictureConsumer.cs | 82 +++++++++---------- Infrastructure/Persistence/Database.cs | 2 +- 5 files changed, 46 insertions(+), 46 deletions(-) rename Domain/Entities/{JobExecution.cs => JobOperation.cs} (96%) diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 16534e2..5b3cdf2 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -17,7 +17,7 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Jobs { get; } - DbSet JobExecutions { get; } + DbSet JobOperations { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/Domain/Entities/JobExecution.cs b/Domain/Entities/JobOperation.cs similarity index 96% rename from Domain/Entities/JobExecution.cs rename to Domain/Entities/JobOperation.cs index ad20805..096ea08 100644 --- a/Domain/Entities/JobExecution.cs +++ b/Domain/Entities/JobOperation.cs @@ -5,7 +5,7 @@ namespace Musify.Domain.Entities { #pragma warning disable CS8618 [Table("JobExecution")] - public class JobExecution + public class JobOperation { [Key] public Guid Id { get; set; } = Guid.NewGuid(); diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index ed0741a..1f71d37 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -31,11 +31,11 @@ namespace Musify.Infrastructure.Messaging.Consumers logger.LogInformation("Created new job with id {JobId} for removing file {BucketName}/{KeyName}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName); } - var jobExecution = new JobExecution + var jobExecution = new JobOperation { JobId = job.Id }; - await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); + await database.JobOperations.AddAsync(jobExecution, consumeContext.CancellationToken); var removeFile = await storageHandler.RemoveFileAsync( consumeContext.Message.BucketName, diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index da3f850..f7a2619 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -12,7 +12,7 @@ namespace Musify.Infrastructure.Messaging.Consumers public class ResizePictureConsumer(IStorageHandler storageHandler, IPictureHandler pictureHandler, IDatabase database, ILogger logger) : IConsumer { - private record JobExecutionItem(ResizePictureItems Item, JobExecution JobExecution); + private record JobOperationItem(ResizePictureItems Item, JobOperation JobOperation); public const string QueueName = "picture-resize-queue"; @@ -24,24 +24,24 @@ namespace Musify.Infrastructure.Messaging.Consumers var cancellationToken = consumeContext.CancellationToken; var job = await GetOrCreateJobAsync(message, cancellationToken); - var jobExecutions = await CreateJobExecutionsAsync(message.Items, job.Id, cancellationToken); + var jobOperations = await CreateJobOperationAsync(message.Items, job.Id, cancellationToken); await database.SaveChangesAsync(cancellationToken); var sourceImageResult = await GetSourceImageAsync(message.BucketName, message.KeyName, cancellationToken); if (!sourceImageResult.IsSuccess) { - MarkAllAsFailed(jobExecutions); + MarkAllAsFailed(jobOperations); await database.SaveChangesAsync(cancellationToken); return; } using var sourceImage = sourceImageResult.Value; - foreach (var executionItem in jobExecutions) + foreach (var operationItem in jobOperations) { - await ProcessExecutionAsync( - executionItem, + await ProcessOperationAsync( + operationItem, message.BucketName, sourceImage, cancellationToken); @@ -78,25 +78,25 @@ namespace Musify.Infrastructure.Messaging.Consumers return job; } - private async Task> CreateJobExecutionsAsync( - IReadOnlyCollection items, + private async Task> CreateJobOperationAsync( + IReadOnlyCollection resizeItems, Guid jobId, CancellationToken cancellationToken) { - var jobExecutions = new List(items.Count); + var jobOperations = new List(resizeItems.Count); - foreach (var item in items) + foreach (var resizeItem in resizeItems) { - var jobExecution = new JobExecution + var jobOperation = new JobOperation { JobId = jobId }; - await database.JobExecutions.AddAsync(jobExecution, cancellationToken); - jobExecutions.Add(new JobExecutionItem(item, jobExecution)); + await database.JobOperations.AddAsync(jobOperation, cancellationToken); + jobOperations.Add(new JobOperationItem(resizeItem, jobOperation)); } - return jobExecutions; + return jobOperations; } private async Task> GetSourceImageAsync(string bucketName, string keyName, CancellationToken cancellationToken) @@ -126,32 +126,32 @@ namespace Musify.Infrastructure.Messaging.Consumers return Result.Success(memoryStream); } - private async Task ProcessExecutionAsync( - JobExecutionItem executionItem, + private async Task ProcessOperationAsync( + JobOperationItem operationItem, string bucketName, MemoryStream sourceImage, CancellationToken cancellationToken) { - var jobExecution = executionItem.JobExecution; + var jobOperation = operationItem.JobOperation; try { - jobExecution.JobState = JobState.InProgress; + jobOperation.JobState = JobState.InProgress; sourceImage.Position = 0; var resizedPictureResult = await pictureHandler.ResizePictureAsync( sourceImage, - executionItem.Item.Width, - executionItem.Item.Height, + operationItem.Item.Width, + operationItem.Item.Height, cancellationToken); if (!resizedPictureResult.IsSuccess) { - logger.LogError("Failed to resize picture for JobExecutionId={JobExecutionId}. Error: {ErrorMessage}", - jobExecution.Id, + logger.LogError("Failed to resize picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", + jobOperation.Id, string.Join("; ", resizedPictureResult.Errors)); - MarkAsFailed(jobExecution); + MarkAsFailed(jobOperation); return; } @@ -159,47 +159,47 @@ namespace Musify.Infrastructure.Messaging.Consumers resizedPictureResult.Value, "image/webp", bucketName, - $"{executionItem.Item.SaveOnRoute}/{Guid.NewGuid()}.webp", + $"{operationItem.Item.SaveOnRoute}/{Guid.NewGuid()}.webp", cancellationToken); if (!saveResult.IsSuccess) { - logger.LogError("Failed to upload resized picture for JobExecutionId={JobExecutionId}. Error: {ErrorMessage}", - jobExecution.Id, + logger.LogError("Failed to upload resized picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", + jobOperation.Id, string.Join("; ", saveResult.Errors)); - MarkAsFailed(jobExecution); + MarkAsFailed(jobOperation); return; } - jobExecution.JobState = JobState.Completed; - jobExecution.FinishedAt = DateTime.UtcNow; + jobOperation.JobState = JobState.Completed; + jobOperation.FinishedAt = DateTime.UtcNow; } catch (Exception exception) { logger.LogError(exception, - "An error occurred while processing resize for JobExecutionId={JobExecutionId}, Width={Width}, Height={Height}, SaveOnRoute={SaveOnRoute}", - jobExecution.Id, - executionItem.Item.Width, - executionItem.Item.Height, - executionItem.Item.SaveOnRoute); + "An error occurred while processing resize for JobOperationId={JobOperationId}, Width={Width}, Height={Height}, SaveOnRoute={SaveOnRoute}", + jobOperation.Id, + operationItem.Item.Width, + operationItem.Item.Height, + operationItem.Item.SaveOnRoute); - MarkAsFailed(jobExecution); + MarkAsFailed(jobOperation); } } - private static void MarkAllAsFailed(IEnumerable jobExecutions) + private static void MarkAllAsFailed(IEnumerable jobOperations) { - foreach (var executionItem in jobExecutions) + foreach (var operationItem in jobOperations) { - MarkAsFailed(executionItem.JobExecution); + MarkAsFailed(operationItem.JobOperation); } } - private static void MarkAsFailed(JobExecution jobExecution) + private static void MarkAsFailed(JobOperation jobOperation) { - jobExecution.JobState = JobState.Failed; - jobExecution.FinishedAt = DateTime.UtcNow; + jobOperation.JobState = JobState.Failed; + jobOperation.FinishedAt = DateTime.UtcNow; } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index be2c9d1..4dd8f61 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -25,6 +25,6 @@ namespace Musify.Infrastructure.Persistence public DbSet Jobs => Set(); - public DbSet JobExecutions => Set(); + public DbSet JobOperations => Set(); } } \ No newline at end of file From db0f11d27e95c323dbdd7d7e1dbe3f3e6aae3ddf Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 12:01:08 +0100 Subject: [PATCH 043/172] Error message --- Domain/Entities/JobOperation.cs | 4 ++++ .../Consumers/ResizePictureConsumer.cs | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Domain/Entities/JobOperation.cs b/Domain/Entities/JobOperation.cs index 096ea08..3e14c26 100644 --- a/Domain/Entities/JobOperation.cs +++ b/Domain/Entities/JobOperation.cs @@ -25,6 +25,10 @@ namespace Musify.Domain.Entities [NotMapped] public bool IsPending => JobState == JobState.Pending; + [Required] + [MaxLength(512)] + public string ErrorMessage { get; set; } = string.Empty; + [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index f7a2619..bc261f9 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -31,7 +31,7 @@ namespace Musify.Infrastructure.Messaging.Consumers var sourceImageResult = await GetSourceImageAsync(message.BucketName, message.KeyName, cancellationToken); if (!sourceImageResult.IsSuccess) { - MarkAllAsFailed(jobOperations); + MarkAllAsFailed(jobOperations, $"Failed to retrieve source image from storage, ResultState: {sourceImageResult.Status}"); await database.SaveChangesAsync(cancellationToken); return; } @@ -147,11 +147,12 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!resizedPictureResult.IsSuccess) { + var errorMessage = string.Join("; ", resizedPictureResult.Errors); logger.LogError("Failed to resize picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", jobOperation.Id, - string.Join("; ", resizedPictureResult.Errors)); + errorMessage); - MarkAsFailed(jobOperation); + MarkAsFailed(jobOperation, errorMessage); return; } @@ -164,11 +165,12 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!saveResult.IsSuccess) { + var errorMessage = string.Join("; ", saveResult.Errors); logger.LogError("Failed to upload resized picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", jobOperation.Id, - string.Join("; ", saveResult.Errors)); + errorMessage); - MarkAsFailed(jobOperation); + MarkAsFailed(jobOperation, errorMessage); return; } @@ -184,22 +186,23 @@ namespace Musify.Infrastructure.Messaging.Consumers operationItem.Item.Height, operationItem.Item.SaveOnRoute); - MarkAsFailed(jobOperation); + MarkAsFailed(jobOperation, exception.Message); } } - private static void MarkAllAsFailed(IEnumerable jobOperations) + private static void MarkAllAsFailed(IEnumerable jobOperations, string errorMessage) { foreach (var operationItem in jobOperations) { - MarkAsFailed(operationItem.JobOperation); + MarkAsFailed(operationItem.JobOperation, errorMessage); } } - private static void MarkAsFailed(JobOperation jobOperation) + private static void MarkAsFailed(JobOperation jobOperation, string errorMessage) { jobOperation.JobState = JobState.Failed; jobOperation.FinishedAt = DateTime.UtcNow; + jobOperation.ErrorMessage = errorMessage; } } } \ No newline at end of file From 79a633e5e004444462fac6f0ad3e60e65c820d9d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 12:10:54 +0100 Subject: [PATCH 044/172] Refactor --- .../Messaging/Consumers/RemoveFileConsumer.cs | 120 +++++++++++++----- .../Consumers/ResizePictureConsumer.cs | 16 +-- 2 files changed, 98 insertions(+), 38 deletions(-) diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 1f71d37..ed33dbb 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -15,47 +15,109 @@ namespace Musify.Infrastructure.Messaging.Consumers public async Task Consume(ConsumeContext consumeContext) { - var payload = JsonSerializer.Serialize(consumeContext.Message); + var message = consumeContext.Message; + var cancellationToken = consumeContext.CancellationToken; - var job = await database.Jobs - .SingleOrDefaultAsync(j => j.Id == consumeContext.Message.JobId); + var job = await GetOrCreateJobAsync(message, cancellationToken); + var jobOperation = await CreateJobOperationAsync(job.Id, cancellationToken); + await database.SaveChangesAsync(cancellationToken); - if (job is null) + try { - job = new Job + jobOperation.JobState = JobState.InProgress; + await database.SaveChangesAsync(cancellationToken); + + var removeResult = await storageHandler.RemoveFileAsync( + message.BucketName, + message.KeyName, + cancellationToken); + + if (!removeResult.IsSuccess) { - JobType = JobType.RemoveFile, - Payload = payload - }; - await database.Jobs.AddAsync(job, consumeContext.CancellationToken); - logger.LogInformation("Created new job with id {JobId} for removing file {BucketName}/{KeyName}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName); + MarkAsFailed(jobOperation, string.Join("; ", removeResult.Errors)); + + logger.LogError( + "Failed to remove file {BucketName}/{KeyName} for JobId={JobId}, JobOperationId={JobOperationId}. Error: {ErrorMessage}", + message.BucketName, + message.KeyName, + job.Id, + jobOperation.Id, + jobOperation.ErrorMessage); + + await database.SaveChangesAsync(cancellationToken); + return; + } + + jobOperation.JobState = JobState.Completed; + jobOperation.FinishedAt = DateTime.UtcNow; + + logger.LogInformation( + "Successfully removed file {BucketName}/{KeyName} for JobId={JobId}, JobOperationId={JobOperationId}.", + message.BucketName, + message.KeyName, + job.Id, + jobOperation.Id); + + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, + "An error occurred while consuming RemoveFileConsumer message for JobId={JobId}, BucketName={BucketName}, KeyName={KeyName}", + message.JobId, + message.BucketName, + message.KeyName); + MarkAsFailed(jobOperation, exception.Message); + await database.SaveChangesAsync(cancellationToken); + + throw; + } + } + + async Task GetOrCreateJobAsync(RemoveFileEvent message, CancellationToken cancellationToken) + { + var job = await database.Jobs + .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); + + if (job is not null) + { + return job; } - var jobExecution = new JobOperation + job = new Job { - JobId = job.Id + Id = message.JobId, + JobType = JobType.RemoveFile, + Payload = JsonSerializer.Serialize(message) }; - await database.JobOperations.AddAsync(jobExecution, consumeContext.CancellationToken); - var removeFile = await storageHandler.RemoveFileAsync( - consumeContext.Message.BucketName, - consumeContext.Message.KeyName, - consumeContext.CancellationToken); + await database.Jobs.AddAsync(job, cancellationToken); - if (!removeFile.IsSuccess) + logger.LogInformation( + "Created new job with id {JobId} for removing file {BucketName}/{KeyName}", + job.Id, + message.BucketName, + message.KeyName); + + return job; + } + + async Task CreateJobOperationAsync(Guid jobId, CancellationToken cancellationToken) + { + var jobOperation = new JobOperation { - jobExecution.JobState = JobState.Failed; - logger.LogInformation("Failed to remove file {BucketName}/{KeyName} for jobId={JobId}, JobExecutionId={jobExecutionId}. Error: {ErrorMessage}", - consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, jobExecution.Id, string.Join("; ", removeFile.Errors)); - } - else - { - logger.LogInformation("Successfully removed file {BucketName}/{KeyName} for jobId={JobId}, JobExecutionId={jobExecutionId}.", - consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, jobExecution.Id); - } + JobId = jobId + }; - jobExecution.FinishedAt = DateTime.UtcNow; - await database.SaveChangesAsync(consumeContext.CancellationToken); + await database.JobOperations.AddAsync(jobOperation, cancellationToken); + return jobOperation; + } + + void MarkAsFailed(JobOperation jobOperation, string errorMessage) + { + jobOperation.JobState = JobState.Failed; + jobOperation.ErrorMessage = errorMessage; + jobOperation.FinishedAt = DateTime.UtcNow; } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index bc261f9..af9a40b 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.Messaging.Consumers } } - private async Task GetOrCreateJobAsync(ResizePictureEvent message, CancellationToken cancellationToken) + async Task GetOrCreateJobAsync(ResizePictureEvent message, CancellationToken cancellationToken) { var job = await database.Jobs .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); @@ -78,9 +78,7 @@ namespace Musify.Infrastructure.Messaging.Consumers return job; } - private async Task> CreateJobOperationAsync( - IReadOnlyCollection resizeItems, - Guid jobId, + async Task> CreateJobOperationAsync(IReadOnlyCollection resizeItems, Guid jobId, CancellationToken cancellationToken) { var jobOperations = new List(resizeItems.Count); @@ -99,7 +97,7 @@ namespace Musify.Infrastructure.Messaging.Consumers return jobOperations; } - private async Task> GetSourceImageAsync(string bucketName, string keyName, CancellationToken cancellationToken) + async Task> GetSourceImageAsync(string bucketName, string keyName, CancellationToken cancellationToken) { var fileResult = await storageHandler.GetFileAsync(bucketName, keyName, cancellationToken); @@ -126,7 +124,7 @@ namespace Musify.Infrastructure.Messaging.Consumers return Result.Success(memoryStream); } - private async Task ProcessOperationAsync( + async Task ProcessOperationAsync( JobOperationItem operationItem, string bucketName, MemoryStream sourceImage, @@ -160,7 +158,7 @@ namespace Musify.Infrastructure.Messaging.Consumers resizedPictureResult.Value, "image/webp", bucketName, - $"{operationItem.Item.SaveOnRoute}/{Guid.NewGuid()}.webp", + $"{operationItem.Item.SaveOnRoute}/{Guid.NewGuid() }.webp", cancellationToken); if (!saveResult.IsSuccess) @@ -190,7 +188,7 @@ namespace Musify.Infrastructure.Messaging.Consumers } } - private static void MarkAllAsFailed(IEnumerable jobOperations, string errorMessage) + static void MarkAllAsFailed(IEnumerable jobOperations, string errorMessage) { foreach (var operationItem in jobOperations) { @@ -198,7 +196,7 @@ namespace Musify.Infrastructure.Messaging.Consumers } } - private static void MarkAsFailed(JobOperation jobOperation, string errorMessage) + static void MarkAsFailed(JobOperation jobOperation, string errorMessage) { jobOperation.JobState = JobState.Failed; jobOperation.FinishedAt = DateTime.UtcNow; From 9ba67654aa5b19e2b572ad33349c2b122f199984 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 12:22:07 +0100 Subject: [PATCH 045/172] Changes --- Application/Contracts/Application/IEvent.cs | 7 +++++++ Application/Events/RemoveFileEvent.cs | 8 +++++--- Application/Events/ResizePictureEvent.cs | 8 +++++--- .../CreatePlayListCommandHandler.cs | 4 ++-- .../DeletePlayListCommandHandler.cs | 16 +++++++-------- .../UpdatePlayListCommandHandler.cs | 20 +++++++++---------- 6 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 Application/Contracts/Application/IEvent.cs diff --git a/Application/Contracts/Application/IEvent.cs b/Application/Contracts/Application/IEvent.cs new file mode 100644 index 0000000..1d495fb --- /dev/null +++ b/Application/Contracts/Application/IEvent.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Contracts.Application +{ + public interface IEvent + { + Guid JobId { get; } + } +} \ No newline at end of file diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs index ff89abb..1106536 100644 --- a/Application/Events/RemoveFileEvent.cs +++ b/Application/Events/RemoveFileEvent.cs @@ -1,7 +1,9 @@ -namespace Musify.Application.Events +using Musify.Application.Contracts.Application; + +namespace Musify.Application.Events { public record RemoveFileEvent( + Guid JobId, string BucketName, - string KeyName, - Guid JobId); + string KeyName) : IEvent; } \ No newline at end of file diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs index df4f3d9..7366c6a 100644 --- a/Application/Events/ResizePictureEvent.cs +++ b/Application/Events/ResizePictureEvent.cs @@ -1,8 +1,10 @@ -namespace Musify.Application.Events +using Musify.Application.Contracts.Application; + +namespace Musify.Application.Events { public record ResizePictureEvent( + Guid JobId, string BucketName, string KeyName, - ResizePictureItems[] Items, - Guid JobId); + ResizePictureItems[] Items) : IEvent; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 5ba20da..b3fb755 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -61,6 +61,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList try { await eventBus.PublishAsync(new ResizePictureEvent( + Guid.NewGuid(), storageConfiguration.BucketName, originalImageStorageKey, [ @@ -76,8 +77,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ], - Guid.NewGuid()), cancellationToken); + ] ), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index 674a0fc..2e1d2b6 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -36,9 +36,9 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - originalImageStorageKey, - Guid.NewGuid()), cancellationToken); + originalImageStorageKey), cancellationToken); } catch (Exception exception) { @@ -51,9 +51,9 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - playList.SmallPictureKeyName, - Guid.NewGuid()), cancellationToken); + playList.SmallPictureKeyName), cancellationToken); } catch (Exception exception) { @@ -67,9 +67,9 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - playList.MediumPictureKeyName, - Guid.NewGuid()), cancellationToken); + playList.MediumPictureKeyName), cancellationToken); } catch (Exception exception) { @@ -83,9 +83,9 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - playList.LargePictureKeyName, - Guid.NewGuid()), cancellationToken); + playList.LargePictureKeyName), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 8d82457..4426615 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -54,9 +54,9 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - originalImageStorageKey, - Guid.NewGuid()), cancellationToken); + originalImageStorageKey), cancellationToken); } catch (Exception exception) { @@ -72,9 +72,9 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - playList.SmallPictureKeyName, - Guid.NewGuid()), cancellationToken); + playList.SmallPictureKeyName), cancellationToken); } catch (Exception exception) { @@ -88,9 +88,9 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - playList.MediumPictureKeyName, - Guid.NewGuid()), cancellationToken); + playList.MediumPictureKeyName), cancellationToken); } catch (Exception exception) { @@ -104,9 +104,9 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), storageConfiguration.BucketName, - playList.LargePictureKeyName, - Guid.NewGuid()), cancellationToken); + playList.LargePictureKeyName), cancellationToken); } catch (Exception exception) { @@ -139,6 +139,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new ResizePictureEvent( + Guid.NewGuid(), storageConfiguration.BucketName, newImageStorageKey, [ @@ -154,8 +155,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ], - Guid.NewGuid() + ] ), cancellationToken); } catch (Exception exception) From 03cbbae48afb2ec52de957f7b5310b8f978d7518 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 12:27:45 +0100 Subject: [PATCH 046/172] Rename --- .../{ResizePictureItems.cs => ResizePictureArguments.cs} | 2 +- Application/Events/ResizePictureEvent.cs | 2 +- .../CreatePlayList/CreatePlayListCommandHandler.cs | 6 +++--- .../UpdatePlayList/UpdatePlayListCommandHandler.cs | 6 +++--- .../Messaging/Consumers/ResizePictureConsumer.cs | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) rename Application/Events/{ResizePictureItems.cs => ResizePictureArguments.cs} (72%) diff --git a/Application/Events/ResizePictureItems.cs b/Application/Events/ResizePictureArguments.cs similarity index 72% rename from Application/Events/ResizePictureItems.cs rename to Application/Events/ResizePictureArguments.cs index 6af2dae..2944d5b 100644 --- a/Application/Events/ResizePictureItems.cs +++ b/Application/Events/ResizePictureArguments.cs @@ -1,6 +1,6 @@ namespace Musify.Application.Events { - public record ResizePictureItems( + public record ResizePictureArguments( int Width, int Height, string SaveOnRoute); diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs index 7366c6a..985bc41 100644 --- a/Application/Events/ResizePictureEvent.cs +++ b/Application/Events/ResizePictureEvent.cs @@ -6,5 +6,5 @@ namespace Musify.Application.Events Guid JobId, string BucketName, string KeyName, - ResizePictureItems[] Items) : IEvent; + ResizePictureArguments[] Arguments) : IEvent; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index b3fb755..0e0115c 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -65,15 +65,15 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList storageConfiguration.BucketName, originalImageStorageKey, [ - new ResizePictureItems( + new ResizePictureArguments( playListConfiguration.PicturesSizes.SmallPictureWidth, playListConfiguration.PicturesSizes.SmallPictureHeight, playListConfiguration.Routes.SmallPictures), - new ResizePictureItems( + new ResizePictureArguments( playListConfiguration.PicturesSizes.MediumPictureWidth, playListConfiguration.PicturesSizes.MediumPictureHeight, playListConfiguration.Routes.MediumPictures), - new ResizePictureItems( + new ResizePictureArguments( playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 4426615..dc8d1e3 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -143,15 +143,15 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList storageConfiguration.BucketName, newImageStorageKey, [ - new ResizePictureItems( + new ResizePictureArguments( playListConfiguration.PicturesSizes.SmallPictureWidth, playListConfiguration.PicturesSizes.SmallPictureHeight, playListConfiguration.Routes.SmallPictures), - new ResizePictureItems( + new ResizePictureArguments( playListConfiguration.PicturesSizes.MediumPictureWidth, playListConfiguration.PicturesSizes.MediumPictureHeight, playListConfiguration.Routes.MediumPictures), - new ResizePictureItems( + new ResizePictureArguments( playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index af9a40b..ca8cb45 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -12,7 +12,7 @@ namespace Musify.Infrastructure.Messaging.Consumers public class ResizePictureConsumer(IStorageHandler storageHandler, IPictureHandler pictureHandler, IDatabase database, ILogger logger) : IConsumer { - private record JobOperationItem(ResizePictureItems Item, JobOperation JobOperation); + private record JobOperationItem(ResizePictureArguments Item, JobOperation JobOperation); public const string QueueName = "picture-resize-queue"; @@ -24,7 +24,7 @@ namespace Musify.Infrastructure.Messaging.Consumers var cancellationToken = consumeContext.CancellationToken; var job = await GetOrCreateJobAsync(message, cancellationToken); - var jobOperations = await CreateJobOperationAsync(message.Items, job.Id, cancellationToken); + var jobOperations = await CreateJobOperationAsync(message.Arguments, job.Id, cancellationToken); await database.SaveChangesAsync(cancellationToken); @@ -66,7 +66,7 @@ namespace Musify.Infrastructure.Messaging.Consumers return job; } - var payloadJson = JsonSerializer.Serialize(message.Items); + var payloadJson = JsonSerializer.Serialize(message.Arguments); job = new Job { Id = message.JobId, @@ -78,7 +78,7 @@ namespace Musify.Infrastructure.Messaging.Consumers return job; } - async Task> CreateJobOperationAsync(IReadOnlyCollection resizeItems, Guid jobId, + async Task> CreateJobOperationAsync(IReadOnlyCollection resizeItems, Guid jobId, CancellationToken cancellationToken) { var jobOperations = new List(resizeItems.Count); From c378c7a0dd3992f5f8e2d02a03b09d38ebf63403 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 20:09:36 +0100 Subject: [PATCH 047/172] Changes --- .../Infrastructure/IPictureHandler.cs | 4 + .../Infrastructure/IStorageHandler.cs | 3 + Application/Events/ResizePictureArguments.cs | 7 - Application/Events/ResizePictureEvent.cs | 10 - .../Events/UpdatePlayListPictureEvent.cs | 6 + .../CreatePlayListCommandHandler.cs | 19 +- .../UpdatePlayListCommandHandler.cs | 20 +- .../ConfigurationDependencyInjection.cs | 3 +- .../StorageClientConfiguration.cs | 2 + .../WorkerStorageConfiguration.cs | 21 ++ .../Activities/Arguments/CopyFileArguments.cs | 8 + .../Arguments/RemoveFileArguments.cs | 6 + .../Arguments/ResizePictureArgument.cs | 10 + .../UpdatePlayListPictureArguments.cs | 8 + .../Messaging/Activities/CopyFilesActivity.cs | 39 ++++ .../Activities/RemoveFilesActivity.cs | 30 +++ .../Activities/ResizePictureActivity.cs | 51 +++++ .../UpdatePlayListPictureActivity.cs | 29 +++ .../Messaging/Consumers/RemoveFileConsumer.cs | 123 ----------- .../Consumers/ResizePictureConsumer.cs | 206 ------------------ .../UpdatePlayListPictureConsumer.cs | 82 +++++++ .../MassTransitDependencyInjection.cs | 11 +- Infrastructure/Pictures/PictureHandler.cs | 4 + Infrastructure/Storage/StorageHandler.cs | 26 +++ 24 files changed, 336 insertions(+), 392 deletions(-) delete mode 100644 Application/Events/ResizePictureArguments.cs delete mode 100644 Application/Events/ResizePictureEvent.cs create mode 100644 Application/Events/UpdatePlayListPictureEvent.cs create mode 100644 Infrastructure/Configuration/WorkerStorageConfiguration.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs create mode 100644 Infrastructure/Messaging/Activities/CopyFilesActivity.cs create mode 100644 Infrastructure/Messaging/Activities/RemoveFilesActivity.cs create mode 100644 Infrastructure/Messaging/Activities/ResizePictureActivity.cs create mode 100644 Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs delete mode 100644 Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs delete mode 100644 Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs create mode 100644 Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs diff --git a/Application/Contracts/Infrastructure/IPictureHandler.cs b/Application/Contracts/Infrastructure/IPictureHandler.cs index 63ba2e1..e582b42 100644 --- a/Application/Contracts/Infrastructure/IPictureHandler.cs +++ b/Application/Contracts/Infrastructure/IPictureHandler.cs @@ -4,6 +4,10 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IPictureHandler { + public string FileExtension { get; } + + public string ContentType { get; } + Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs index 613b524..c247c88 100644 --- a/Application/Contracts/Infrastructure/IStorageHandler.cs +++ b/Application/Contracts/Infrastructure/IStorageHandler.cs @@ -13,5 +13,8 @@ namespace Musify.Application.Contracts.Infrastructure Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); Task RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken); + + Task CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName, + CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/ResizePictureArguments.cs b/Application/Events/ResizePictureArguments.cs deleted file mode 100644 index 2944d5b..0000000 --- a/Application/Events/ResizePictureArguments.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Musify.Application.Events -{ - public record ResizePictureArguments( - int Width, - int Height, - string SaveOnRoute); -} \ No newline at end of file diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs deleted file mode 100644 index 985bc41..0000000 --- a/Application/Events/ResizePictureEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Musify.Application.Contracts.Application; - -namespace Musify.Application.Events -{ - public record ResizePictureEvent( - Guid JobId, - string BucketName, - string KeyName, - ResizePictureArguments[] Arguments) : IEvent; -} \ No newline at end of file diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs new file mode 100644 index 0000000..258c790 --- /dev/null +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -0,0 +1,6 @@ +namespace Musify.Application.Events +{ + public record UpdatePlayListPictureEvent( + Guid PlayListId, + string OriginalPictureKeyName); +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 0e0115c..ced2c65 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -60,24 +60,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList try { - await eventBus.PublishAsync(new ResizePictureEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - originalImageStorageKey, - [ - new ResizePictureArguments( - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.SmallPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.MediumPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - playListConfiguration.Routes.LargePictures) - ] ), cancellationToken); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index dc8d1e3..b5dba46 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -138,25 +138,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { - await eventBus.PublishAsync(new ResizePictureEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - newImageStorageKey, - [ - new ResizePictureArguments( - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.SmallPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.MediumPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - playListConfiguration.Routes.LargePictures) - ] - ), cancellationToken); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken); } catch (Exception exception) { diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index a357138..50f54bf 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -11,7 +11,8 @@ namespace Musify.Infrastructure.Configuration serviceDescriptors .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) - .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName); + .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName) + .AddValidatedConfiguration(configuration, WorkerStorageConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/StorageClientConfiguration.cs b/Infrastructure/Configuration/StorageClientConfiguration.cs index 7e7fc62..0175c0a 100644 --- a/Infrastructure/Configuration/StorageClientConfiguration.cs +++ b/Infrastructure/Configuration/StorageClientConfiguration.cs @@ -18,8 +18,10 @@ namespace Musify.Infrastructure.Configuration [MinLength(8)] public required string SecretAccessKey { get; set; } + [Required] public bool ForcePathStyle { get; set; } + [Required] public bool UseHttp { get; set; } } } diff --git a/Infrastructure/Configuration/WorkerStorageConfiguration.cs b/Infrastructure/Configuration/WorkerStorageConfiguration.cs new file mode 100644 index 0000000..6b556e1 --- /dev/null +++ b/Infrastructure/Configuration/WorkerStorageConfiguration.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class WorkerStorageConfiguration + { + public const string SectionName = "WorkerStorage"; + + public Routes Routes { get; set; } = new Routes(); + + [Required] + [MinLength(3)] + public required string BucketName { get; set; } + } + + public class Routes + { + [Required] + public string TemporaryFiles { get; set; } = "TemporaryFiles"; + } +} diff --git a/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs b/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs new file mode 100644 index 0000000..b5bbc96 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record CopyFileArguments( + string SourceBucketName, + string SourceKeyName, + string DestinationBucketName, + string DestinationKeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs b/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs new file mode 100644 index 0000000..a0e8851 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record RemoveFileArguments( + string BucketName, + string KeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs b/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs new file mode 100644 index 0000000..8d54d7e --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs @@ -0,0 +1,10 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record ResizePictureArgument( + string OriginalBucketName, + string OriginalKeyName, + string DestinationBucketName, + string DestinationKeyName, + int Width, + int Height); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs new file mode 100644 index 0000000..7fbd4bc --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record UpdatePlayListPictureArguments( + Guid PlayListId, + string SmallPictureKeyName, + string MediumPictureKeyName, + string LargePictureKeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/CopyFilesActivity.cs b/Infrastructure/Messaging/Activities/CopyFilesActivity.cs new file mode 100644 index 0000000..64499c2 --- /dev/null +++ b/Infrastructure/Messaging/Activities/CopyFilesActivity.cs @@ -0,0 +1,39 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class CopyFilesActivity(IStorageHandler storageHandler, ILogger logger) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + foreach (var file in executeContext.Arguments) + { + var copyFileResult = await storageHandler.CopyFileAsync( + file.SourceBucketName, + file.SourceKeyName, + file.DestinationBucketName, + file.SourceKeyName, + executeContext.CancellationToken); + + if (!copyFileResult.IsSuccess) + { + string errorMessage = string.Join("; ", copyFileResult.Errors); + logger.LogError( + "Failed to copy file from {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}. Error: {ErrorMessage}", + file.SourceBucketName, + file.SourceKeyName, + file.DestinationBucketName, + file.DestinationKeyName, + errorMessage); + throw new Exception(errorMessage); + } + } + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs new file mode 100644 index 0000000..ef53c37 --- /dev/null +++ b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs @@ -0,0 +1,30 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class RemoveFilesActivity(IStorageHandler storageHandler, ILogger logger) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + foreach (var file in executeContext.Arguments) + { + var removeFile = await storageHandler.RemoveFileAsync( + file.BucketName, + file.KeyName, + executeContext.CancellationToken); + + if (!removeFile.IsSuccess) + { + string errorMessage = string.Join(";", removeFile.Errors); + logger.LogWarning("Failed to remove file {File} from bucket {BucketName}: {ErrorMessage}", + file, file.BucketName, errorMessage); + } + } + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs new file mode 100644 index 0000000..868a02b --- /dev/null +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -0,0 +1,51 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class ResizePictureActivity(IStorageHandler storageHandler, IPictureHandler pictureHandler) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + var file = await storageHandler.GetFileAsync( + executeContext.Arguments.OriginalBucketName, + executeContext.Arguments.OriginalKeyName, + executeContext.CancellationToken); + + if (!file.IsSuccess) + { + string errorMessages = string.Join(", ", file.Errors); + throw new Exception(errorMessages); + } + + var resizedPicture = await pictureHandler.ResizePictureAsync( + file.Value, + executeContext.Arguments.Width, + executeContext.Arguments.Height, + executeContext.CancellationToken); + + if (!resizedPicture.IsSuccess) + { + string errorMessage = string.Join(", ", resizedPicture.Errors); + throw new Exception(errorMessage); + } + + var uploadFile = await storageHandler.UploadFileAsync( + resizedPicture.Value, + pictureHandler.ContentType, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + executeContext.CancellationToken); + + if (!uploadFile.IsSuccess) + { + var errorMessage = string.Join(", ", uploadFile.Errors); + throw new Exception(errorMessage); + } + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs new file mode 100644 index 0000000..2266ba1 --- /dev/null +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -0,0 +1,29 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class UpdatePlayListPictureActivity(IDatabase database) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + var PlayList = await database.PlayLists.FindAsync(executeContext.Arguments.PlayListId); + + if (PlayList is null) + { + throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); + } + + PlayList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + PlayList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + PlayList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + + database.PlayLists.Update(PlayList); + await database.SaveChangesAsync(executeContext.CancellationToken); + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs deleted file mode 100644 index ed33dbb..0000000 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ /dev/null @@ -1,123 +0,0 @@ -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 RemoveFileConsumer(IDatabase database, IStorageHandler storageHandler, ILogger logger) - : IConsumer - { - public const string QueueName = "file-remove-queue"; - - public async Task Consume(ConsumeContext consumeContext) - { - var message = consumeContext.Message; - var cancellationToken = consumeContext.CancellationToken; - - var job = await GetOrCreateJobAsync(message, cancellationToken); - var jobOperation = await CreateJobOperationAsync(job.Id, cancellationToken); - await database.SaveChangesAsync(cancellationToken); - - try - { - jobOperation.JobState = JobState.InProgress; - await database.SaveChangesAsync(cancellationToken); - - var removeResult = await storageHandler.RemoveFileAsync( - message.BucketName, - message.KeyName, - cancellationToken); - - if (!removeResult.IsSuccess) - { - MarkAsFailed(jobOperation, string.Join("; ", removeResult.Errors)); - - logger.LogError( - "Failed to remove file {BucketName}/{KeyName} for JobId={JobId}, JobOperationId={JobOperationId}. Error: {ErrorMessage}", - message.BucketName, - message.KeyName, - job.Id, - jobOperation.Id, - jobOperation.ErrorMessage); - - await database.SaveChangesAsync(cancellationToken); - return; - } - - jobOperation.JobState = JobState.Completed; - jobOperation.FinishedAt = DateTime.UtcNow; - - logger.LogInformation( - "Successfully removed file {BucketName}/{KeyName} for JobId={JobId}, JobOperationId={JobOperationId}.", - message.BucketName, - message.KeyName, - job.Id, - jobOperation.Id); - - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, - "An error occurred while consuming RemoveFileConsumer message for JobId={JobId}, BucketName={BucketName}, KeyName={KeyName}", - message.JobId, - message.BucketName, - message.KeyName); - MarkAsFailed(jobOperation, exception.Message); - await database.SaveChangesAsync(cancellationToken); - - throw; - } - } - - async Task GetOrCreateJobAsync(RemoveFileEvent message, CancellationToken cancellationToken) - { - var job = await database.Jobs - .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); - - if (job is not null) - { - return job; - } - - job = new Job - { - Id = message.JobId, - JobType = JobType.RemoveFile, - Payload = JsonSerializer.Serialize(message) - }; - - await database.Jobs.AddAsync(job, cancellationToken); - - logger.LogInformation( - "Created new job with id {JobId} for removing file {BucketName}/{KeyName}", - job.Id, - message.BucketName, - message.KeyName); - - return job; - } - - async Task CreateJobOperationAsync(Guid jobId, CancellationToken cancellationToken) - { - var jobOperation = new JobOperation - { - JobId = jobId - }; - - await database.JobOperations.AddAsync(jobOperation, cancellationToken); - return jobOperation; - } - - void MarkAsFailed(JobOperation jobOperation, string errorMessage) - { - jobOperation.JobState = JobState.Failed; - jobOperation.ErrorMessage = errorMessage; - jobOperation.FinishedAt = DateTime.UtcNow; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs deleted file mode 100644 index ca8cb45..0000000 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ /dev/null @@ -1,206 +0,0 @@ -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 - { - private record JobOperationItem(ResizePictureArguments Item, JobOperation JobOperation); - - public const string QueueName = "picture-resize-queue"; - - public async Task Consume(ConsumeContext consumeContext) - { - try - { - var message = consumeContext.Message; - var cancellationToken = consumeContext.CancellationToken; - - var job = await GetOrCreateJobAsync(message, cancellationToken); - var jobOperations = await CreateJobOperationAsync(message.Arguments, job.Id, cancellationToken); - - await database.SaveChangesAsync(cancellationToken); - - var sourceImageResult = await GetSourceImageAsync(message.BucketName, message.KeyName, cancellationToken); - if (!sourceImageResult.IsSuccess) - { - MarkAllAsFailed(jobOperations, $"Failed to retrieve source image from storage, ResultState: {sourceImageResult.Status}"); - await database.SaveChangesAsync(cancellationToken); - return; - } - - using var sourceImage = sourceImageResult.Value; - - foreach (var operationItem in jobOperations) - { - await ProcessOperationAsync( - operationItem, - message.BucketName, - sourceImage, - cancellationToken); - - await database.SaveChangesAsync(cancellationToken); - } - } - catch (Exception exception) - { - logger.LogError(exception, "An error occurred while consuming the ResizePictureConsumer message."); - throw; - } - } - - async Task GetOrCreateJobAsync(ResizePictureEvent message, CancellationToken cancellationToken) - { - var job = await database.Jobs - .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); - - if (job is not null) - { - return job; - } - - var payloadJson = JsonSerializer.Serialize(message.Arguments); - job = new Job - { - Id = message.JobId, - JobType = JobType.ResizePicture, - Payload = payloadJson - }; - - await database.Jobs.AddAsync(job, cancellationToken); - return job; - } - - async Task> CreateJobOperationAsync(IReadOnlyCollection resizeItems, Guid jobId, - CancellationToken cancellationToken) - { - var jobOperations = new List(resizeItems.Count); - - foreach (var resizeItem in resizeItems) - { - var jobOperation = new JobOperation - { - JobId = jobId - }; - - await database.JobOperations.AddAsync(jobOperation, cancellationToken); - jobOperations.Add(new JobOperationItem(resizeItem, jobOperation)); - } - - return jobOperations; - } - - async Task> GetSourceImageAsync(string bucketName, string keyName, CancellationToken cancellationToken) - { - var fileResult = await storageHandler.GetFileAsync(bucketName, keyName, cancellationToken); - - if (fileResult.IsNotFound()) - { - logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", bucketName, keyName); - return Result.NotFound(); - } - - if (!fileResult.IsSuccess) - { - logger.LogError("Failed to read source image from storage: BucketName={BucketName}, KeyName={KeyName}. Errors: {Errors}", - bucketName, - keyName, - string.Join("; ", fileResult.Errors)); - return Result.Error(string.Join("; ", fileResult.Errors)); - } - - using var fileStream = fileResult.Value; - var memoryStream = new MemoryStream(); - await fileStream.CopyToAsync(memoryStream, cancellationToken); - memoryStream.Position = 0; - - return Result.Success(memoryStream); - } - - async Task ProcessOperationAsync( - JobOperationItem operationItem, - string bucketName, - MemoryStream sourceImage, - CancellationToken cancellationToken) - { - var jobOperation = operationItem.JobOperation; - - try - { - jobOperation.JobState = JobState.InProgress; - sourceImage.Position = 0; - - var resizedPictureResult = await pictureHandler.ResizePictureAsync( - sourceImage, - operationItem.Item.Width, - operationItem.Item.Height, - cancellationToken); - - if (!resizedPictureResult.IsSuccess) - { - var errorMessage = string.Join("; ", resizedPictureResult.Errors); - logger.LogError("Failed to resize picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", - jobOperation.Id, - errorMessage); - - MarkAsFailed(jobOperation, errorMessage); - return; - } - - var saveResult = await storageHandler.UploadFileAsync( - resizedPictureResult.Value, - "image/webp", - bucketName, - $"{operationItem.Item.SaveOnRoute}/{Guid.NewGuid() }.webp", - cancellationToken); - - if (!saveResult.IsSuccess) - { - var errorMessage = string.Join("; ", saveResult.Errors); - logger.LogError("Failed to upload resized picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", - jobOperation.Id, - errorMessage); - - MarkAsFailed(jobOperation, errorMessage); - return; - } - - jobOperation.JobState = JobState.Completed; - jobOperation.FinishedAt = DateTime.UtcNow; - } - catch (Exception exception) - { - logger.LogError(exception, - "An error occurred while processing resize for JobOperationId={JobOperationId}, Width={Width}, Height={Height}, SaveOnRoute={SaveOnRoute}", - jobOperation.Id, - operationItem.Item.Width, - operationItem.Item.Height, - operationItem.Item.SaveOnRoute); - - MarkAsFailed(jobOperation, exception.Message); - } - } - - static void MarkAllAsFailed(IEnumerable jobOperations, string errorMessage) - { - foreach (var operationItem in jobOperations) - { - MarkAsFailed(operationItem.JobOperation, errorMessage); - } - } - - static void MarkAsFailed(JobOperation jobOperation, string errorMessage) - { - jobOperation.JobState = JobState.Failed; - jobOperation.FinishedAt = DateTime.UtcNow; - jobOperation.ErrorMessage = errorMessage; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs new file mode 100644 index 0000000..e3e6ada --- /dev/null +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -0,0 +1,82 @@ +using MassTransit; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class UpdatePlayListPictureConsumer(IBus bus, IPictureHandler pictureHandler, + StorageSettings storageSettings, PlayListConfiguration playListConfiguration) + : IConsumer + { + public const string QueueName = "Update-PlayList-Picture"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + var smallPictureKeyName = Path.Combine( + playListConfiguration.Routes.SmallPictures, + $"{consumeContext.Message.PlayListId}_small.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + playListConfiguration.Routes.MediumPictures, + $"{consumeContext.Message.PlayListId}_medium.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + playListConfiguration.Routes.LargePictures, + $"{consumeContext.Message.PlayListId}_large.{pictureHandler.FileExtension}"); + + routingSlipBuilder.AddActivity( + "ResizeSmall", + new Uri("Queue:Resize-Picture_Execute"), + new ResizePictureArgument( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName, + storageSettings.BucketName, + playListConfiguration.Routes.SmallPictures, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight)); + + routingSlipBuilder.AddActivity( + "ResizeMedium", + new Uri("Queue:Resize-Picture_Execute"), + new ResizePictureArgument( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName, + storageSettings.BucketName, + playListConfiguration.Routes.MediumPictures, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight)); + + routingSlipBuilder.AddActivity( + "ResizeLarge", + new Uri("Queue:Resize-Picture_Execute"), + new ResizePictureArgument( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName, + storageSettings.BucketName, + playListConfiguration.Routes.LargePictures, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)); + + routingSlipBuilder.AddActivity( + "DeleteOriginal", + new Uri("Queue:Delete-Picture_Execute"), + new RemoveFileArguments( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName)); + + routingSlipBuilder.AddActivity( + "UpdatePlayListPicture", + new Uri("Queue:Update-PlayList-Picture_Execute"), + new UpdatePlayListPictureArguments( + consumeContext.Message.PlayListId, + smallPictureKeyName, + mediumPictureKeyName, + largePictureKeyName)); + + var routingSlip = routingSlipBuilder.Build(); + await bus.Execute(routingSlip); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index def4d94..f44ddcf 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -38,8 +38,7 @@ namespace Musify.Infrastructure.Messaging { serviceDescriptors.AddMassTransit(options => { - options.AddConsumer(); - options.AddConsumer(); + options.AddConsumer(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -60,13 +59,9 @@ namespace Musify.Infrastructure.Messaging options.Password(password); } ); - busFactoryConfigurator.ReceiveEndpoint(ResizePictureConsumer.QueueName, endpointConfigurator => + busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); - busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); + endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); } ); } ); diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index 1e5db72..4185761 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -9,6 +9,10 @@ namespace Musify.Infrastructure.Pictures public class PictureHandler(ILogger logger) : IPictureHandler { + public string FileExtension => "webp"; + + public string ContentType => "image/webp"; + public async Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) { try diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index a7b9e28..d6a42cf 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -142,5 +142,31 @@ namespace Musify.Infrastructure.Storage return Result.Success(keyName); } + + public async Task CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName, + CancellationToken cancellationToken) + { + var request = new CopyObjectRequest + { + SourceBucket = sourceBucketName, + SourceKey = sourceKeyName, + DestinationBucket = destinationBucketName, + DestinationKey = destinationKeyName + }; + + try + { + var response = await amazonS3.CopyObjectAsync(request, cancellationToken); + logger.LogInformation("Successfully copied file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", + sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); + return Result.Success(); + } + catch + { + logger.LogError("Failed to copy file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", + sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); + return Result.Error($"Failed to copy file from S3 with bucket name {sourceBucketName} and key name {sourceKeyName} to bucket name {destinationBucketName} and key name {destinationKeyName}"); + } + } } } \ No newline at end of file From 61f4435cc22367e7b58f5f26e17b65db5f2b17bc Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 20:24:10 +0100 Subject: [PATCH 048/172] Refactor --- .../ConfigurationDependencyInjection.cs | 3 +- .../WorkerStorageConfiguration.cs | 21 ----- .../Activities/RemoveFilesActivity.cs | 33 ++++--- .../Activities/ResizePictureActivity.cs | 2 + .../UpdatePlayListPictureActivity.cs | 18 ++-- .../UpdatePlayListPictureConsumer.cs | 80 +++++++++------- .../MassTransitDependencyInjection.cs | 92 ++++++++++++------- 7 files changed, 134 insertions(+), 115 deletions(-) delete mode 100644 Infrastructure/Configuration/WorkerStorageConfiguration.cs diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index 50f54bf..a357138 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -11,8 +11,7 @@ namespace Musify.Infrastructure.Configuration serviceDescriptors .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) - .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName) - .AddValidatedConfiguration(configuration, WorkerStorageConfiguration.SectionName); + .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/WorkerStorageConfiguration.cs b/Infrastructure/Configuration/WorkerStorageConfiguration.cs deleted file mode 100644 index 6b556e1..0000000 --- a/Infrastructure/Configuration/WorkerStorageConfiguration.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Musify.Infrastructure.Configuration -{ - public class WorkerStorageConfiguration - { - public const string SectionName = "WorkerStorage"; - - public Routes Routes { get; set; } = new Routes(); - - [Required] - [MinLength(3)] - public required string BucketName { get; set; } - } - - public class Routes - { - [Required] - public string TemporaryFiles { get; set; } = "TemporaryFiles"; - } -} diff --git a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs index ef53c37..4a4479d 100644 --- a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs +++ b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs @@ -6,24 +6,27 @@ using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { public class RemoveFilesActivity(IStorageHandler storageHandler, ILogger logger) - : IExecuteActivity + : IExecuteActivity { - public async Task Execute(ExecuteContext executeContext) - { - foreach (var file in executeContext.Arguments) - { - var removeFile = await storageHandler.RemoveFileAsync( - file.BucketName, - file.KeyName, - executeContext.CancellationToken); + public const string ExecuteEndpointName = "Remove-File"; - if (!removeFile.IsSuccess) - { - string errorMessage = string.Join(";", removeFile.Errors); - logger.LogWarning("Failed to remove file {File} from bucket {BucketName}: {ErrorMessage}", - file, file.BucketName, errorMessage); - } + public async Task Execute(ExecuteContext executeContext) + { + var removeFile = await storageHandler.RemoveFileAsync( + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + executeContext.CancellationToken); + + if (!removeFile.IsSuccess) + { + string errorMessage = string.Join(";", removeFile.Errors); + logger.LogWarning( + "Failed to remove file {KeyName} from bucket {BucketName}: {ErrorMessage}", + executeContext.Arguments.KeyName, + executeContext.Arguments.BucketName, + errorMessage); } + return executeContext.Completed(); } } diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs index 868a02b..5e75db4 100644 --- a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -7,6 +7,8 @@ namespace Musify.Infrastructure.Messaging.Activities public class ResizePictureActivity(IStorageHandler storageHandler, IPictureHandler pictureHandler) : IExecuteActivity { + public const string ExecuteEndpointName = "Resize-Picture"; + public async Task Execute(ExecuteContext executeContext) { var file = await storageHandler.GetFileAsync( diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs index 2266ba1..0d5ea4b 100644 --- a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -1,4 +1,5 @@ using MassTransit; +using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Messaging.Activities.Arguments; @@ -7,20 +8,21 @@ namespace Musify.Infrastructure.Messaging.Activities public class UpdatePlayListPictureActivity(IDatabase database) : IExecuteActivity { + public const string ExecuteEndpointName = "Update-PlayList-Picture"; + public async Task Execute(ExecuteContext executeContext) { - var PlayList = await database.PlayLists.FindAsync(executeContext.Arguments.PlayListId); + var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, + executeContext.CancellationToken); - if (PlayList is null) - { + if (playList is null) throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); - } - PlayList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; - PlayList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; - PlayList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; - database.PlayLists.Update(PlayList); + database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); return executeContext.Completed(); diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index e3e6ada..5f5a57a 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -2,6 +2,7 @@ using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Consumers @@ -26,49 +27,38 @@ namespace Musify.Infrastructure.Messaging.Consumers playListConfiguration.Routes.LargePictures, $"{consumeContext.Message.PlayListId}_large.{pictureHandler.FileExtension}"); - routingSlipBuilder.AddActivity( + AddResizeActivity( + routingSlipBuilder, "ResizeSmall", - new Uri("Queue:Resize-Picture_Execute"), - new ResizePictureArgument( - storageSettings.BucketName, - consumeContext.Message.OriginalPictureKeyName, - storageSettings.BucketName, - playListConfiguration.Routes.SmallPictures, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight)); - - routingSlipBuilder.AddActivity( - "ResizeMedium", - new Uri("Queue:Resize-Picture_Execute"), - new ResizePictureArgument( - storageSettings.BucketName, - consumeContext.Message.OriginalPictureKeyName, - storageSettings.BucketName, - playListConfiguration.Routes.MediumPictures, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight)); + smallPictureKeyName, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + consumeContext.Message.OriginalPictureKeyName); - routingSlipBuilder.AddActivity( + AddResizeActivity( + routingSlipBuilder, + "ResizeMedium", + mediumPictureKeyName, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + consumeContext.Message.OriginalPictureKeyName); + + AddResizeActivity( + routingSlipBuilder, "ResizeLarge", - new Uri("Queue:Resize-Picture_Execute"), - new ResizePictureArgument( - storageSettings.BucketName, - consumeContext.Message.OriginalPictureKeyName, - storageSettings.BucketName, - playListConfiguration.Routes.LargePictures, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)); + largePictureKeyName, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight, + consumeContext.Message.OriginalPictureKeyName); routingSlipBuilder.AddActivity( "DeleteOriginal", - new Uri("Queue:Delete-Picture_Execute"), - new RemoveFileArguments( - storageSettings.BucketName, - consumeContext.Message.OriginalPictureKeyName)); + BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), + new RemoveFileArguments(storageSettings.BucketName, consumeContext.Message.OriginalPictureKeyName)); routingSlipBuilder.AddActivity( "UpdatePlayListPicture", - new Uri("Queue:Update-PlayList-Picture_Execute"), + BuildExecuteUri(UpdatePlayListPictureActivity.ExecuteEndpointName), new UpdatePlayListPictureArguments( consumeContext.Message.PlayListId, smallPictureKeyName, @@ -78,5 +68,27 @@ namespace Musify.Infrastructure.Messaging.Consumers var routingSlip = routingSlipBuilder.Build(); await bus.Execute(routingSlip); } + + void AddResizeActivity( + RoutingSlipBuilder routingSlipBuilder, + string activityName, + string destinationKeyName, + int width, + int height, + string originalPictureKeyName) + { + routingSlipBuilder.AddActivity( + activityName, + BuildExecuteUri(ResizePictureActivity.ExecuteEndpointName), + new ResizePictureArgument( + storageSettings.BucketName, + originalPictureKeyName, + storageSettings.BucketName, + destinationKeyName, + width, + height)); + } + + static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); } } \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index f44ddcf..d2e843e 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -1,6 +1,8 @@ using MassTransit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Consumers; namespace Musify.Infrastructure.Messaging @@ -11,26 +13,10 @@ namespace Musify.Infrastructure.Messaging { serviceDescriptors.AddMassTransit(options => { - options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => - { - var section = configuration.GetSection("MassTransit"); - - var host = section["Host"] - ?? throw new InvalidOperationException("MassTransit:Host configuration value not found."); - - var username = section["Username"] - ?? throw new InvalidOperationException("MassTransit:Username configuration value not found."); - - var password = section["Password"] - ?? throw new InvalidOperationException("MassTransit:Password configuration value not found."); - - busFactoryConfigurator.Host(host, options => - { - options.Username(username); - options.Password(password); - } ); - } ); + options.UsingRabbitMq((_, busFactoryConfigurator) => + ConfigureRabbitMqHost(busFactoryConfigurator, configuration)); } ); + return serviceDescriptors; } @@ -39,34 +25,70 @@ namespace Musify.Infrastructure.Messaging serviceDescriptors.AddMassTransit(options => { options.AddConsumer(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { - var section = configuration.GetSection("MassTransit"); - - var host = section["Host"] - ?? throw new InvalidOperationException("MassTransit:Host configuration value not found."); - - var username = section["Username"] - ?? throw new InvalidOperationException("MassTransit:Username configuration value not found."); - - var password = section["Password"] - ?? throw new InvalidOperationException("MassTransit:Password configuration value not found."); - - busFactoryConfigurator.Host(host, options => - { - options.Username(username); - options.Password(password); - } ); + ConfigureRabbitMqHost(busFactoryConfigurator, configuration); busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + ResizePictureActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + RemoveFilesActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdatePlayListPictureActivity.ExecuteEndpointName); } ); } ); return serviceDescriptors; } + + static void ConfigureRabbitMqHost(IRabbitMqBusFactoryConfigurator busFactoryConfigurator, IConfiguration configuration) + { + var section = configuration.GetSection("MassTransit"); + + var host = section["Host"] + ?? throw new InvalidOperationException("MassTransit:Host configuration value not found."); + + var username = section["Username"] + ?? throw new InvalidOperationException("MassTransit:Username configuration value not found."); + + var password = section["Password"] + ?? throw new InvalidOperationException("MassTransit:Password configuration value not found."); + + busFactoryConfigurator.Host(host, options => + { + options.Username(username); + options.Password(password); + } ); + } + + static void ConfigureExecuteActivityEndpoint( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string endpointName) + where TActivity : class, IExecuteActivity + where TArguments : class + { + busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => + { + endpointConfigurator.ExecuteActivityHost(busRegistrationContext); + } ); + } } } \ No newline at end of file From b97b2f7fb5e7ed25fb05ea4d62e8ef92d366795b Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 20:24:46 +0100 Subject: [PATCH 049/172] Renames --- ...{StorageSettings.cs => ApplicationStorageConfiguration.cs} | 4 ++-- Application/Configuration/ConfigurationDependencyInjection.cs | 2 +- .../Commands/CreatePlayList/CreatePlayListCommandHandler.cs | 2 +- .../Commands/DeletePlayList/DeletePlayListCommandHandler.cs | 2 +- .../Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs | 2 +- .../Configuration/ConfigurationDependencyInjection.cs | 2 +- ...Configuration.cs => InfrastructureStorageConfiguration.cs} | 4 ++-- .../Messaging/Consumers/UpdatePlayListPictureConsumer.cs | 2 +- Infrastructure/Storage/StorageDependencyInjection.cs | 2 +- Infrastructure/Storage/StorageHandler.cs | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) rename Application/Configuration/{StorageSettings.cs => ApplicationStorageConfiguration.cs} (66%) rename Infrastructure/Configuration/{StorageClientConfiguration.cs => InfrastructureStorageConfiguration.cs} (81%) diff --git a/Application/Configuration/StorageSettings.cs b/Application/Configuration/ApplicationStorageConfiguration.cs similarity index 66% rename from Application/Configuration/StorageSettings.cs rename to Application/Configuration/ApplicationStorageConfiguration.cs index f2c0af1..2439621 100644 --- a/Application/Configuration/StorageSettings.cs +++ b/Application/Configuration/ApplicationStorageConfiguration.cs @@ -2,9 +2,9 @@ using System.ComponentModel.DataAnnotations; namespace Musify.Application.Configuration { - public class StorageSettings + public class ApplicationStorageConfiguration { - public const string SectionName = "Storage"; + public const string SectionName = "ApplicationStorage"; [Required] [MinLength(3)] diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs index 4630946..be5b37d 100644 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -9,7 +9,7 @@ namespace Musify.Application.Configuration public static IServiceCollection AddApplicationConfigurations(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors - .AddValidatedConfiguration(configuration, StorageSettings.SectionName) + .AddValidatedConfiguration(configuration, ApplicationStorageConfiguration.SectionName) .AddValidatedConfiguration(configuration, PlayListConfiguration.SectionName); return serviceDescriptors; diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index ced2c65..c5bad12 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -11,7 +11,7 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands.CreatePlayList { public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, - ILogger logger, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration) + ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index 2e1d2b6..cb15121 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -10,7 +10,7 @@ using Musify.Application.PlayLists; namespace Musify.Application.PlayLists.Commands.DeletePlayList { public class DeletePlayListCommandHandler(IDatabase database, IEventBus eventBus, - StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) + ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) : IRequestHandler> { public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index b5dba46..5791661 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -11,7 +11,7 @@ using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Commands.UpdatePlayList { public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, - ILogger logger, StorageSettings storageConfiguration, PlayListConfiguration playListConfiguration) + ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index a357138..6a6ccde 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -11,7 +11,7 @@ namespace Musify.Infrastructure.Configuration serviceDescriptors .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) - .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName); + .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/StorageClientConfiguration.cs b/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs similarity index 81% rename from Infrastructure/Configuration/StorageClientConfiguration.cs rename to Infrastructure/Configuration/InfrastructureStorageConfiguration.cs index 0175c0a..18cdd2a 100644 --- a/Infrastructure/Configuration/StorageClientConfiguration.cs +++ b/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs @@ -2,9 +2,9 @@ using System.ComponentModel.DataAnnotations; namespace Musify.Infrastructure.Configuration { - public class StorageClientConfiguration + public class InfrastructureStorageConfiguration { - public const string SectionName = "Storage"; + public const string SectionName = "InfrastructureStorage"; [Required] [Url] diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index 5f5a57a..a5deed2 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -8,7 +8,7 @@ using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Consumers { public class UpdatePlayListPictureConsumer(IBus bus, IPictureHandler pictureHandler, - StorageSettings storageSettings, PlayListConfiguration playListConfiguration) + ApplicationStorageConfiguration storageSettings, PlayListConfiguration playListConfiguration) : IConsumer { public const string QueueName = "Update-PlayList-Picture"; diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs index d0f46ad..5e2f4ae 100644 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ b/Infrastructure/Storage/StorageDependencyInjection.cs @@ -11,7 +11,7 @@ namespace Musify.Infrastructure.Storage { serviceDescriptors.AddScoped(serviceProvider => { - var configuration = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetRequiredService(); var s3Configuration = new AmazonS3Config { ServiceURL = configuration.ServiceUrl, diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index d6a42cf..edacf15 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -9,7 +9,7 @@ using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Storage { - public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, StorageClientConfiguration storageClientConfiguration, ILogger logger) + public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, InfrastructureStorageConfiguration storageClientConfiguration, ILogger logger) : IStorageHandler { public async Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) From b781374691a9d2ffbc5b6ca2f48fd4c3b887fb55 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 20:33:56 +0100 Subject: [PATCH 050/172] Last consumer --- .../DeletePlayListCommandHandler.cs | 81 ++++++++----------- .../Messaging/Consumers/RemoveFileConsumer.cs | 30 +++++++ .../MassTransitDependencyInjection.cs | 6 ++ 3 files changed, 68 insertions(+), 49 deletions(-) create mode 100644 Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index cb15121..1148783 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -1,11 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; -using Musify.Application.PlayLists; namespace Musify.Application.PlayLists.Commands.DeletePlayList { @@ -31,66 +29,33 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) { - var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, playList.OriginalPictureKeyName); - - try + var keysToRemove = new List { - await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - originalImageStorageKey), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for original picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + Path.Join(playListConfiguration.Routes.OriginalPictures, playList.OriginalPictureKeyName) + }; if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for small picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + keysToRemove.Add(playList.SmallPictureKeyName); } if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for medium picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + keysToRemove.Add(playList.MediumPictureKeyName); } if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) { - try + keysToRemove.Add(playList.LargePictureKeyName); + } + + foreach (var key in keysToRemove) + { + var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); + + if (!publishResult.IsSuccess) { - await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for large picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); + return publishResult; } } } @@ -99,5 +64,23 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList await database.SaveChangesAsync(cancellationToken); return Result.NoContent(); } + + async Task TryPublishRemoveFileEventAsync(Guid playListId, string keyName, CancellationToken cancellationToken) + { + try + { + await eventBus.PublishAsync(new RemoveFileEvent( + Guid.NewGuid(), + storageConfiguration.BucketName, + keyName), cancellationToken); + + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for PlayList id={PlayListId} and key={KeyName}.", playListId, keyName); + return Result.Error($"Failed to publish remove file event for PlayList with id {playListId}."); + } + } } } \ 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..a057f45 --- /dev/null +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -0,0 +1,30 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class RemoveFileConsumer(IBus bus) + : IConsumer + { + public const string QueueName = "Remove-File"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + routingSlipBuilder.AddActivity( + "RemoveFile", + BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), + new RemoveFileArguments( + consumeContext.Message.BucketName, + consumeContext.Message.KeyName)); + + var routingSlip = routingSlipBuilder.Build(); + await bus.Execute(routingSlip); + } + + static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); + } +} diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index d2e843e..55b85b1 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -25,6 +25,7 @@ namespace Musify.Infrastructure.Messaging serviceDescriptors.AddMassTransit(options => { options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); @@ -38,6 +39,11 @@ namespace Musify.Infrastructure.Messaging endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); + busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + ConfigureExecuteActivityEndpoint( busFactoryConfigurator, busRegistrationContext, From 4a17b21f199d3466db088b6649b2c22d21f40405 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 21:40:05 +0100 Subject: [PATCH 051/172] Rework jobs --- Application/Contracts/Application/IEvent.cs | 7 - .../Contracts/Infrastructure/IDatabase.cs | 4 +- .../Infrastructure/IProcessTrackingStore.cs | 25 ++++ Application/Events/RemoveFileEvent.cs | 3 +- .../DeletePlayListCommandHandler.cs | 1 - .../UpdatePlayListCommandHandler.cs | 5 - Domain/Entities/Job.cs | 22 ---- Domain/Entities/JobOperation.cs | 41 ------ Domain/Entities/JobState.cs | 10 -- Domain/Entities/JobType.cs | 8 -- Domain/Entities/ProcessExecution.cs | 36 ++++++ Domain/Entities/ProcessExecutionStatus.cs | 10 ++ Domain/Entities/ProcessStepComponentType.cs | 8 ++ Domain/Entities/ProcessStepExecution.cs | 40 ++++++ .../InfrastructureDependencyInjection.cs | 6 +- .../Activities/RemoveFilesActivity.cs | 55 ++++++-- .../Activities/ResizePictureActivity.cs | 87 ++++++++----- .../UpdatePlayListPictureActivity.cs | 44 +++++-- .../Filters/ProcessTrackingConsumeFilter.cs | 48 +++++++ .../MassTransitDependencyInjection.cs | 3 + Infrastructure/Persistence/Database.cs | 21 ++- .../Persistence/ProcessTrackingStore.cs | 122 ++++++++++++++++++ 22 files changed, 449 insertions(+), 157 deletions(-) delete mode 100644 Application/Contracts/Application/IEvent.cs create mode 100644 Application/Contracts/Infrastructure/IProcessTrackingStore.cs delete mode 100644 Domain/Entities/Job.cs delete mode 100644 Domain/Entities/JobOperation.cs delete mode 100644 Domain/Entities/JobState.cs delete mode 100644 Domain/Entities/JobType.cs create mode 100644 Domain/Entities/ProcessExecution.cs create mode 100644 Domain/Entities/ProcessExecutionStatus.cs create mode 100644 Domain/Entities/ProcessStepComponentType.cs create mode 100644 Domain/Entities/ProcessStepExecution.cs create mode 100644 Infrastructure/Messaging/Filters/ProcessTrackingConsumeFilter.cs create mode 100644 Infrastructure/Persistence/ProcessTrackingStore.cs diff --git a/Application/Contracts/Application/IEvent.cs b/Application/Contracts/Application/IEvent.cs deleted file mode 100644 index 1d495fb..0000000 --- a/Application/Contracts/Application/IEvent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Musify.Application.Contracts.Application -{ - public interface IEvent - { - Guid JobId { get; } - } -} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 5b3cdf2..ab56211 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -15,9 +15,9 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Uploads { get; } - DbSet Jobs { get; } + DbSet ProcessExecutions { get; } - DbSet JobOperations { get; } + DbSet ProcessStepExecutions { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/Application/Contracts/Infrastructure/IProcessTrackingStore.cs b/Application/Contracts/Infrastructure/IProcessTrackingStore.cs new file mode 100644 index 0000000..97921ca --- /dev/null +++ b/Application/Contracts/Infrastructure/IProcessTrackingStore.cs @@ -0,0 +1,25 @@ +using Musify.Domain.Entities; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IProcessTrackingStore + { + Task GetOrCreateProcessAsync( + string processName, + Guid? correlationId, + Guid? conversationId, + Guid? messageId, + CancellationToken cancellationToken); + + Task StartStepAsync( + Guid processExecutionId, + string stepName, + ProcessStepComponentType componentType, + int attempt, + CancellationToken cancellationToken); + + Task CompleteStepAsync(Guid processExecutionId, Guid stepExecutionId, CancellationToken cancellationToken); + + Task FailStepAsync(Guid processExecutionId, Guid stepExecutionId, string errorMessage, CancellationToken cancellationToken); + } +} diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs index 1106536..88cbd0e 100644 --- a/Application/Events/RemoveFileEvent.cs +++ b/Application/Events/RemoveFileEvent.cs @@ -3,7 +3,6 @@ namespace Musify.Application.Events { public record RemoveFileEvent( - Guid JobId, string BucketName, - string KeyName) : IEvent; + string KeyName); } \ No newline at end of file diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index 1148783..8853bdd 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -70,7 +70,6 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), storageConfiguration.BucketName, keyName), cancellationToken); diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 5791661..8225c26 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -1,6 +1,5 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; @@ -54,7 +53,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), storageConfiguration.BucketName, originalImageStorageKey), cancellationToken); } @@ -72,7 +70,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), storageConfiguration.BucketName, playList.SmallPictureKeyName), cancellationToken); } @@ -88,7 +85,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), storageConfiguration.BucketName, playList.MediumPictureKeyName), cancellationToken); } @@ -104,7 +100,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { await eventBus.PublishAsync(new RemoveFileEvent( - Guid.NewGuid(), storageConfiguration.BucketName, playList.LargePictureKeyName), cancellationToken); } diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs deleted file mode 100644 index 45f07dc..0000000 --- a/Domain/Entities/Job.cs +++ /dev/null @@ -1,22 +0,0 @@ -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] - [MaxLength(512)] - public required string Payload { get; set; } - - [Required] - public DateTime CreateAt { get; set; } = DateTime.UtcNow; - } -} \ No newline at end of file diff --git a/Domain/Entities/JobOperation.cs b/Domain/Entities/JobOperation.cs deleted file mode 100644 index 3e14c26..0000000 --- a/Domain/Entities/JobOperation.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Musify.Domain.Entities -{ -#pragma warning disable CS8618 - [Table("JobExecution")] - public class JobOperation - { - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - [Required] - public required Guid JobId { get; set; } - - [Required] - public JobState JobState { get; set; } = JobState.Pending; - - [NotMapped] - public bool IsCompleted => JobState == JobState.Completed; - - [NotMapped] - public bool IsFailed => JobState == JobState.Failed; - - [NotMapped] - public bool IsPending => JobState == JobState.Pending; - - [Required] - [MaxLength(512)] - public string ErrorMessage { get; set; } = string.Empty; - - [Required] - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - [Required] - public DateTime FinishedAt { get; set; } - - [ForeignKey(nameof(JobId)) ] - public Job Job { get; set; } - } -} \ No newline at end of file diff --git a/Domain/Entities/JobState.cs b/Domain/Entities/JobState.cs deleted file mode 100644 index 9cc6cde..0000000 --- a/Domain/Entities/JobState.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index c0ded2f..0000000 --- a/Domain/Entities/JobType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Musify.Domain.Entities -{ - public enum JobType - { - ResizePicture, - RemoveFile, - } -} \ No newline at end of file diff --git a/Domain/Entities/ProcessExecution.cs b/Domain/Entities/ProcessExecution.cs new file mode 100644 index 0000000..b0bbf21 --- /dev/null +++ b/Domain/Entities/ProcessExecution.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("ProcessExecution")] + public class ProcessExecution + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid? CorrelationId { get; set; } + + public Guid? ConversationId { get; set; } + + public Guid? MessageId { get; set; } + + [Required] + [MaxLength(256)] + public required string ProcessName { get; set; } + + [Required] + public ProcessExecutionStatus Status { get; set; } = ProcessExecutionStatus.Running; + + [Required] + public DateTime StartedDateTime { get; set; } = DateTime.UtcNow; + + public DateTime FinishedDateTime { get; set; } + + [MaxLength(2048)] + public string ErrorMessage { get; set; } + + public ICollection Steps { get; set; } = new List(); + } +} diff --git a/Domain/Entities/ProcessExecutionStatus.cs b/Domain/Entities/ProcessExecutionStatus.cs new file mode 100644 index 0000000..b609694 --- /dev/null +++ b/Domain/Entities/ProcessExecutionStatus.cs @@ -0,0 +1,10 @@ +namespace Musify.Domain.Entities +{ + public enum ProcessExecutionStatus + { + Running, + Succeeded, + Failed, + Compensated + } +} diff --git a/Domain/Entities/ProcessStepComponentType.cs b/Domain/Entities/ProcessStepComponentType.cs new file mode 100644 index 0000000..43c4848 --- /dev/null +++ b/Domain/Entities/ProcessStepComponentType.cs @@ -0,0 +1,8 @@ +namespace Musify.Domain.Entities +{ + public enum ProcessStepComponentType + { + Consumer, + Activity + } +} diff --git a/Domain/Entities/ProcessStepExecution.cs b/Domain/Entities/ProcessStepExecution.cs new file mode 100644 index 0000000..a1d01cc --- /dev/null +++ b/Domain/Entities/ProcessStepExecution.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("ProcessStepExecution")] + public class ProcessStepExecution + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public Guid ProcessExecutionId { get; set; } + + [Required] + [MaxLength(256)] + public required string StepName { get; set; } + + [Required] + public ProcessStepComponentType ComponentType { get; set; } + + [Required] + public ProcessExecutionStatus Status { get; set; } = ProcessExecutionStatus.Running; + + [Required] + public int Attempt { get; set; } + + [Required] + public DateTime StartedDateTime { get; set; } = DateTime.UtcNow; + + public DateTime FinishedDateTime { get; set; } + + [MaxLength(2048)] + public string ErrorMessage { get; set; } + + [ForeignKey(nameof(ProcessExecutionId)) ] + public ProcessExecution ProcessExecution { get; set; } + } +} diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 5eddee6..6588275 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -21,7 +21,8 @@ namespace Musify.Infrastructure .AddStorageHandler() .AddPictureHandler() .AddMassTransitClient(configuration) - .AddScoped(); + .AddScoped() + .AddScoped(); return serviceDescriptors; } @@ -34,7 +35,8 @@ namespace Musify.Infrastructure .AddStorageHandler() .AddPictureHandler() .AddMassTransitConsumers(configuration) - .AddScoped(); + .AddScoped() + .AddScoped(); return serviceDescriptors; } diff --git a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs index 4a4479d..fd0b6b8 100644 --- a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs +++ b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs @@ -1,33 +1,60 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class RemoveFilesActivity(IStorageHandler storageHandler, ILogger logger) + public class RemoveFilesActivity( + IStorageHandler storageHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Remove-File"; public async Task Execute(ExecuteContext executeContext) { - var removeFile = await storageHandler.RemoveFileAsync( - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, executeContext.CancellationToken); - if (!removeFile.IsSuccess) - { - string errorMessage = string.Join(";", removeFile.Errors); - logger.LogWarning( - "Failed to remove file {KeyName} from bucket {BucketName}: {ErrorMessage}", - executeContext.Arguments.KeyName, - executeContext.Arguments.BucketName, - errorMessage); - } + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(RemoveFilesActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); - return executeContext.Completed(); + try + { + var removeFile = await storageHandler.RemoveFileAsync( + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + executeContext.CancellationToken); + + if (!removeFile.IsSuccess) + { + string errorMessage = string.Join(";", removeFile.Errors); + logger.LogWarning( + "Failed to remove file {KeyName} from bucket {BucketName}: {ErrorMessage}", + executeContext.Arguments.KeyName, + executeContext.Arguments.BucketName, + errorMessage); + } + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs index 5e75db4..d28292e 100644 --- a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -1,53 +1,80 @@ using MassTransit; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class ResizePictureActivity(IStorageHandler storageHandler, IPictureHandler pictureHandler) + public class ResizePictureActivity( + IStorageHandler storageHandler, + IPictureHandler pictureHandler, + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Resize-Picture"; public async Task Execute(ExecuteContext executeContext) { - var file = await storageHandler.GetFileAsync( - executeContext.Arguments.OriginalBucketName, - executeContext.Arguments.OriginalKeyName, + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, executeContext.CancellationToken); - if (!file.IsSuccess) - { - string errorMessages = string.Join(", ", file.Errors); - throw new Exception(errorMessages); - } - - var resizedPicture = await pictureHandler.ResizePictureAsync( - file.Value, - executeContext.Arguments.Width, - executeContext.Arguments.Height, + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(ResizePictureActivity), + ProcessStepComponentType.Activity, + 0, executeContext.CancellationToken); - if (!resizedPicture.IsSuccess) + try { - string errorMessage = string.Join(", ", resizedPicture.Errors); - throw new Exception(errorMessage); + var file = await storageHandler.GetFileAsync( + executeContext.Arguments.OriginalBucketName, + executeContext.Arguments.OriginalKeyName, + executeContext.CancellationToken); + + if (!file.IsSuccess) + { + string errorMessages = string.Join(", ", file.Errors); + throw new Exception(errorMessages); + } + + var resizedPicture = await pictureHandler.ResizePictureAsync( + file.Value, + executeContext.Arguments.Width, + executeContext.Arguments.Height, + executeContext.CancellationToken); + + if (!resizedPicture.IsSuccess) + { + string errorMessage = string.Join(", ", resizedPicture.Errors); + throw new Exception(errorMessage); + } + + var uploadFile = await storageHandler.UploadFileAsync( + resizedPicture.Value, + pictureHandler.ContentType, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + executeContext.CancellationToken); + + if (!uploadFile.IsSuccess) + { + var errorMessage = string.Join(", ", uploadFile.Errors); + throw new Exception(errorMessage); + } + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); } - - var uploadFile = await storageHandler.UploadFileAsync( - resizedPicture.Value, - pictureHandler.ContentType, - executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, - executeContext.CancellationToken); - - if (!uploadFile.IsSuccess) + catch (Exception exception) { - var errorMessage = string.Join(", ", uploadFile.Errors); - throw new Exception(errorMessage); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; } - - return executeContext.Completed(); } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs index 0d5ea4b..cbd4c1b 100644 --- a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -1,31 +1,53 @@ using MassTransit; -using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class UpdatePlayListPictureActivity(IDatabase database) + public class UpdatePlayListPictureActivity(IDatabase database, IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Update-PlayList-Picture"; public async Task Execute(ExecuteContext executeContext) { - var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, executeContext.CancellationToken); - if (playList is null) - throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(UpdatePlayListPictureActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); - playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; - playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; - playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + try + { + var playList = await database.PlayLists.FindAsync(executeContext.Arguments.PlayListId, executeContext.CancellationToken); - database.PlayLists.Update(playList); - await database.SaveChangesAsync(executeContext.CancellationToken); + if (playList is null) + throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); - return executeContext.Completed(); + playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + + database.PlayLists.Update(playList); + await database.SaveChangesAsync(executeContext.CancellationToken); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Filters/ProcessTrackingConsumeFilter.cs b/Infrastructure/Messaging/Filters/ProcessTrackingConsumeFilter.cs new file mode 100644 index 0000000..3df0b38 --- /dev/null +++ b/Infrastructure/Messaging/Filters/ProcessTrackingConsumeFilter.cs @@ -0,0 +1,48 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.Messaging.Filters +{ + public class ProcessTrackingConsumeFilter(IProcessTrackingStore processTrackingStore) + : IFilter> + where TMessage : class + { + public async Task Send(ConsumeContext context, IPipe> next) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + typeof(TMessage).Name, + context.CorrelationId ?? context.MessageId, + context.ConversationId, + context.MessageId, + context.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + $"{typeof(TMessage).Name}.Consume", + ProcessStepComponentType.Consumer, + 0, + context.CancellationToken); + + try + { + await next.Send(context); + await processTrackingStore.CompleteStepAsync(processId, stepId, context.CancellationToken); + } + catch (Exception exception) + { + await processTrackingStore.FailStepAsync( + processId, + stepId, + exception.Message, + context.CancellationToken); + throw; + } + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope(nameof(ProcessTrackingConsumeFilter)); + } + } +} diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index 55b85b1..ae5a99b 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Consumers; +using Musify.Infrastructure.Messaging.Filters; namespace Musify.Infrastructure.Messaging { @@ -34,6 +35,8 @@ namespace Musify.Infrastructure.Messaging { ConfigureRabbitMqHost(busFactoryConfigurator, configuration); + busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); + busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 4dd8f61..5676339 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -23,8 +23,25 @@ namespace Musify.Infrastructure.Persistence public DbSet Uploads => Set(); - public DbSet Jobs => Set(); + public DbSet ProcessExecutions => Set(); - public DbSet JobOperations => Set(); + public DbSet ProcessStepExecutions => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(process => new { process.CorrelationId, process.ProcessName }); + + modelBuilder.Entity() + .HasIndex(step => new { step.ProcessExecutionId, step.StepName }); + + modelBuilder.Entity() + .HasMany(process => process.Steps) + .WithOne(step => step.ProcessExecution) + .HasForeignKey(step => step.ProcessExecutionId) + .OnDelete(DeleteBehavior.Cascade); + + base.OnModelCreating(modelBuilder); + } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/ProcessTrackingStore.cs b/Infrastructure/Persistence/ProcessTrackingStore.cs new file mode 100644 index 0000000..3636609 --- /dev/null +++ b/Infrastructure/Persistence/ProcessTrackingStore.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.Persistence +{ + public class ProcessTrackingStore(IDatabase database) + : IProcessTrackingStore + { + public async Task GetOrCreateProcessAsync( + string processName, + Guid? correlationId, + Guid? conversationId, + Guid? messageId, + CancellationToken cancellationToken) + { + ProcessExecution? existingProcess = null; + + if (correlationId.HasValue) + { + existingProcess = await database.ProcessExecutions + .OrderByDescending(process => process.StartedDateTime) + .FirstOrDefaultAsync( + process => process.CorrelationId == correlationId && process.ProcessName == processName, + cancellationToken); + } + + if (existingProcess is not null) + { + return existingProcess.Id; + } + + var newProcess = new ProcessExecution + { + ProcessName = processName, + CorrelationId = correlationId, + ConversationId = conversationId, + MessageId = messageId, + Status = ProcessExecutionStatus.Running, + StartedDateTime = DateTime.UtcNow, + }; + + await database.ProcessExecutions.AddAsync(newProcess, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + return newProcess.Id; + } + + public async Task StartStepAsync( + Guid processExecutionId, + string stepName, + ProcessStepComponentType componentType, + int attempt, + CancellationToken cancellationToken) + { + var step = new ProcessStepExecution + { + ProcessExecutionId = processExecutionId, + StepName = stepName, + ComponentType = componentType, + Attempt = attempt, + Status = ProcessExecutionStatus.Running, + StartedDateTime = DateTime.UtcNow, + }; + + await database.ProcessStepExecutions.AddAsync(step, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + return step.Id; + } + + public async Task CompleteStepAsync(Guid processExecutionId, Guid stepExecutionId, CancellationToken cancellationToken) + { + var step = await database.ProcessStepExecutions + .SingleAsync(currentStep => currentStep.Id == stepExecutionId, cancellationToken); + + step.Status = ProcessExecutionStatus.Succeeded; + step.FinishedDateTime = DateTime.UtcNow; + + var process = await database.ProcessExecutions + .SingleAsync(currentProcess => currentProcess.Id == processExecutionId, cancellationToken); + + var hasRunningSteps = await database.ProcessStepExecutions + .AnyAsync( + currentStep => currentStep.ProcessExecutionId == processExecutionId + && currentStep.Status == ProcessExecutionStatus.Running, + cancellationToken); + + var hasFailedSteps = await database.ProcessStepExecutions + .AnyAsync( + currentStep => currentStep.ProcessExecutionId == processExecutionId + && currentStep.Status == ProcessExecutionStatus.Failed, + cancellationToken); + + if (!hasRunningSteps && !hasFailedSteps) + { + process.Status = ProcessExecutionStatus.Succeeded; + process.FinishedDateTime = DateTime.UtcNow; + process.ErrorMessage = string.Empty; + } + + await database.SaveChangesAsync(cancellationToken); + } + + public async Task FailStepAsync(Guid processExecutionId, Guid stepExecutionId, string errorMessage, CancellationToken cancellationToken) + { + var step = await database.ProcessStepExecutions + .SingleAsync(currentStep => currentStep.Id == stepExecutionId, cancellationToken); + + step.Status = ProcessExecutionStatus.Failed; + step.FinishedDateTime = DateTime.UtcNow; + step.ErrorMessage = errorMessage; + + var process = await database.ProcessExecutions + .SingleAsync(currentProcess => currentProcess.Id == processExecutionId, cancellationToken); + + process.Status = ProcessExecutionStatus.Failed; + process.FinishedDateTime = DateTime.UtcNow; + process.ErrorMessage = errorMessage; + + await database.SaveChangesAsync(cancellationToken); + } + } +} From 3c09bb89dd5bcb34725e08e31f822cce51dac904 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 21:45:36 +0100 Subject: [PATCH 052/172] Config --- .../ConfigurationDependencyInjection.cs | 3 +- .../Configuration/MessagingConfiguration.cs | 18 +++++++++ .../InfrastructureDependencyInjection.cs | 4 +- ...ion.cs => MessagingDependencyInjection.cs} | 39 ++++++++----------- 4 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 Infrastructure/Configuration/MessagingConfiguration.cs rename Infrastructure/Messaging/{MassTransitDependencyInjection.cs => MessagingDependencyInjection.cs} (72%) diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index 6a6ccde..0310508 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -11,7 +11,8 @@ namespace Musify.Infrastructure.Configuration serviceDescriptors .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) - .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName); + .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName) + .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/MessagingConfiguration.cs b/Infrastructure/Configuration/MessagingConfiguration.cs new file mode 100644 index 0000000..045ea0c --- /dev/null +++ b/Infrastructure/Configuration/MessagingConfiguration.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class MessagingConfiguration + { + public const string SectionName = "Messaging"; + + [Required] + public required string Host { get; set; } + + [Required] + public required string Username { get; set; } + + [Required] + public required string Password { get; set; } + } +} diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 6588275..64e7474 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -20,7 +20,7 @@ namespace Musify.Infrastructure .AddKeycloakService(configuration) .AddStorageHandler() .AddPictureHandler() - .AddMassTransitClient(configuration) + .AddMassTransitClient() .AddScoped() .AddScoped(); @@ -34,7 +34,7 @@ namespace Musify.Infrastructure .AddDatabase(configuration) .AddStorageHandler() .AddPictureHandler() - .AddMassTransitConsumers(configuration) + .AddMassTransitConsumers() .AddScoped() .AddScoped(); diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MessagingDependencyInjection.cs similarity index 72% rename from Infrastructure/Messaging/MassTransitDependencyInjection.cs rename to Infrastructure/Messaging/MessagingDependencyInjection.cs index ae5a99b..0613f72 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MessagingDependencyInjection.cs @@ -1,6 +1,6 @@ using MassTransit; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Consumers; @@ -8,20 +8,22 @@ using Musify.Infrastructure.Messaging.Filters; namespace Musify.Infrastructure.Messaging { - public static class MassTransitDependencyInjection + public static class MessagingDependencyInjection { - public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors) { serviceDescriptors.AddMassTransit(options => { - options.UsingRabbitMq((_, busFactoryConfigurator) => - ConfigureRabbitMqHost(busFactoryConfigurator, configuration)); + options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => + ConfigureRabbitMqHost( + busFactoryConfigurator, + busRegistrationContext.GetRequiredService())); } ); return serviceDescriptors; } - public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors) { serviceDescriptors.AddMassTransit(options => { @@ -33,7 +35,9 @@ namespace Musify.Infrastructure.Messaging options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { - ConfigureRabbitMqHost(busFactoryConfigurator, configuration); + ConfigureRabbitMqHost( + busFactoryConfigurator, + busRegistrationContext.GetRequiredService()); busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); @@ -67,23 +71,14 @@ namespace Musify.Infrastructure.Messaging return serviceDescriptors; } - static void ConfigureRabbitMqHost(IRabbitMqBusFactoryConfigurator busFactoryConfigurator, IConfiguration configuration) + static void ConfigureRabbitMqHost( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + MessagingConfiguration massTransitConfiguration) { - var section = configuration.GetSection("MassTransit"); - - var host = section["Host"] - ?? throw new InvalidOperationException("MassTransit:Host configuration value not found."); - - var username = section["Username"] - ?? throw new InvalidOperationException("MassTransit:Username configuration value not found."); - - var password = section["Password"] - ?? throw new InvalidOperationException("MassTransit:Password configuration value not found."); - - busFactoryConfigurator.Host(host, options => + busFactoryConfigurator.Host(massTransitConfiguration.Host, options => { - options.Username(username); - options.Password(password); + options.Username(massTransitConfiguration.Username); + options.Password(massTransitConfiguration.Password); } ); } From d38c0cf4a887ea4cba930c22df19dffc6517396a Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 22:18:37 +0100 Subject: [PATCH 053/172] OT --- Infrastructure/Infrastructure.csproj | 1 + .../InfrastructureDependencyInjection.cs | 3 +++ .../OpenTelemetryDependencyInjection.cs | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 Infrastructure/Observability/OpenTelemetryDependencyInjection.cs diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 3bc091b..a813415 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -22,6 +22,7 @@ + diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 64e7474..8923781 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -4,6 +4,7 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Identity; using Musify.Infrastructure.Messaging; +using Musify.Infrastructure.Observability; using Musify.Infrastructure.Persistence; using Musify.Infrastructure.Pictures; using Musify.Infrastructure.Storage; @@ -20,6 +21,7 @@ namespace Musify.Infrastructure .AddKeycloakService(configuration) .AddStorageHandler() .AddPictureHandler() + .AddInfrastructureOpenTelemetry() .AddMassTransitClient() .AddScoped() .AddScoped(); @@ -34,6 +36,7 @@ namespace Musify.Infrastructure .AddDatabase(configuration) .AddStorageHandler() .AddPictureHandler() + .AddInfrastructureOpenTelemetry() .AddMassTransitConsumers() .AddScoped() .AddScoped(); diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs new file mode 100644 index 0000000..4618185 --- /dev/null +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Musify.Infrastructure.Observability +{ + public static class OpenTelemetryDependencyInjection + { + public static IServiceCollection AddInfrastructureOpenTelemetry(this IServiceCollection serviceDescriptors) + { + serviceDescriptors + .AddOpenTelemetry() + .WithTracing(tracing => + { + tracing.AddSource("MassTransit"); + } ) + .WithMetrics(metrics => + { + metrics.AddMeter("MassTransit"); + } ); + + return serviceDescriptors; + } + } +} From 57557ec51a5b7ff6d59c9c194d77994c287d8353 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 22:28:32 +0100 Subject: [PATCH 054/172] Grafana --- .../ConfigurationDependencyInjection.cs | 3 ++- .../Configuration/OpenTelemetryConfiguration.cs | 13 +++++++++++++ Infrastructure/Infrastructure.csproj | 1 + .../OpenTelemetryDependencyInjection.cs | 15 ++++++++++++++- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 Infrastructure/Configuration/OpenTelemetryConfiguration.cs diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index 0310508..10a6be7 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -12,7 +12,8 @@ namespace Musify.Infrastructure.Configuration .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName) - .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName); + .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName) + .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/OpenTelemetryConfiguration.cs b/Infrastructure/Configuration/OpenTelemetryConfiguration.cs new file mode 100644 index 0000000..ad7a232 --- /dev/null +++ b/Infrastructure/Configuration/OpenTelemetryConfiguration.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class OpenTelemetryConfiguration + { + public const string SectionName = "OpenTelemetry"; + + [Required] + [Url] + public required string OtlpEndpoint { get; set; } + } +} diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index a813415..d26a09e 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -22,6 +22,7 @@ + diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs index 4618185..67b30d5 100644 --- a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -1,4 +1,8 @@ using Microsoft.Extensions.DependencyInjection; +using Musify.Infrastructure.Configuration; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; namespace Musify.Infrastructure.Observability { @@ -6,18 +10,27 @@ namespace Musify.Infrastructure.Observability { public static IServiceCollection AddInfrastructureOpenTelemetry(this IServiceCollection serviceDescriptors) { + serviceDescriptors + .AddOptions() + .Configure((exporterOptions, openTelemetryConfiguration) => + { + exporterOptions.Endpoint = new Uri(openTelemetryConfiguration.OtlpEndpoint); + } ); + serviceDescriptors .AddOpenTelemetry() .WithTracing(tracing => { tracing.AddSource("MassTransit"); + tracing.AddOtlpExporter(); } ) .WithMetrics(metrics => { metrics.AddMeter("MassTransit"); + metrics.AddOtlpExporter(); } ); return serviceDescriptors; } } -} +} \ No newline at end of file From 8be0f8656077aefa2bfb31a29a8f9a2dba1b0b10 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 23:24:32 +0100 Subject: [PATCH 055/172] Nullable --- Domain/Entities/ProcessExecution.cs | 4 ++-- Domain/Entities/ProcessStepExecution.cs | 4 ++-- Infrastructure/Storage/StorageHandler.cs | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Domain/Entities/ProcessExecution.cs b/Domain/Entities/ProcessExecution.cs index b0bbf21..cc6df6f 100644 --- a/Domain/Entities/ProcessExecution.cs +++ b/Domain/Entities/ProcessExecution.cs @@ -26,10 +26,10 @@ namespace Musify.Domain.Entities [Required] public DateTime StartedDateTime { get; set; } = DateTime.UtcNow; - public DateTime FinishedDateTime { get; set; } + public DateTime? FinishedDateTime { get; set; } [MaxLength(2048)] - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } public ICollection Steps { get; set; } = new List(); } diff --git a/Domain/Entities/ProcessStepExecution.cs b/Domain/Entities/ProcessStepExecution.cs index a1d01cc..0847ecf 100644 --- a/Domain/Entities/ProcessStepExecution.cs +++ b/Domain/Entities/ProcessStepExecution.cs @@ -29,10 +29,10 @@ namespace Musify.Domain.Entities [Required] public DateTime StartedDateTime { get; set; } = DateTime.UtcNow; - public DateTime FinishedDateTime { get; set; } + public DateTime? FinishedDateTime { get; set; } [MaxLength(2048)] - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } [ForeignKey(nameof(ProcessExecutionId)) ] public ProcessExecution ProcessExecution { get; set; } diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index edacf15..1968b9f 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -29,9 +29,9 @@ namespace Musify.Infrastructure.Storage logger.LogDebug("Successfully got file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); return Result.Success(response.ResponseStream); } - catch + catch (Exception exception) { - logger.LogDebug("Failed to get file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogDebug(exception, "Failed to get file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); return Result.NotFound(); } } @@ -92,9 +92,9 @@ namespace Musify.Infrastructure.Storage upload.State = UploadState.Successful; logger.LogInformation("Successfully transferred file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); } - catch + catch (Exception exception) { - logger.LogError("Failed to transfer file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); + logger.LogError(exception, "Failed to transfer file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); upload.State = UploadState.Failed; } } @@ -129,9 +129,9 @@ namespace Musify.Infrastructure.Storage var response = await amazonS3.PutObjectAsync(request, cancellationToken); logger.LogInformation("Successfully uploaded file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); } - catch + catch (Exception exception) { - logger.LogError("Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogError(exception, "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}"); } @@ -161,9 +161,9 @@ namespace Musify.Infrastructure.Storage sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); return Result.Success(); } - catch + catch (Exception exception) { - logger.LogError("Failed to copy file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", + logger.LogError(exception, "Failed to copy file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); return Result.Error($"Failed to copy file from S3 with bucket name {sourceBucketName} and key name {sourceKeyName} to bucket name {destinationBucketName} and key name {destinationKeyName}"); } From 68ef19dd6ee3159316080c1592faeb9022105f49 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 23:26:49 +0100 Subject: [PATCH 056/172] Message --- Infrastructure/Storage/StorageHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index 1968b9f..edec90c 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -32,7 +32,7 @@ namespace Musify.Infrastructure.Storage catch (Exception exception) { logger.LogDebug(exception, "Failed to get file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); - return Result.NotFound(); + return Result.NotFound($"File not found in S3 with bucket name {bucketName} and key name {keyName}"); } } From 32d4c21dd7f3a0738d75c859a5036b40102ed0a2 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 00:04:19 +0100 Subject: [PATCH 057/172] Routes --- .../Events/UpdatePlayListPictureEvent.cs | 12 +++- .../CreatePlayListCommandHandler.cs | 27 ++++++++- .../UpdatePlayListCommandHandler.cs | 27 ++++++++- .../UpdatePlayListPictureConsumer.cs | 57 ++++++++----------- 4 files changed, 85 insertions(+), 38 deletions(-) diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs index 258c790..f5590d9 100644 --- a/Application/Events/UpdatePlayListPictureEvent.cs +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -2,5 +2,15 @@ { public record UpdatePlayListPictureEvent( Guid PlayListId, - string OriginalPictureKeyName); + string BucketName, + string OriginalPictureKeyName, + string SmallPictureKeyName, + int SmallPictureWidth, + int SmallPictureHeight, + string MediumPictureKeyName, + int MediumPictureWidth, + int MediumPictureHeight, + string LargePictureKeyName, + int LargePictureWidth, + int LargePictureHeight); } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index c5bad12..bea11d8 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -11,7 +11,8 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands.CreatePlayList { public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, - ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) + ILogger logger, ApplicationStorageConfiguration storageConfiguration, + PlayListConfiguration playListConfiguration, IPictureHandler pictureHandler) : IRequestHandler> > { public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) @@ -58,9 +59,31 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList playList.OriginalPictureKeyName = pictureName; logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); + var smallPictureKeyName = Path.Combine( + playListConfiguration.Routes.SmallPictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + playListConfiguration.Routes.MediumPictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + playListConfiguration.Routes.LargePictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + try { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + originalImageStorageKey, + smallPictureKeyName, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + mediumPictureKeyName, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + largePictureKeyName, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 8225c26..3429cfd 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -10,7 +10,8 @@ using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Commands.UpdatePlayList { public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, - ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) + ILogger logger, ApplicationStorageConfiguration storageConfiguration, + PlayListConfiguration playListConfiguration, IPictureHandler pictureHandler) : IRequestHandler> > { public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) @@ -131,9 +132,31 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList playList.OriginalPictureKeyName = pictureName; + var smallPictureKeyName = Path.Combine( + playListConfiguration.Routes.SmallPictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + playListConfiguration.Routes.MediumPictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + playListConfiguration.Routes.LargePictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + try { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + newImageStorageKey, + smallPictureKeyName, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + mediumPictureKeyName, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + largePictureKeyName, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } catch (Exception exception) { diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index a5deed2..ccf6b9a 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -1,14 +1,11 @@ using MassTransit; -using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Consumers { - public class UpdatePlayListPictureConsumer(IBus bus, IPictureHandler pictureHandler, - ApplicationStorageConfiguration storageSettings, PlayListConfiguration playListConfiguration) + public class UpdatePlayListPictureConsumer(IBus bus) : IConsumer { public const string QueueName = "Update-PlayList-Picture"; @@ -17,53 +14,46 @@ namespace Musify.Infrastructure.Messaging.Consumers { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - var smallPictureKeyName = Path.Combine( - playListConfiguration.Routes.SmallPictures, - $"{consumeContext.Message.PlayListId}_small.{pictureHandler.FileExtension}"); - var mediumPictureKeyName = Path.Combine( - playListConfiguration.Routes.MediumPictures, - $"{consumeContext.Message.PlayListId}_medium.{pictureHandler.FileExtension}"); - var largePictureKeyName = Path.Combine( - playListConfiguration.Routes.LargePictures, - $"{consumeContext.Message.PlayListId}_large.{pictureHandler.FileExtension}"); - AddResizeActivity( routingSlipBuilder, "ResizeSmall", - smallPictureKeyName, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - consumeContext.Message.OriginalPictureKeyName); + consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.SmallPictureWidth, + consumeContext.Message.SmallPictureHeight, + consumeContext.Message.OriginalPictureKeyName, + consumeContext.Message.BucketName); AddResizeActivity( routingSlipBuilder, "ResizeMedium", - mediumPictureKeyName, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - consumeContext.Message.OriginalPictureKeyName); + consumeContext.Message.MediumPictureKeyName, + consumeContext.Message.MediumPictureWidth, + consumeContext.Message.MediumPictureHeight, + consumeContext.Message.OriginalPictureKeyName, + consumeContext.Message.BucketName); AddResizeActivity( routingSlipBuilder, "ResizeLarge", - largePictureKeyName, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - consumeContext.Message.OriginalPictureKeyName); + consumeContext.Message.LargePictureKeyName, + consumeContext.Message.LargePictureWidth, + consumeContext.Message.LargePictureHeight, + consumeContext.Message.OriginalPictureKeyName, + consumeContext.Message.BucketName); routingSlipBuilder.AddActivity( "DeleteOriginal", BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), - new RemoveFileArguments(storageSettings.BucketName, consumeContext.Message.OriginalPictureKeyName)); + new RemoveFileArguments(consumeContext.Message.BucketName, consumeContext.Message.OriginalPictureKeyName)); routingSlipBuilder.AddActivity( "UpdatePlayListPicture", BuildExecuteUri(UpdatePlayListPictureActivity.ExecuteEndpointName), new UpdatePlayListPictureArguments( consumeContext.Message.PlayListId, - smallPictureKeyName, - mediumPictureKeyName, - largePictureKeyName)); + consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.MediumPictureKeyName, + consumeContext.Message.LargePictureKeyName)); var routingSlip = routingSlipBuilder.Build(); await bus.Execute(routingSlip); @@ -75,15 +65,16 @@ namespace Musify.Infrastructure.Messaging.Consumers string destinationKeyName, int width, int height, - string originalPictureKeyName) + string originalPictureKeyName, + string bucketName) { routingSlipBuilder.AddActivity( activityName, BuildExecuteUri(ResizePictureActivity.ExecuteEndpointName), new ResizePictureArgument( - storageSettings.BucketName, + bucketName, originalPictureKeyName, - storageSettings.BucketName, + bucketName, destinationKeyName, width, height)); From 4a78455947d53f1e712ebf8de164651dc26d26a6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 03:45:29 +0100 Subject: [PATCH 058/172] A lot of changes --- Application/Application.csproj | 4 + .../ConfigurationDependencyInjection.cs | 3 +- .../Configuration/PlayListConfiguration.cs | 11 ++- .../Configuration/TrackConfiguration.cs | 63 +++++++++++++ .../Contracts/Infrastructure/IDatabase.cs | 2 + Application/Events/ProcessTrackEvent.cs | 4 + Application/Events/RemoveFileEvent.cs | 4 +- Application/Events/UpdateTrackPictureEvent.cs | 16 ++++ .../CreatePlayListCommandHandler.cs | 7 +- .../DeletePlayListCommandHandler.cs | 44 ++++------ .../UpdatePlayListCommandHandler.cs | 76 ++++++---------- .../CreateTrack/CreateTrackCommand.cs | 14 +++ .../CreateTrack/CreateTrackCommandHandler.cs | 87 ++++++++++++++++++ Application/Tracks/Contracts/TrackResponse.cs | 20 +++++ Domain/Entities/PlayList.cs | 3 - Domain/Entities/Track.cs | 12 ++- Domain/Entities/User.cs | 2 +- Domain/Entities/UserHasTrack.cs | 24 +++++ .../Arguments/UpdateTrackPictureArguments.cs | 8 ++ .../Activities/UpdateTrackPictureActivity.cs | 53 +++++++++++ .../UpdatePlayListPictureConsumer.cs | 5 +- .../Consumers/UpdateTrackPictureConsumer.cs | 88 +++++++++++++++++++ Infrastructure/Persistence/Database.cs | 2 + 23 files changed, 456 insertions(+), 96 deletions(-) create mode 100644 Application/Configuration/TrackConfiguration.cs create mode 100644 Application/Events/ProcessTrackEvent.cs create mode 100644 Application/Events/UpdateTrackPictureEvent.cs create mode 100644 Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs create mode 100644 Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs create mode 100644 Application/Tracks/Contracts/TrackResponse.cs create mode 100644 Domain/Entities/UserHasTrack.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs create mode 100644 Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs create mode 100644 Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index cde91d6..0a86904 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs index be5b37d..b82a141 100644 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -10,7 +10,8 @@ namespace Musify.Application.Configuration { serviceDescriptors .AddValidatedConfiguration(configuration, ApplicationStorageConfiguration.SectionName) - .AddValidatedConfiguration(configuration, PlayListConfiguration.SectionName); + .AddValidatedConfiguration(configuration, PlayListConfiguration.SectionName) + .AddValidatedConfiguration(configuration, TrackConfiguration.SectionName); return serviceDescriptors; } diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 18707cd..4effc04 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -7,14 +7,17 @@ namespace Musify.Application.Configuration public const string SectionName = "PlayListConfiguration"; [Required] - public Routes Routes { get; set; } = new Routes(); + public PlayListRoutes Routes { get; set; } = new PlayListRoutes(); [Required] - public PicturesSizes PicturesSizes { get; set; } = new PicturesSizes(); + public PlayListPicturesSizes PicturesSizes { get; set; } = new PlayListPicturesSizes(); } - public class Routes + public class PlayListRoutes { + [Required] + public string ParentFolders { get; set; } = "PlayLists"; + [Required] public string OriginalPictures { get; set; } = "OriginalPictures"; @@ -37,7 +40,7 @@ namespace Musify.Application.Configuration public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; } - public class PicturesSizes + public class PlayListPicturesSizes { [Range(1, 1024)] public int SmallPictureWidth { get; set; } = 128; diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs new file mode 100644 index 0000000..298a66a --- /dev/null +++ b/Application/Configuration/TrackConfiguration.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Application.Configuration +{ + public class TrackConfiguration + { + public const string SectionName = "TrackConfiguration"; + + [Required] + public TrackRoutes Routes { get; set; } = new TrackRoutes(); + + [Required] + public TrackPicturesSizes PicturesSizes { get; set; } = new TrackPicturesSizes(); + } + + public class TrackRoutes + { + [Required] + public string ParentFolder { get; set; } = "Tracks"; + + [Required] + public string OriginalPictures { get; set; } = "OriginalTracks"; + + [Required] + public string SmallPictures { get; set; } = "SmallTracks"; + + [Required] + public string MediumPictures { get; set; } = "MediumTracks"; + + [Required] + public string LargePictures { get; set; } = "LargeTracks"; + + [Required] + public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp"; + + [Required] + public string PresetMediumPicture { get; set; } = "PresetMediumPicture.webp"; + + [Required] + public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; + } + + public class TrackPicturesSizes + { + [Range(1, 1024)] + public int SmallPictureWidth { get; set; } = 128; + + [Range(1, 1024)] + public int SmallPictureHeight { get; set; } = 128; + + [Range(1, 1024)] + public int MediumPictureWidth { get; private set; } = 256; + + [Range(1, 1024)] + public int MediumPictureHeight { get; private set; } = 256; + + [Range(1, 1024)] + public int LargePictureWidth { get; private set; } = 512; + + [Range(1, 1024)] + public int LargePictureHeight { get; private set; } = 512; + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index ab56211..9f49cf4 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -11,6 +11,8 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Tracks { get; } + DbSet UserHasTracks { get; } + DbSet PlayListHasTracks { get; } DbSet Uploads { get; } diff --git a/Application/Events/ProcessTrackEvent.cs b/Application/Events/ProcessTrackEvent.cs new file mode 100644 index 0000000..30899f2 --- /dev/null +++ b/Application/Events/ProcessTrackEvent.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record ProcessTrackEvent(); +} \ No newline at end of file diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs index 88cbd0e..76fc108 100644 --- a/Application/Events/RemoveFileEvent.cs +++ b/Application/Events/RemoveFileEvent.cs @@ -1,6 +1,4 @@ -using Musify.Application.Contracts.Application; - -namespace Musify.Application.Events +namespace Musify.Application.Events { public record RemoveFileEvent( string BucketName, diff --git a/Application/Events/UpdateTrackPictureEvent.cs b/Application/Events/UpdateTrackPictureEvent.cs new file mode 100644 index 0000000..ada8d60 --- /dev/null +++ b/Application/Events/UpdateTrackPictureEvent.cs @@ -0,0 +1,16 @@ +namespace Musify.Application.Events +{ + public record UpdateTrackPictureEvent( + Guid TrackId, + string BucketName, + string OriginalPictureKeyName, + string SmallPictureKeyName, + int SmallPictureWidth, + int SmallPictureHeight, + string MediumPictureKeyName, + int MediumPictureWidth, + int MediumPictureHeight, + string LargePictureKeyName, + int LargePictureWidth, + int LargePictureHeight); +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index bea11d8..3d4a6f2 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -10,9 +10,8 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands.CreatePlayList { - public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, - ILogger logger, ApplicationStorageConfiguration storageConfiguration, - PlayListConfiguration playListConfiguration, IPictureHandler pictureHandler) + public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, IPictureHandler pictureHandler, + ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) @@ -55,8 +54,6 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList playList.Id, string.Join("; ", uploadResult.Errors)); return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } - - playList.OriginalPictureKeyName = pictureName; logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); var smallPictureKeyName = Path.Combine( diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index 8853bdd..fc03dd8 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -27,36 +27,28 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList return Result.Unauthorized(); } - if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) + var keysToRemove = new List(); + + if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) { - var keysToRemove = new List - { - Path.Join(playListConfiguration.Routes.OriginalPictures, playList.OriginalPictureKeyName) - }; + keysToRemove.Add(playList.SmallPictureKeyName); + } + if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + { + keysToRemove.Add(playList.MediumPictureKeyName); + } + if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + { + keysToRemove.Add(playList.LargePictureKeyName); + } - if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) - { - keysToRemove.Add(playList.SmallPictureKeyName); - } + foreach (var key in keysToRemove) + { + var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); - if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + if (!publishResult.IsSuccess) { - keysToRemove.Add(playList.MediumPictureKeyName); - } - - if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - { - keysToRemove.Add(playList.LargePictureKeyName); - } - - foreach (var key in keysToRemove) - { - var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); - - if (!publishResult.IsSuccess) - { - return publishResult; - } + return publishResult; } } diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 3429cfd..e58e352 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -47,68 +47,46 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList return Result.Success(PlayListResponse.FromEntity(playList)); } - if (!string.IsNullOrWhiteSpace(playList.OriginalPictureKeyName)) + if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) { - var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, playList.OriginalPictureKeyName); - try { await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - originalImageStorageKey), cancellationToken); + storageConfiguration.BucketName, + playList.SmallPictureKeyName), cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for original picture of PlayList id={PlayListId}.", playList.Id); + logger.LogError(exception, "Failed to publish remove file event for small picture of PlayList id={PlayListId}.", playList.Id); return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); } - - logger.LogInformation("Published event to remove old picture with key name {KeyName} from bucket {BucketName}", - originalImageStorageKey, storageConfiguration.BucketName); - - if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + } + if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + { + try { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for small picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + await eventBus.PublishAsync(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.MediumPictureKeyName), cancellationToken); } - - if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + catch (Exception exception) { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for medium picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + logger.LogError(exception, "Failed to publish remove file event for medium picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); } - - if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + } + if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + { + try { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for large picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + await eventBus.PublishAsync(new RemoveFileEvent( + storageConfiguration.BucketName, + playList.LargePictureKeyName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for large picture of PlayList id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); } } @@ -130,8 +108,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } - playList.OriginalPictureKeyName = pictureName; - var smallPictureKeyName = Path.Combine( playListConfiguration.Routes.SmallPictures, $"{playList.Id}.{pictureHandler.FileExtension}"); diff --git a/Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs new file mode 100644 index 0000000..c10007c --- /dev/null +++ b/Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs @@ -0,0 +1,14 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; +using Musify.Application.Tracks.Contracts; + +namespace Musify.Application.Tracks.Commands.CreateTrack +{ + public record CreateTrackCommand( + Guid UserId, + string Title, + IFileData Picture, + IFileData Audio) + : IRequest >>; +} \ No newline at end of file diff --git a/Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs b/Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs new file mode 100644 index 0000000..2b0a745 --- /dev/null +++ b/Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs @@ -0,0 +1,87 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Events; +using Musify.Application.Tracks.Contracts; +using Musify.Domain.Entities; + +namespace Musify.Application.Tracks.Commands.CreateTrack +{ + public class CreateTrackCommandHandler(IDatabase database, IPictureHandler pictureHandler, IEventBus eventBus, IStorageHandler storageHandler, + ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) + : IRequestHandler >> + { + public async Task> Handle(CreateTrackCommand request, CancellationToken cancellationToken) + { + var userExists = await database.Users.FindAsync(request.UserId, cancellationToken); + + if (userExists is null) + { + logger.LogWarning("User with id {UserId} not found", request.UserId); + return Result.NotFound($"User with id {request.UserId} not found"); + } + + var originalPictureKeyName = $"{Guid.NewGuid() }.{request.Picture.FileType}"; + var track = new Track + { + Title = request.Title, + NormalizedTitle = request.Title.Trim().ToUpperInvariant(), + SmallPictureKeyName = trackConfiguration.Routes.PresetSmallPicture, + MediumPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, + LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, + }; + + var pictureUploadResult = await storageHandler.UploadFileAsync( + request.Picture.FileStream, + request.Picture.ContentType, + storageConfiguration.BucketName, + originalPictureKeyName, + cancellationToken); + + if (!pictureUploadResult.IsSuccess) + { + logger.LogError("Failed to upload picture for track {TrackTitle}", request.Title); + return Result.Error($"Failed to upload picture for track {request.Title}"); + } + + var smallPictureKeyName = Path.Combine( + trackConfiguration.Routes.SmallPictures, + $"{track.Id}.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + trackConfiguration.Routes.MediumPictures, + $"{track.Id}.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + trackConfiguration.Routes.LargePictures, + $"{track.Id}.{pictureHandler.FileExtension}"); + + try + { + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + track.Id, + storageConfiguration.BucketName, + originalPictureKeyName, + smallPictureKeyName, + trackConfiguration.PicturesSizes.SmallPictureWidth, + trackConfiguration.PicturesSizes.SmallPictureHeight, + mediumPictureKeyName, + trackConfiguration.PicturesSizes.MediumPictureWidth, + trackConfiguration.PicturesSizes.MediumPictureHeight, + largePictureKeyName, + trackConfiguration.PicturesSizes.LargePictureWidth, + trackConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish resize picture event for Track with id={TrackId}.", track.Id); + return Result.Error($"Failed to publish resize picture event for Track with id {track.Id}."); + } + + await database.Tracks.AddAsync(track, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + + return Result.Success(); + } + } +} diff --git a/Application/Tracks/Contracts/TrackResponse.cs b/Application/Tracks/Contracts/TrackResponse.cs new file mode 100644 index 0000000..8a0a67e --- /dev/null +++ b/Application/Tracks/Contracts/TrackResponse.cs @@ -0,0 +1,20 @@ +using Musify.Domain.Entities; + +namespace Musify.Application.Tracks.Contracts +{ + public record TrackResponse( + Guid Id, + string Title, + DateTime CreatedDate, + DateTime UpdatedDate) + { + public static TrackResponse FromEntity(Track track) + { + return new TrackResponse( + track.Id, + track.Title, + track.CreatedDate, + track.UpdatedDate); + } + } +} \ No newline at end of file diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 7975558..44d751f 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -25,9 +25,6 @@ namespace Musify.Domain.Entities [MaxLength(254)] public required string Description { get; set; } = "No description was provided."; - [MaxLength(64)] - public string? OriginalPictureKeyName { get; set; } - [Required] [MaxLength(64)] public required string SmallPictureKeyName { get; set; } diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index 6fae133..8986fdd 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -18,7 +18,15 @@ namespace Musify.Domain.Entities [Required] [MaxLength(64)] - public required string ImageName { get; set; } + public required string SmallPictureKeyName { get; set; } + + [Required] + [MaxLength(64)] + public required string MediumPictureKeyName { get; set; } + + [Required] + [MaxLength(64)] + public required string LargePictureKeyName { get; set; } [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; @@ -26,7 +34,7 @@ namespace Musify.Domain.Entities [Required] public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; - public ICollection Users { get; set; } = new List(); + public ICollection UserTracks { get; set; } = new List(); public ICollection PlayListTracks { get; set; } = new List(); } diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 08784e3..2703c2a 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -15,7 +15,7 @@ namespace Musify.Domain.Entities [Required] public DateTime UpdatedAt { get; set; } = DateTime.Now; - public ICollection Tracks { get; set; } = new List(); + public ICollection UserTracks { get; set; } = new List(); public ICollection PlayLists { get; set; } = new List(); } diff --git a/Domain/Entities/UserHasTrack.cs b/Domain/Entities/UserHasTrack.cs new file mode 100644 index 0000000..fbf0d07 --- /dev/null +++ b/Domain/Entities/UserHasTrack.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("UserHasTrack")] + public class UserHasTrack + { + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required Guid UserId { get; set; } + + [Required] + public required Guid TrackId { get; set; } + + [ForeignKey(nameof(UserId)) ] + public User User { get; set; } + + [ForeignKey(nameof(TrackId)) ] + public Track Track { get; set; } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs b/Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs new file mode 100644 index 0000000..be1106f --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record UpdateTrackPictureArguments( + Guid TrackId, + string SmallPictureKeyName, + string MediumPictureKeyName, + string LargePictureKeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs new file mode 100644 index 0000000..63671de --- /dev/null +++ b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs @@ -0,0 +1,53 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class UpdateTrackPictureActivity(IDatabase database, IProcessTrackingStore processTrackingStore) + : IExecuteActivity + { + public const string ExecuteEndpointName = "Update-Track-Picture"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(UpdateTrackPictureActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + var track = await database.Tracks.FindAsync(executeContext.Arguments.TrackId, executeContext.CancellationToken); + + if (track is null) + throw new Exception($"Track with id {executeContext.Arguments.TrackId} not found."); + + track.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + track.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + track.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + + database.Tracks.Update(track); + await database.SaveChangesAsync(executeContext.CancellationToken); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index ccf6b9a..e542bc0 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -44,7 +44,10 @@ namespace Musify.Infrastructure.Messaging.Consumers routingSlipBuilder.AddActivity( "DeleteOriginal", BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), - new RemoveFileArguments(consumeContext.Message.BucketName, consumeContext.Message.OriginalPictureKeyName)); + new RemoveFileArguments( + consumeContext.Message.BucketName, + consumeContext.Message.OriginalPictureKeyName) + ); routingSlipBuilder.AddActivity( "UpdatePlayListPicture", diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs new file mode 100644 index 0000000..00e0911 --- /dev/null +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -0,0 +1,88 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class UpdateTrackPictureConsumer(IBus bus) + : IConsumer + { + public const string QueueName = "update-track-picture"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + AddResizeActivity( + routingSlipBuilder, + "ResizeSmall", + consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.SmallPictureWidth, + consumeContext.Message.SmallPictureHeight, + consumeContext.Message.OriginalPictureKeyName, + consumeContext.Message.BucketName); + + AddResizeActivity( + routingSlipBuilder, + "ResizeMedium", + consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.MediumPictureWidth, + consumeContext.Message.MediumPictureHeight, + consumeContext.Message.OriginalPictureKeyName, + consumeContext.Message.BucketName); + + AddResizeActivity( + routingSlipBuilder, + "ResizeMedium", + consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.LargePictureWidth, + consumeContext.Message.LargePictureHeight, + consumeContext.Message.OriginalPictureKeyName, + consumeContext.Message.BucketName); + + routingSlipBuilder.AddActivity( + "DeleteOriginal", + BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), + new RemoveFileArguments( + consumeContext.Message.BucketName, + consumeContext.Message.OriginalPictureKeyName) + ); + + routingSlipBuilder.AddActivity( + "UpdateTrackPictureActivity", + BuildExecuteUri(UpdateTrackPictureActivity.ExecuteEndpointName), + new UpdateTrackPictureArguments( + consumeContext.Message.TrackId, + consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.MediumPictureKeyName, + consumeContext.Message.LargePictureKeyName)); + + var routingSlip = routingSlipBuilder.Build(); + await bus.Execute(routingSlip); + } + + void AddResizeActivity( + RoutingSlipBuilder routingSlipBuilder, + string activityName, + string destinationKeyName, + int width, + int height, + string originalPictureKeyName, + string bucketName) + { + routingSlipBuilder.AddActivity( + activityName, + BuildExecuteUri(ResizePictureActivity.ExecuteEndpointName), + new ResizePictureArgument( + bucketName, + originalPictureKeyName, + bucketName, + destinationKeyName, + width, + height)); + } + + static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); + } +} \ No newline at end of file diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 5676339..993fe77 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -19,6 +19,8 @@ namespace Musify.Infrastructure.Persistence public DbSet PlayLists => Set(); + public DbSet UserHasTracks => Set(); + public DbSet PlayListHasTracks => Set(); public DbSet Uploads => Set(); From 8306211f499f6475dd67e6e6c44f767d1d5671c5 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 04:00:12 +0100 Subject: [PATCH 059/172] Queries y algunos cambios --- Application/Application.csproj | 4 -- .../Application/PaginatedResponse.cs | 11 +++++ .../Contracts/PaginatedPlayListResponse.cs | 12 ----- .../Queries/GetPlayLists/GetPlayListsQuery.cs | 3 +- .../GetPlayLists/GetPlayListsQueryHandler.cs | 8 ++-- .../GetPlayListsByUserIdQuery.cs | 3 +- .../GetPlayListsByUserIdQueryHandler.cs | 7 +-- .../Queries/GetTrackById/GetTrackByIdQuery.cs | 9 ++++ .../GetTrackById/GetTrackByIdQueryHandler.cs | 20 ++++++++ .../Queries/GetTracks/GetTracksQuery.cs | 10 ++++ .../GetTracks/GetTracksQueryHandler.cs | 40 ++++++++++++++++ .../GetTracksByUserIdQuery.cs | 14 ++++++ .../GetTracksByUserIdQueryHandler.cs | 47 +++++++++++++++++++ 13 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 Application/Contracts/Application/PaginatedResponse.cs delete mode 100644 Application/PlayLists/Contracts/PaginatedPlayListResponse.cs create mode 100644 Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs create mode 100644 Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs create mode 100644 Application/Tracks/Queries/GetTracks/GetTracksQuery.cs create mode 100644 Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs create mode 100644 Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs create mode 100644 Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index 0a86904..cde91d6 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -24,8 +24,4 @@ - - - - diff --git a/Application/Contracts/Application/PaginatedResponse.cs b/Application/Contracts/Application/PaginatedResponse.cs new file mode 100644 index 0000000..8106a4b --- /dev/null +++ b/Application/Contracts/Application/PaginatedResponse.cs @@ -0,0 +1,11 @@ +namespace Musify.Application.Contracts.Application +{ + public record PaginatedResponse( + IReadOnlyCollection Items, + int PageNumber, + int PageSize, + int PageCount, + int TotalItemCount, + bool HasNextPage, + bool HasPreviousPage); +} diff --git a/Application/PlayLists/Contracts/PaginatedPlayListResponse.cs b/Application/PlayLists/Contracts/PaginatedPlayListResponse.cs deleted file mode 100644 index 1431574..0000000 --- a/Application/PlayLists/Contracts/PaginatedPlayListResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Musify.Application.PlayLists.Contracts -{ - public record PaginatedPlayListResponse( - IReadOnlyCollection PlayLists, - int PageNumber, - int PageSize, - int PageCount, - int TotalItemCount, - bool HasNextPage, - bool HasPreviousPage - ); -} \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs index ba213f7..e5de1e1 100644 --- a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs +++ b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs @@ -1,9 +1,10 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Queries.GetPlayLists { public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10) - : IRequest>>; + : IRequest> >>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs index ecda141..8a5835b 100644 --- a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs @@ -1,17 +1,17 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Contracts; -using Musify.Domain.Entities; using X.PagedList.EF; namespace Musify.Application.PlayLists.Queries.GetPlayLists { public class GetPlayListsQueryHandler(IDatabase database) - : IRequestHandler> > + : IRequestHandler >> > { - public async Task> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) + public async Task >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -24,7 +24,7 @@ namespace Musify.Application.PlayLists.Queries.GetPlayLists .Select(p => PlayListResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var response = new PaginatedPlayListResponse( + var response = new PaginatedResponse( pagedPlayLists.ToArray(), pagedPlayLists.PageNumber, pagedPlayLists.PageSize, diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs index efeb755..b90358a 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Contracts; namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId @@ -9,5 +10,5 @@ namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId string Name = "", int PageNumber = 1, int PageSize = 10) - : IRequest>>; + : IRequest> >>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs index 58403b4..a54bf3d 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs @@ -1,6 +1,7 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Contracts; using X.PagedList.EF; @@ -8,9 +9,9 @@ using X.PagedList.EF; namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId { public class GetPlayListsByUserIdQueryHandler(IDatabase database) - : IRequestHandler> > + : IRequestHandler > >> { - public async Task> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) + public async Task >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -30,7 +31,7 @@ namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId .Select(p => PlayListResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var response = new PaginatedPlayListResponse( + var response = new PaginatedResponse( pagedPlayLists.ToArray(), pagedPlayLists.PageNumber, pagedPlayLists.PageSize, diff --git a/Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs b/Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs new file mode 100644 index 0000000..9a3f9c4 --- /dev/null +++ b/Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Tracks.Contracts; + +namespace Musify.Application.Tracks.Queries.GetTrackById +{ + public record GetTrackByIdQuery(Guid TrackId) + : IRequest >>; +} \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs b/Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs new file mode 100644 index 0000000..151ac1f --- /dev/null +++ b/Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs @@ -0,0 +1,20 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Tracks.Contracts; + +namespace Musify.Application.Tracks.Queries.GetTrackById +{ + public class GetTrackByIdQueryHandler(IDatabase database) + : IRequestHandler >> + { + public async Task> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) + { + var track = await database.Tracks.FindAsync(request.TrackId); + + return track is null + ? Result.NotFound() + : Result.Success(TrackResponse.FromEntity(track)); + } + } +} \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracks/GetTracksQuery.cs b/Application/Tracks/Queries/GetTracks/GetTracksQuery.cs new file mode 100644 index 0000000..37a81cb --- /dev/null +++ b/Application/Tracks/Queries/GetTracks/GetTracksQuery.cs @@ -0,0 +1,10 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; +using Musify.Application.Tracks.Contracts; + +namespace Musify.Application.Tracks.Queries.GetTracks +{ + public record GetTracksQuery(int PageNumber = 1, int PageSize = 10) + : IRequest >> >; +} \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs b/Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs new file mode 100644 index 0000000..e0bd672 --- /dev/null +++ b/Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs @@ -0,0 +1,40 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Application; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Tracks.Contracts; +using X.PagedList.EF; + +namespace Musify.Application.Tracks.Queries.GetTracks +{ + public class GetTracksQueryHandler(IDatabase database) + : IRequestHandler> > > + { + public async Task >> Handle(GetTracksQuery request, CancellationToken cancellationToken) + { + var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; + var pageSize = request.PageSize < 1 ? 10 : request.PageSize; + + var totalCount = await database.PlayLists.CountAsync(cancellationToken); + + var pagedPlayLists = await database.UserHasTracks + .AsNoTracking() + .OrderBy(t => t.Id) + .Include(ut => ut.Track) + .Select(t => TrackResponse.FromEntity(t.Track)) + .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + + var response = new PaginatedResponse( + pagedPlayLists.ToArray(), + pagedPlayLists.PageNumber, + pagedPlayLists.PageSize, + pagedPlayLists.PageCount, + pagedPlayLists.TotalItemCount, + pagedPlayLists.HasNextPage, + pagedPlayLists.HasPreviousPage); + + return Result.Success(response); + } + } +} \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs new file mode 100644 index 0000000..7968dff --- /dev/null +++ b/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs @@ -0,0 +1,14 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; +using Musify.Application.Tracks.Contracts; + +namespace Musify.Application.Tracks.Queries.GetTracksByUserId +{ + public record GetTracksByUserIdQuery( + Guid UserId, + string Name = "", + int PageNumber = 1, + int PageSize = 10) + : IRequest > >>; +} \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs new file mode 100644 index 0000000..b4c5285 --- /dev/null +++ b/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs @@ -0,0 +1,47 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Application; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Tracks.Contracts; +using X.PagedList.EF; + +namespace Musify.Application.Tracks.Queries.GetTracksByUserId +{ + public class GetTracksByUserIdQueryHandler(IDatabase database) + : IRequestHandler> >> + { + public async Task >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken) + { + var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; + var pageSize = request.PageSize < 1 ? 10 : request.PageSize; + var normalizedName = request.Name?.Trim().ToUpperInvariant() ?? string.Empty; + + var tracksQuery = database.UserHasTracks + .AsNoTracking() + .Include(ut => ut.Track) + .Where(p => p.UserId == request.UserId); + + if (!string.IsNullOrWhiteSpace(normalizedName)) + tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName)); + + var totalCount = await tracksQuery.CountAsync(cancellationToken); + + var pagedTracks = await tracksQuery + .OrderBy(t => t.Track.CreatedDate) + .Select(t => TrackResponse.FromEntity(t.Track)) + .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + + var response = new PaginatedResponse( + pagedTracks.ToArray(), + pagedTracks.PageNumber, + pagedTracks.PageSize, + pagedTracks.PageCount, + pagedTracks.TotalItemCount, + pagedTracks.HasNextPage, + pagedTracks.HasPreviousPage); + + return Result.Success(response); + } + } +} \ No newline at end of file From 2ed2974ab44083a93df970e54a9c13c4756940ee Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 20:33:15 +0100 Subject: [PATCH 060/172] Move --- .../Infrastructure/IKeycloakUserService.cs | 2 +- .../{CreatePlayList => }/CreatePlayListCommand.cs | 4 ++-- .../{DeletePlayList => }/DeletePlayListCommand.cs | 2 +- .../{UpdatePlayList => }/UpdatePlayListCommand.cs | 4 ++-- .../CreatePlayListCommandHandler.cs | 5 +++-- .../DeletePlayListCommandHandler.cs | 3 ++- .../GetPlayListByIdQueryHandler.cs | 5 +++-- .../GetPlayListsByUserIdQueryHandler.cs | 5 +++-- .../GetPlayListsQueryHandler.cs | 5 +++-- .../UpdatePlayListCommandHandler.cs | 5 +++-- .../{GetPlayListById => }/GetPlayListByIdQuery.cs | 4 ++-- .../GetPlayListsByUserIdQuery.cs | 4 ++-- .../{GetPlayLists => }/GetPlayListsQuery.cs | 4 ++-- .../{Contracts => Responses}/PlayListResponse.cs | 2 +- .../{CreateTrack => }/CreateTrackCommand.cs | 4 ++-- .../CreateTrackCommandHandler.cs | 5 +++-- .../GetTrackByIdQueryHandler.cs | 5 +++-- .../GetTracksByUserIdQueryHandler.cs | 5 +++-- .../GetTracks => Handler}/GetTracksQueryHandler.cs | 5 +++-- .../{GetTrackById => }/GetTrackByIdQuery.cs | 4 ++-- .../GetTracksByUserIdQuery.cs | 4 ++-- .../Queries/{GetTracks => }/GetTracksQuery.cs | 4 ++-- .../{Contracts => Responses}/TrackResponse.cs | 2 +- .../Commands/{CreateUser => }/CreateUserCommand.cs | 4 ++-- .../Commands/{DeleteUser => }/DeleteUserCommand.cs | 2 +- .../Users/Contracts/PaginatedUserResponse.cs | 12 ------------ .../CreateUserCommandHandler.cs | 5 +++-- .../DeleteUserCommandHandler.cs | 2 +- .../GetUserByIdQueryHandler.cs | 7 ++++--- .../GetUserByKeycloakQueryHandler.cs | 7 ++++--- .../GetUsers => Handlers}/GetUsersQueryHandler.cs | 14 ++++++++------ .../Queries/{GetUserById => }/GetUserByIdQuery.cs | 4 ++-- .../GetUserByKeycloakQuery.cs | 4 ++-- .../Users/Queries/GetUsers/GetUsersQuery.cs | 9 --------- Application/Users/Queries/GetUsersQuery.cs | 10 ++++++++++ .../KeycloakUserResponse.cs | 2 +- .../Users/{Contracts => Responses}/UserResponse.cs | 2 +- Infrastructure/Identity/KeycloakUserMapper.cs | 2 +- Infrastructure/Identity/KeycloakUserService.cs | 2 +- 39 files changed, 92 insertions(+), 88 deletions(-) rename Application/PlayLists/Commands/{CreatePlayList => }/CreatePlayListCommand.cs (75%) rename Application/PlayLists/Commands/{DeletePlayList => }/DeletePlayListCommand.cs (75%) rename Application/PlayLists/Commands/{UpdatePlayList => }/UpdatePlayListCommand.cs (76%) rename Application/PlayLists/{Commands/CreatePlayList => Handlers}/CreatePlayListCommandHandler.cs (97%) rename Application/PlayLists/{Commands/DeletePlayList => Handlers}/DeletePlayListCommandHandler.cs (97%) rename Application/PlayLists/{Queries/GetPlayListById => Handlers}/GetPlayListByIdQueryHandler.cs (85%) rename Application/PlayLists/{Queries/GetPlayListsByUserId => Handlers}/GetPlayListsByUserIdQueryHandler.cs (93%) rename Application/PlayLists/{Queries/GetPlayLists => Handlers}/GetPlayListsQueryHandler.cs (91%) rename Application/PlayLists/{Commands/UpdatePlayList => Handlers}/UpdatePlayListCommandHandler.cs (98%) rename Application/PlayLists/Queries/{GetPlayListById => }/GetPlayListByIdQuery.cs (63%) rename Application/PlayLists/Queries/{GetPlayListsByUserId => }/GetPlayListsByUserIdQuery.cs (75%) rename Application/PlayLists/Queries/{GetPlayLists => }/GetPlayListsQuery.cs (72%) rename Application/PlayLists/{Contracts => Responses}/PlayListResponse.cs (93%) rename Application/Tracks/Commands/{CreateTrack => }/CreateTrackCommand.cs (75%) rename Application/Tracks/{Commands/CreateTrack => Handler}/CreateTrackCommandHandler.cs (96%) rename Application/Tracks/{Queries/GetTrackById => Handler}/GetTrackByIdQueryHandler.cs (82%) rename Application/Tracks/{Queries/GetTracksByUserId => Handler}/GetTracksByUserIdQueryHandler.cs (93%) rename Application/Tracks/{Queries/GetTracks => Handler}/GetTracksQueryHandler.cs (92%) rename Application/Tracks/Queries/{GetTrackById => }/GetTrackByIdQuery.cs (64%) rename Application/Tracks/Queries/{GetTracksByUserId => }/GetTracksByUserIdQuery.cs (76%) rename Application/Tracks/Queries/{GetTracks => }/GetTracksQuery.cs (73%) rename Application/Tracks/{Contracts => Responses}/TrackResponse.cs (90%) rename Application/Users/Commands/{CreateUser => }/CreateUserCommand.cs (64%) rename Application/Users/Commands/{DeleteUser => }/DeleteUserCommand.cs (74%) delete mode 100644 Application/Users/Contracts/PaginatedUserResponse.cs rename Application/Users/{Commands/CreateUser => Handlers}/CreateUserCommandHandler.cs (94%) rename Application/Users/{Commands/DeleteUser => Handlers}/DeleteUserCommandHandler.cs (92%) rename Application/Users/{Queries/GetUserById => Handlers}/GetUserByIdQueryHandler.cs (90%) rename Application/Users/{Queries/GetUserByKeycloakId => Handlers}/GetUserByKeycloakQueryHandler.cs (89%) rename Application/Users/{Queries/GetUsers => Handlers}/GetUsersQueryHandler.cs (81%) rename Application/Users/Queries/{GetUserById => }/GetUserByIdQuery.cs (64%) rename Application/Users/Queries/{GetUserByKeycloakId => }/GetUserByKeycloakQuery.cs (63%) delete mode 100644 Application/Users/Queries/GetUsers/GetUsersQuery.cs create mode 100644 Application/Users/Queries/GetUsersQuery.cs rename Application/Users/{Contracts => Responses}/KeycloakUserResponse.cs (73%) rename Application/Users/{Contracts => Responses}/UserResponse.cs (79%) diff --git a/Application/Contracts/Infrastructure/IKeycloakUserService.cs b/Application/Contracts/Infrastructure/IKeycloakUserService.cs index 055f7f4..f78c4a3 100644 --- a/Application/Contracts/Infrastructure/IKeycloakUserService.cs +++ b/Application/Contracts/Infrastructure/IKeycloakUserService.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Responses; namespace Musify.Application.Contracts.Infrastructure { diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs similarity index 75% rename from Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs rename to Application/PlayLists/Commands/CreatePlayListCommand.cs index b59d8ec..0ceddcc 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; -namespace Musify.Application.PlayLists.Commands.CreatePlayList +namespace Musify.Application.PlayLists.Commands { public record CreatePlayListCommand( Guid UserId, diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs b/Application/PlayLists/Commands/DeletePlayListCommand.cs similarity index 75% rename from Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs rename to Application/PlayLists/Commands/DeletePlayListCommand.cs index 4057db9..d4fd556 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs +++ b/Application/PlayLists/Commands/DeletePlayListCommand.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -namespace Musify.Application.PlayLists.Commands.DeletePlayList +namespace Musify.Application.PlayLists.Commands { public record DeletePlayListCommand(Guid UserId, Guid PlayListId) : IRequest >; diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayListCommand.cs similarity index 76% rename from Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs rename to Application/PlayLists/Commands/UpdatePlayListCommand.cs index 1ef0e49..32442ef 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommand.cs +++ b/Application/PlayLists/Commands/UpdatePlayListCommand.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; -namespace Musify.Application.PlayLists.Commands.UpdatePlayList +namespace Musify.Application.PlayLists.Commands { public record UpdatePlayListCommand( Guid UserId, diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs similarity index 97% rename from Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs rename to Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 3d4a6f2..c26a888 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -5,10 +5,11 @@ using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; -namespace Musify.Application.PlayLists.Commands.CreatePlayList +namespace Musify.Application.PlayLists.Handlers { public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, IPictureHandler pictureHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs similarity index 97% rename from Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs rename to Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index fc03dd8..10c1a3c 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -4,8 +4,9 @@ using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Application.PlayLists.Commands; -namespace Musify.Application.PlayLists.Commands.DeletePlayList +namespace Musify.Application.PlayLists.Handlers { public class DeletePlayListCommandHandler(IDatabase database, IEventBus eventBus, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) diff --git a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs similarity index 85% rename from Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs rename to Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index f2313f4..55301ab 100644 --- a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -2,9 +2,10 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; +using Musify.Application.PlayLists.Queries; -namespace Musify.Application.PlayLists.Queries.GetPlayListById +namespace Musify.Application.PlayLists.Handlers { public class GetPlayListByIdQueryHandler(IDatabase database) : IRequestHandler> > diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs similarity index 93% rename from Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs rename to Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index a54bf3d..18c1fda 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -3,10 +3,11 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; +using Musify.Application.PlayLists.Queries; using X.PagedList.EF; -namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId +namespace Musify.Application.PlayLists.Handlers { public class GetPlayListsByUserIdQueryHandler(IDatabase database) : IRequestHandler > >> diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs similarity index 91% rename from Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs rename to Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index 8a5835b..8cb758e 100644 --- a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -3,10 +3,11 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; +using Musify.Application.PlayLists.Queries; using X.PagedList.EF; -namespace Musify.Application.PlayLists.Queries.GetPlayLists +namespace Musify.Application.PlayLists.Handlers { public class GetPlayListsQueryHandler(IDatabase database) : IRequestHandler >> > diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs similarity index 98% rename from Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs rename to Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index e58e352..ffd1a0d 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -5,9 +5,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Responses; -namespace Musify.Application.PlayLists.Commands.UpdatePlayList +namespace Musify.Application.PlayLists.Handlers { public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, diff --git a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs b/Application/PlayLists/Queries/GetPlayListByIdQuery.cs similarity index 63% rename from Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs rename to Application/PlayLists/Queries/GetPlayListByIdQuery.cs index 031af87..4bf4360 100644 --- a/Application/PlayLists/Queries/GetPlayListById/GetPlayListByIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListByIdQuery.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; -namespace Musify.Application.PlayLists.Queries.GetPlayListById +namespace Musify.Application.PlayLists.Queries { public record GetPlayListByIdQuery(Guid Id) : IRequest>>; diff --git a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs similarity index 75% rename from Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs rename to Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index b90358a..5610553 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserId/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; -namespace Musify.Application.PlayLists.Queries.GetPlayListsByUserId +namespace Musify.Application.PlayLists.Queries { public record GetPlayListsByUserIdQuery( Guid UserId, diff --git a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayListsQuery.cs similarity index 72% rename from Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs rename to Application/PlayLists/Queries/GetPlayListsQuery.cs index e5de1e1..9764acd 100644 --- a/Application/PlayLists/Queries/GetPlayLists/GetPlayListsQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.PlayLists.Contracts; +using Musify.Application.PlayLists.Responses; -namespace Musify.Application.PlayLists.Queries.GetPlayLists +namespace Musify.Application.PlayLists.Queries { public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10) : IRequest> >>; diff --git a/Application/PlayLists/Contracts/PlayListResponse.cs b/Application/PlayLists/Responses/PlayListResponse.cs similarity index 93% rename from Application/PlayLists/Contracts/PlayListResponse.cs rename to Application/PlayLists/Responses/PlayListResponse.cs index 8ec1f54..d2888fc 100644 --- a/Application/PlayLists/Contracts/PlayListResponse.cs +++ b/Application/PlayLists/Responses/PlayListResponse.cs @@ -1,6 +1,6 @@ using Musify.Domain.Entities; -namespace Musify.Application.PlayLists.Contracts +namespace Musify.Application.PlayLists.Responses { public record PlayListResponse( Guid Id, diff --git a/Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs similarity index 75% rename from Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs rename to Application/Tracks/Commands/CreateTrackCommand.cs index c10007c..2a6c939 100644 --- a/Application/Tracks/Commands/CreateTrack/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Responses; -namespace Musify.Application.Tracks.Commands.CreateTrack +namespace Musify.Application.Tracks.Commands { public record CreateTrackCommand( Guid UserId, diff --git a/Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs similarity index 96% rename from Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs rename to Application/Tracks/Handler/CreateTrackCommandHandler.cs index 2b0a745..9ab3158 100644 --- a/Application/Tracks/Commands/CreateTrack/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -4,10 +4,11 @@ using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Commands; +using Musify.Application.Tracks.Responses; using Musify.Domain.Entities; -namespace Musify.Application.Tracks.Commands.CreateTrack +namespace Musify.Application.Tracks.Handler { public class CreateTrackCommandHandler(IDatabase database, IPictureHandler pictureHandler, IEventBus eventBus, IStorageHandler storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) diff --git a/Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs similarity index 82% rename from Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs rename to Application/Tracks/Handler/GetTrackByIdQueryHandler.cs index 151ac1f..c0d4b2e 100644 --- a/Application/Tracks/Queries/GetTrackById/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs @@ -1,9 +1,10 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Queries; +using Musify.Application.Tracks.Responses; -namespace Musify.Application.Tracks.Queries.GetTrackById +namespace Musify.Application.Tracks.Handler { public class GetTrackByIdQueryHandler(IDatabase database) : IRequestHandler >> diff --git a/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs similarity index 93% rename from Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs rename to Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs index b4c5285..4dd81b8 100644 --- a/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs @@ -3,10 +3,11 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Queries; +using Musify.Application.Tracks.Responses; using X.PagedList.EF; -namespace Musify.Application.Tracks.Queries.GetTracksByUserId +namespace Musify.Application.Tracks.Handler { public class GetTracksByUserIdQueryHandler(IDatabase database) : IRequestHandler> >> diff --git a/Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs b/Application/Tracks/Handler/GetTracksQueryHandler.cs similarity index 92% rename from Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs rename to Application/Tracks/Handler/GetTracksQueryHandler.cs index e0bd672..59e43e3 100644 --- a/Application/Tracks/Queries/GetTracks/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksQueryHandler.cs @@ -3,10 +3,11 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Queries; +using Musify.Application.Tracks.Responses; using X.PagedList.EF; -namespace Musify.Application.Tracks.Queries.GetTracks +namespace Musify.Application.Tracks.Handler { public class GetTracksQueryHandler(IDatabase database) : IRequestHandler> > > diff --git a/Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs b/Application/Tracks/Queries/GetTrackByIdQuery.cs similarity index 64% rename from Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs rename to Application/Tracks/Queries/GetTrackByIdQuery.cs index 9a3f9c4..1008b99 100644 --- a/Application/Tracks/Queries/GetTrackById/GetTrackByIdQuery.cs +++ b/Application/Tracks/Queries/GetTrackByIdQuery.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Responses; -namespace Musify.Application.Tracks.Queries.GetTrackById +namespace Musify.Application.Tracks.Queries { public record GetTrackByIdQuery(Guid TrackId) : IRequest >>; diff --git a/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs similarity index 76% rename from Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs rename to Application/Tracks/Queries/GetTracksByUserIdQuery.cs index 7968dff..25afa9f 100644 --- a/Application/Tracks/Queries/GetTracksByUserId/GetTracksByUserIdQuery.cs +++ b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Responses; -namespace Musify.Application.Tracks.Queries.GetTracksByUserId +namespace Musify.Application.Tracks.Queries { public record GetTracksByUserIdQuery( Guid UserId, diff --git a/Application/Tracks/Queries/GetTracks/GetTracksQuery.cs b/Application/Tracks/Queries/GetTracksQuery.cs similarity index 73% rename from Application/Tracks/Queries/GetTracks/GetTracksQuery.cs rename to Application/Tracks/Queries/GetTracksQuery.cs index 37a81cb..3a10860 100644 --- a/Application/Tracks/Queries/GetTracks/GetTracksQuery.cs +++ b/Application/Tracks/Queries/GetTracksQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; -using Musify.Application.Tracks.Contracts; +using Musify.Application.Tracks.Responses; -namespace Musify.Application.Tracks.Queries.GetTracks +namespace Musify.Application.Tracks.Queries { public record GetTracksQuery(int PageNumber = 1, int PageSize = 10) : IRequest >> >; diff --git a/Application/Tracks/Contracts/TrackResponse.cs b/Application/Tracks/Responses/TrackResponse.cs similarity index 90% rename from Application/Tracks/Contracts/TrackResponse.cs rename to Application/Tracks/Responses/TrackResponse.cs index 8a0a67e..78143dd 100644 --- a/Application/Tracks/Contracts/TrackResponse.cs +++ b/Application/Tracks/Responses/TrackResponse.cs @@ -1,6 +1,6 @@ using Musify.Domain.Entities; -namespace Musify.Application.Tracks.Contracts +namespace Musify.Application.Tracks.Responses { public record TrackResponse( Guid Id, diff --git a/Application/Users/Commands/CreateUser/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs similarity index 64% rename from Application/Users/Commands/CreateUser/CreateUserCommand.cs rename to Application/Users/Commands/CreateUserCommand.cs index ebb068b..afa6f51 100644 --- a/Application/Users/Commands/CreateUser/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Responses; -namespace Musify.Application.Users.Commands.CreateUser +namespace Musify.Application.Users.Commands { public record CreateUserCommand(Guid Id) : IRequest>>; diff --git a/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs b/Application/Users/Commands/DeleteUserCommand.cs similarity index 74% rename from Application/Users/Commands/DeleteUser/DeleteUserCommand.cs rename to Application/Users/Commands/DeleteUserCommand.cs index 720fc53..2773d0a 100644 --- a/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs +++ b/Application/Users/Commands/DeleteUserCommand.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -namespace Musify.Application.Users.Commands.DeleteUser +namespace Musify.Application.Users.Commands { public record DeleteUserCommand(Guid Id) : IRequest>; diff --git a/Application/Users/Contracts/PaginatedUserResponse.cs b/Application/Users/Contracts/PaginatedUserResponse.cs deleted file mode 100644 index 042dd32..0000000 --- a/Application/Users/Contracts/PaginatedUserResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Musify.Application.Users.Contracts -{ - public record PaginatedUserResponse( - IReadOnlyCollection Users, - int PageNumber, - int PageSize, - int PageCount, - int TotalItemCount, - bool HasNextPage, - bool HasPreviousPage - ); -} \ No newline at end of file diff --git a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs similarity index 94% rename from Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs rename to Application/Users/Handlers/CreateUserCommandHandler.cs index add9aef..64ab778 100644 --- a/Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -3,10 +3,11 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Commands; +using Musify.Application.Users.Responses; using Musify.Domain.Entities; -namespace Musify.Application.Users.Commands.CreateUser +namespace Musify.Application.Users.Handlers { public class CreateUserCommandHandler(IDatabase database, IKeycloakUserService keycloakUserClient, ILogger logger) : IRequestHandler >> diff --git a/Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs b/Application/Users/Handlers/DeleteUserCommandHandler.cs similarity index 92% rename from Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs rename to Application/Users/Handlers/DeleteUserCommandHandler.cs index 6cb9845..906bee3 100644 --- a/Application/Users/Commands/DeleteUser/DeleteUserCommandHandler.cs +++ b/Application/Users/Handlers/DeleteUserCommandHandler.cs @@ -2,7 +2,7 @@ using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Infrastructure; -namespace Musify.Application.Users.Commands.DeleteUser +namespace Musify.Application.Users.Commands { public class DeleteUserCommandHandler(IDatabase database) : IRequestHandler> diff --git a/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs similarity index 90% rename from Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs rename to Application/Users/Handlers/GetUserByIdQueryHandler.cs index f57b93b..44e0008 100644 --- a/Application/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -2,12 +2,13 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Queries; +using Musify.Application.Users.Responses; -namespace Musify.Application.Users.Queries.GetUserById +namespace Musify.Application.Users.Handlers { public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) - : IRequestHandler>> + : IRequestHandler >> { public async Task> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs similarity index 89% rename from Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs rename to Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index 2f7c897..f8e89cc 100644 --- a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -2,12 +2,13 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Queries; +using Musify.Application.Users.Responses; -namespace Musify.Application.Users.Queries.GetUserByKeycloakId +namespace Musify.Application.Users.Handlers { public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) - : IRequestHandler>> + : IRequestHandler >> { public async Task> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) { diff --git a/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs similarity index 81% rename from Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs rename to Application/Users/Handlers/GetUsersQueryHandler.cs index 9e7ecf3..8c5d856 100644 --- a/Application/Users/Queries/GetUsers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -1,17 +1,19 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Queries; +using Musify.Application.Users.Responses; using X.PagedList; using X.PagedList.Extensions; -namespace Musify.Application.Users.Queries.GetUsers +namespace Musify.Application.Users.Handlers { public class GetUsersQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) - : IRequestHandler >> + : IRequestHandler > >> { - public async Task> Handle(GetUsersQuery request, CancellationToken cancellationToken) + public async Task >> Handle(GetUsersQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -44,7 +46,7 @@ namespace Musify.Application.Users.Queries.GetUsers var pagedUsers = new StaticPagedList(users, pageNumber, pageSize, totalItemCount); - var response = new PaginatedUserResponse( + var response = new PaginatedResponse( pagedUsers.ToArray(), pagedUsers.PageNumber, pagedUsers.PageSize, @@ -56,4 +58,4 @@ namespace Musify.Application.Users.Queries.GetUsers return Result.Success(response); } } -} +} \ No newline at end of file diff --git a/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs b/Application/Users/Queries/GetUserByIdQuery.cs similarity index 64% rename from Application/Users/Queries/GetUserById/GetUserByIdQuery.cs rename to Application/Users/Queries/GetUserByIdQuery.cs index adc9340..fbdf6fd 100644 --- a/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs +++ b/Application/Users/Queries/GetUserByIdQuery.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Responses; -namespace Musify.Application.Users.Queries.GetUserById +namespace Musify.Application.Users.Queries { public record GetUserByIdQuery(Guid Id) : IRequest >>; diff --git a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQuery.cs b/Application/Users/Queries/GetUserByKeycloakQuery.cs similarity index 63% rename from Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQuery.cs rename to Application/Users/Queries/GetUserByKeycloakQuery.cs index 55910b2..e53d449 100644 --- a/Application/Users/Queries/GetUserByKeycloakId/GetUserByKeycloakQuery.cs +++ b/Application/Users/Queries/GetUserByKeycloakQuery.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Responses; -namespace Musify.Application.Users.Queries.GetUserByKeycloakId +namespace Musify.Application.Users.Queries { public record GetUserByKeycloakQuery(Guid Id) : IRequest >>; diff --git a/Application/Users/Queries/GetUsers/GetUsersQuery.cs b/Application/Users/Queries/GetUsers/GetUsersQuery.cs deleted file mode 100644 index 7d3aeb2..0000000 --- a/Application/Users/Queries/GetUsers/GetUsersQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ardalis.Result; -using DispatchR.Abstractions.Send; -using Musify.Application.Users.Contracts; - -namespace Musify.Application.Users.Queries.GetUsers -{ - public record GetUsersQuery(int PageNumber, int PageSize, string UsernameSearch) - : IRequest> >; -} diff --git a/Application/Users/Queries/GetUsersQuery.cs b/Application/Users/Queries/GetUsersQuery.cs new file mode 100644 index 0000000..338e01f --- /dev/null +++ b/Application/Users/Queries/GetUsersQuery.cs @@ -0,0 +1,10 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Musify.Application.Contracts.Application; +using Musify.Application.Users.Responses; + +namespace Musify.Application.Users.Queries +{ + public record GetUsersQuery(int PageNumber, int PageSize, string UsernameSearch) + : IRequest >> >; +} diff --git a/Application/Users/Contracts/KeycloakUserResponse.cs b/Application/Users/Responses/KeycloakUserResponse.cs similarity index 73% rename from Application/Users/Contracts/KeycloakUserResponse.cs rename to Application/Users/Responses/KeycloakUserResponse.cs index c4ad490..8cedf5d 100644 --- a/Application/Users/Contracts/KeycloakUserResponse.cs +++ b/Application/Users/Responses/KeycloakUserResponse.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Users.Contracts +namespace Musify.Application.Users.Responses { public record KeycloakUserResponse( Guid Id, diff --git a/Application/Users/Contracts/UserResponse.cs b/Application/Users/Responses/UserResponse.cs similarity index 79% rename from Application/Users/Contracts/UserResponse.cs rename to Application/Users/Responses/UserResponse.cs index 435536c..758153f 100644 --- a/Application/Users/Contracts/UserResponse.cs +++ b/Application/Users/Responses/UserResponse.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Users.Contracts +namespace Musify.Application.Users.Responses { public record UserResponse( string Id, diff --git a/Infrastructure/Identity/KeycloakUserMapper.cs b/Infrastructure/Identity/KeycloakUserMapper.cs index 768e069..fdac511 100644 --- a/Infrastructure/Identity/KeycloakUserMapper.cs +++ b/Infrastructure/Identity/KeycloakUserMapper.cs @@ -1,5 +1,5 @@ using Keycloak.Net.Models.Users; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Responses; namespace Musify.Infrastructure.Identity { diff --git a/Infrastructure/Identity/KeycloakUserService.cs b/Infrastructure/Identity/KeycloakUserService.cs index 175fc3e..16ea1fa 100644 --- a/Infrastructure/Identity/KeycloakUserService.cs +++ b/Infrastructure/Identity/KeycloakUserService.cs @@ -3,7 +3,7 @@ using Keycloak.Net; using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Users.Contracts; +using Musify.Application.Users.Responses; using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Identity From 1c0a253bebc37381c1d5014c36d7f8c7df611e74 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 20:38:13 +0100 Subject: [PATCH 061/172] Refactor --- .../Handler/CreateTrackCommandHandler.cs | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 9ab3158..a161797 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -2,6 +2,7 @@ using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; +using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; @@ -34,44 +35,16 @@ namespace Musify.Application.Tracks.Handler LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, }; - var pictureUploadResult = await storageHandler.UploadFileAsync( - request.Picture.FileStream, - request.Picture.ContentType, - storageConfiguration.BucketName, - originalPictureKeyName, - cancellationToken); - + var pictureUploadResult = await UploadFile(request.Picture, originalPictureKeyName, cancellationToken); if (!pictureUploadResult.IsSuccess) { logger.LogError("Failed to upload picture for track {TrackTitle}", request.Title); return Result.Error($"Failed to upload picture for track {request.Title}"); } - var smallPictureKeyName = Path.Combine( - trackConfiguration.Routes.SmallPictures, - $"{track.Id}.{pictureHandler.FileExtension}"); - var mediumPictureKeyName = Path.Combine( - trackConfiguration.Routes.MediumPictures, - $"{track.Id}.{pictureHandler.FileExtension}"); - var largePictureKeyName = Path.Combine( - trackConfiguration.Routes.LargePictures, - $"{track.Id}.{pictureHandler.FileExtension}"); - try { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - track.Id, - storageConfiguration.BucketName, - originalPictureKeyName, - smallPictureKeyName, - trackConfiguration.PicturesSizes.SmallPictureWidth, - trackConfiguration.PicturesSizes.SmallPictureHeight, - mediumPictureKeyName, - trackConfiguration.PicturesSizes.MediumPictureWidth, - trackConfiguration.PicturesSizes.MediumPictureHeight, - largePictureKeyName, - trackConfiguration.PicturesSizes.LargePictureWidth, - trackConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + await PublishUpdateEvent(originalPictureKeyName, track, cancellationToken); } catch (Exception exception) { @@ -84,5 +57,44 @@ namespace Musify.Application.Tracks.Handler return Result.Success(); } + + async Task> UploadFile(IFileData fileData, string originalPictureKeyName, CancellationToken cancellationToken) + { + var uploadFile = await storageHandler.UploadFileAsync( + fileData.FileStream, + fileData.ContentType, + storageConfiguration.BucketName, + originalPictureKeyName, + cancellationToken); + + return uploadFile; + } + + async Task PublishUpdateEvent(string originalPictureKeyName, Track track, CancellationToken cancellationToken) + { + var smallPictureKeyName = Path.Combine( + trackConfiguration.Routes.SmallPictures, + $"{track.Id}.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + trackConfiguration.Routes.MediumPictures, + $"{track.Id}.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + trackConfiguration.Routes.LargePictures, + $"{track.Id}.{pictureHandler.FileExtension}"); + + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + track.Id, + storageConfiguration.BucketName, + originalPictureKeyName, + smallPictureKeyName, + trackConfiguration.PicturesSizes.SmallPictureWidth, + trackConfiguration.PicturesSizes.SmallPictureHeight, + mediumPictureKeyName, + trackConfiguration.PicturesSizes.MediumPictureWidth, + trackConfiguration.PicturesSizes.MediumPictureHeight, + largePictureKeyName, + trackConfiguration.PicturesSizes.LargePictureWidth, + trackConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + } } } From 0d968795441ef0d064e172f88b05381de8cf5c3f Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 21:01:57 +0100 Subject: [PATCH 062/172] Handler --- .../Handlers/CreatePlayListCommandHandler.cs | 129 +++++++++-------- .../Handlers/DeletePlayListCommandHandler.cs | 5 +- .../Handlers/UpdatePlayListCommandHandler.cs | 135 +++++++++--------- .../Handler/CreateTrackCommandHandler.cs | 2 +- 4 files changed, 144 insertions(+), 127 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index c26a888..f4f8ff4 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -3,6 +3,7 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; +using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; @@ -36,63 +37,12 @@ namespace Musify.Application.PlayLists.Handlers LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, }; - if (request.Picture is not null) - { - var imageId = Guid.NewGuid(); - var pictureName = $"{imageId}{request.Picture.FileType}"; - var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); + var setPictureResult = request.Picture is null + ? Result.Success() + : await SetPicture(playList, request.Picture, cancellationToken); - var uploadResult = await storageHandler.UploadFileAsync( - request.Picture.FileStream, - request.Picture.ContentType, - storageConfiguration.BucketName, - originalImageStorageKey, - 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 image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); - - var smallPictureKeyName = Path.Combine( - playListConfiguration.Routes.SmallPictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); - var mediumPictureKeyName = Path.Combine( - playListConfiguration.Routes.MediumPictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); - var largePictureKeyName = Path.Combine( - playListConfiguration.Routes.LargePictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); - - try - { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.BucketName, - originalImageStorageKey, - smallPictureKeyName, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - mediumPictureKeyName, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - largePictureKeyName, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish resize picture event for PlayList with id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); - } - } - else - { - logger.LogInformation("No picture provided for PlayList with Id={PlayListId}. Using Preset pictures.", playList.Id); - } + if (!setPictureResult.IsSuccess) + return setPictureResult; await database.PlayLists.AddAsync(playList, cancellationToken); await database.SaveChangesAsync(cancellationToken); @@ -100,5 +50,72 @@ namespace Musify.Application.PlayLists.Handlers logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playList.Id, request.UserId); return Result.Created(PlayListResponse.FromEntity(playList)); } + + async Task SetPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) + { + var imageId = Guid.NewGuid(); + var pictureName = $"{imageId}{picture.FileType}"; + var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); + + var uploadResult = await UploadFile(picture, originalImageStorageKey, 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 image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); + + try + { + await PublishUpdateEvent(originalImageStorageKey, playList, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish resize picture event for PlayList with id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); + } + return Result.Success(); + } + + async Task> UploadFile(IFileData fileData, string storageKeyName, CancellationToken cancellationToken) + { + var uploadFileResult = await storageHandler.UploadFileAsync( + fileData.FileStream, + fileData.ContentType, + storageConfiguration.BucketName, + storageKeyName, + cancellationToken); + + return uploadFileResult; + } + + async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) + { + var smallPictureKeyName = Path.Combine( + playListConfiguration.Routes.SmallPictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + playListConfiguration.Routes.MediumPictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + playListConfiguration.Routes.LargePictures, + $"{playList.Id}.{pictureHandler.FileExtension}"); + + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + originalPictureKeyName, + smallPictureKeyName, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + mediumPictureKeyName, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + largePictureKeyName, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 10c1a3c..bcd8486 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -47,14 +47,13 @@ namespace Musify.Application.PlayLists.Handlers { var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); - if (!publishResult.IsSuccess) - { + if (!publishResult.IsSuccess && !publishResult.IsNotFound()) return publishResult; - } } database.PlayLists.Remove(playList); await database.SaveChangesAsync(cancellationToken); + return Result.NoContent(); } diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index ffd1a0d..b85f857 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -3,10 +3,12 @@ using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; +using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; +using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { @@ -44,64 +46,29 @@ namespace Musify.Application.PlayLists.Handlers { database.PlayLists.Update(playList); await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Updated PlayList with id={PlayListId} without picture changes.", playList.Id); return Result.Success(PlayListResponse.FromEntity(playList)); } if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for small picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + await PublishRemoveFileEvent(playList.SmallPictureKeyName, cancellationToken); } if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for medium picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + await PublishRemoveFileEvent(playList.MediumPictureKeyName, cancellationToken); } if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for large picture of PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish remove file event for PlayList with id {playList.Id}."); - } + await PublishRemoveFileEvent(playList.LargePictureKeyName, cancellationToken); } var newImageId = Guid.NewGuid(); var pictureName = $"{newImageId}{request.NewPicture.FileType}"; var newImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); - var uploadResult = await storageHandler.UploadFileAsync( - request.NewPicture.FileStream, - request.NewPicture.ContentType, - storageConfiguration.BucketName, - newImageStorageKey, - cancellationToken); - + var uploadResult = await UploadFile(request.NewPicture, newImageStorageKey, cancellationToken); if (!uploadResult.IsSuccess) { logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error: {ErrorMessage}", @@ -109,6 +76,54 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } + try + { + await PublishUpdateEvent(newImageStorageKey, playList, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish update picture event for PlayList with id {PlayListId}.", playList.Id); + return Result.Error($"Failed to publish update picture event for PlayList with id {playList.Id}."); + } + + database.PlayLists.Update(playList); + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKeyName}.", playList.Id, newImageStorageKey); + return Result.Success(PlayListResponse.FromEntity(playList)); + } + + async Task PublishRemoveFileEvent(string keyName, CancellationToken cancellationToken) + { + try + { + await eventBus.PublishAsync(new RemoveFileEvent( + storageConfiguration.BucketName, + keyName), cancellationToken); + + return Result.NoContent(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish remove file event for key {KeyName}.", keyName); + return Result.Error($"Failed to publish remove file event for key {keyName}."); + } + } + + async Task> UploadFile(IFileData picture, string pictureKeyName, CancellationToken cancellationToken) + { + var uploadFileResult = await storageHandler.UploadFileAsync( + picture.FileStream, + picture.ContentType, + storageConfiguration.BucketName, + pictureKeyName, + cancellationToken); + + return uploadFileResult; + } + + async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) + { var smallPictureKeyName = Path.Combine( playListConfiguration.Routes.SmallPictures, $"{playList.Id}.{pictureHandler.FileExtension}"); @@ -119,33 +134,19 @@ namespace Musify.Application.PlayLists.Handlers playListConfiguration.Routes.LargePictures, $"{playList.Id}.{pictureHandler.FileExtension}"); - try - { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.BucketName, - newImageStorageKey, - smallPictureKeyName, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - mediumPictureKeyName, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - largePictureKeyName, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish resize picture event for PlayList id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); - } - - database.PlayLists.Update(playList); - await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKeyName}.", playList.Id, newImageStorageKey); - return Result.Success(PlayListResponse.FromEntity(playList)); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + originalPictureKeyName, + smallPictureKeyName, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight, + mediumPictureKeyName, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight, + largePictureKeyName, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index a161797..dd0035e 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -82,7 +82,7 @@ namespace Musify.Application.Tracks.Handler trackConfiguration.Routes.LargePictures, $"{track.Id}.{pictureHandler.FileExtension}"); - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, storageConfiguration.BucketName, originalPictureKeyName, From b93c0996bf40c203b42426b7278a334cc703fcdf Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 21 Mar 2026 22:23:31 +0100 Subject: [PATCH 063/172] Audio Transcoder --- .../Infrastructure/IAudioTranscoder.cs | 9 ++ Infrastructure/Audio/AudioTranscoder.cs | 92 +++++++++++++++++++ .../AudioTranscoderDependencyInjection.cs | 14 +++ .../AudioTranscoderConfiguration.cs | 22 +++++ .../ConfigurationDependencyInjection.cs | 3 +- .../InfrastructureDependencyInjection.cs | 3 + 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Application/Contracts/Infrastructure/IAudioTranscoder.cs create mode 100644 Infrastructure/Audio/AudioTranscoder.cs create mode 100644 Infrastructure/Audio/AudioTranscoderDependencyInjection.cs create mode 100644 Infrastructure/Configuration/AudioTranscoderConfiguration.cs diff --git a/Application/Contracts/Infrastructure/IAudioTranscoder.cs b/Application/Contracts/Infrastructure/IAudioTranscoder.cs new file mode 100644 index 0000000..1ec7092 --- /dev/null +++ b/Application/Contracts/Infrastructure/IAudioTranscoder.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; + +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IAudioTranscoder + { + Task TranscodeToDash(Stream audioStream, string folderName, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Audio/AudioTranscoder.cs new file mode 100644 index 0000000..979166f --- /dev/null +++ b/Infrastructure/Audio/AudioTranscoder.cs @@ -0,0 +1,92 @@ +using Ardalis.Result; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; +using System.Diagnostics; + +namespace Musify.Infrastructure.Audio +{ + public class AudioTranscoder(ILogger logger, AudioTranscoderConfiguration transcoderConfiguration) + : IAudioTranscoder + { + const string FfmpegProcessName = "ffmpeg"; + + public async Task TranscodeToDash(Stream audioStream, string folderName, CancellationToken cancellationToken) + { + var workingDirectory = Path.Combine(transcoderConfiguration.Routes.WorkingDirectory, folderName); + + try + { + Directory.CreateDirectory(workingDirectory); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = FfmpegProcessName, + WorkingDirectory = workingDirectory, + Arguments = BuildDashArguments(workingDirectory), + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + + if (audioStream.CanSeek) + audioStream.Position = 0; + await audioStream.CopyToAsync(process.StandardInput.BaseStream, cancellationToken); + process.StandardInput.Close(); + + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + + using var linkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkedCancellationTokens.CancelAfter(transcoderConfiguration.TranscodingTimeout); + await process.WaitForExitAsync(linkedCancellationTokens.Token); + + var standardError = await errorTask; + var standardOutput = await outputTask; + + if (process.ExitCode != 0) + { + logger.LogError("Audio transcoding to DASH format failed with exit code {ExitCode}. Standard Output: {StandardOutput}, Standard Error: {StandardError}", process.ExitCode, standardOutput, standardError); + return Result.Error($"Audio transcoding failed with exit code {process.ExitCode}. See logs for details."); + } + + logger.LogInformation("Audio transcoding to DASH format completed successfully. Uploading to storage..."); + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while transcoding audio to DASH format."); + return Result.Error(exception.Message); + } + } + + static string BuildDashArguments(string outputDirectory) + { + var manifestPath = Path.Combine(outputDirectory, "manifest.mpd"); + + return "-y -hide_banner -loglevel error " + + "-i pipe:0 " + + "-vn " + + "-map 0:a " + + "-c:a aac " + + "-b:a 128k " + + "-ac 2 " + + "-ar 48000 " + + "-profile:a aac_low " + + "-f dash " + + "-seg_duration 4 " + + "-streaming 1 " + + "-use_template 1 " + + "-use_timeline 0 " + + "-init_seg_name \"init-stream$RepresentationID$.m4s\" " + + "-media_seg_name \"chunk-stream$RepresentationID$-$Number$.m4s\" " + + $"\"{manifestPath}\""; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs b/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs new file mode 100644 index 0000000..f7e04c2 --- /dev/null +++ b/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs @@ -0,0 +1,14 @@ +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(); + return serviceDescriptors; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs new file mode 100644 index 0000000..c19ecca --- /dev/null +++ b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ +#pragma warning disable CS8618 + public class AudioTranscoderConfiguration + { + public const string SectionName = "AudioTranscoder"; + + [Required] + public AudioTranscoderRoutes Routes { get; set; } = new AudioTranscoderRoutes(); + + [Required] + public TimeSpan TranscodingTimeout { get; set; } = TimeSpan.FromMinutes(5); + } + + public class AudioTranscoderRoutes + { + [Required] + public string WorkingDirectory { get; set; } + } +} \ No newline at end of file diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index 10a6be7..f275aa4 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -13,7 +13,8 @@ namespace Musify.Infrastructure.Configuration .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName) .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName) - .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName); + .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName) + .AddValidatedConfiguration(configuration, AudioTranscoderConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 8923781..72263f8 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -1,6 +1,7 @@ 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.Messaging; @@ -21,6 +22,7 @@ namespace Musify.Infrastructure .AddKeycloakService(configuration) .AddStorageHandler() .AddPictureHandler() + .AddAudioTranscoder() .AddInfrastructureOpenTelemetry() .AddMassTransitClient() .AddScoped() @@ -36,6 +38,7 @@ namespace Musify.Infrastructure .AddDatabase(configuration) .AddStorageHandler() .AddPictureHandler() + .AddAudioTranscoder() .AddInfrastructureOpenTelemetry() .AddMassTransitConsumers() .AddScoped() From 6ab9a3d2c2e8bc8592432aad3841b9313a7148e6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 22 Mar 2026 04:02:17 +0100 Subject: [PATCH 064/172] First iteration --- .../Messaging/Activities/Logs/TranscodeDashAudioLog.cs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs diff --git a/Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs b/Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs new file mode 100644 index 0000000..2345392 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record TranscodeDashAudioLog(string WorkingDirectory); +} \ No newline at end of file From 678419814ad4f1816eb603b46d9c5536669c86c4 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 22 Mar 2026 04:02:30 +0100 Subject: [PATCH 065/172] Staged --- .../Configuration/TrackConfiguration.cs | 3 + .../Infrastructure/IAudioTranscoder.cs | 2 +- Application/Events/ProcessTrackEvent.cs | 4 - .../Events/TranscodeAudioFromTrackEvent.cs | 8 + .../Handler/CreateTrackCommandHandler.cs | 35 +- Infrastructure/Audio/AudioTranscoder.cs | 15 +- .../DownloadFileFromBucketArguments.cs | 7 + ...ts.cs => RemoveFileFromBucketArguments.cs} | 2 +- .../Arguments/TranscodeDashAudioArguments.cs | 4 + .../TransferFilesToBucketArguments.cs | 7 + .../Messaging/Activities/CopyFilesActivity.cs | 39 -- .../DownloadFileFromBucketActivity.cs | 41 ++ ...ity.cs => RemoveFileFromBucketActivity.cs} | 10 +- .../Activities/TranscodeDashAudioActivity.cs | 49 +++ .../Activities/TransferFilesToBucket.cs | 31 ++ .../Messaging/Consumers/RemoveFileConsumer.cs | 4 +- .../TranscodeAudioFromTrackConsumer.cs | 54 +++ .../UpdatePlayListPictureConsumer.cs | 4 +- .../Consumers/UpdateTrackPictureConsumer.cs | 4 +- .../Messaging/MessagingDependencyInjection.cs | 57 ++- .../20260322023145_FirstMigration.Designer.cs | 382 ++++++++++++++++++ .../20260322023145_FirstMigration.cs | 247 +++++++++++ .../Migrations/DatabaseModelSnapshot.cs | 379 +++++++++++++++++ 23 files changed, 1318 insertions(+), 70 deletions(-) delete mode 100644 Application/Events/ProcessTrackEvent.cs create mode 100644 Application/Events/TranscodeAudioFromTrackEvent.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs rename Infrastructure/Messaging/Activities/Arguments/{RemoveFileArguments.cs => RemoveFileFromBucketArguments.cs} (71%) create mode 100644 Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs delete mode 100644 Infrastructure/Messaging/Activities/CopyFilesActivity.cs create mode 100644 Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs rename Infrastructure/Messaging/Activities/{RemoveFilesActivity.cs => RemoveFileFromBucketActivity.cs} (89%) create mode 100644 Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs create mode 100644 Infrastructure/Messaging/Activities/TransferFilesToBucket.cs create mode 100644 Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs create mode 100644 Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index 298a66a..d352048 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -38,6 +38,9 @@ namespace Musify.Application.Configuration [Required] public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; + + [Required] + public string Audios { get; set; } = "Audios"; } public class TrackPicturesSizes diff --git a/Application/Contracts/Infrastructure/IAudioTranscoder.cs b/Application/Contracts/Infrastructure/IAudioTranscoder.cs index 1ec7092..81f8941 100644 --- a/Application/Contracts/Infrastructure/IAudioTranscoder.cs +++ b/Application/Contracts/Infrastructure/IAudioTranscoder.cs @@ -4,6 +4,6 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IAudioTranscoder { - Task TranscodeToDash(Stream audioStream, string folderName, CancellationToken cancellationToken); + Task TranscodeToDashAsync(Stream audioStream, string destinationPath, TimeSpan timeOut, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/ProcessTrackEvent.cs b/Application/Events/ProcessTrackEvent.cs deleted file mode 100644 index 30899f2..0000000 --- a/Application/Events/ProcessTrackEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Musify.Application.Events -{ - public record ProcessTrackEvent(); -} \ No newline at end of file diff --git a/Application/Events/TranscodeAudioFromTrackEvent.cs b/Application/Events/TranscodeAudioFromTrackEvent.cs new file mode 100644 index 0000000..69cd150 --- /dev/null +++ b/Application/Events/TranscodeAudioFromTrackEvent.cs @@ -0,0 +1,8 @@ +namespace Musify.Application.Events +{ + public record TranscodeAudioFromTrackEvent( + string SourceBucketName, + string SourceKeyName, + string DestinationBucketName, + string DestinationKeyName); +} \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index dd0035e..eeed857 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -52,19 +52,37 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to publish resize picture event for Track with id {track.Id}."); } + var audioKeyName = Path.Combine(trackConfiguration.Routes.Audios, $"{Guid.NewGuid() }.{request.Audio.FileType}"); + var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); + if (!audioUploadResult.IsSuccess) + { + logger.LogError("Failed to upload audio for track {TrackTitle}", request.Title); + return Result.Error($"Failed to upload audio for track {request.Title}"); + } + + try + { + await PublishTranscodeEvent(track.Id, audioUploadResult, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish transcode audio event for Track with id={TrackId}.", track.Id); + return Result.Error($"An error occurred while processing the audio for Track with id {track.Id}."); + } + await database.Tracks.AddAsync(track, cancellationToken); await database.SaveChangesAsync(cancellationToken); return Result.Success(); } - async Task> UploadFile(IFileData fileData, string originalPictureKeyName, CancellationToken cancellationToken) + async Task> UploadFile(IFileData fileData, string keyName, CancellationToken cancellationToken) { var uploadFile = await storageHandler.UploadFileAsync( fileData.FileStream, fileData.ContentType, storageConfiguration.BucketName, - originalPictureKeyName, + keyName, cancellationToken); return uploadFile; @@ -96,5 +114,18 @@ namespace Musify.Application.Tracks.Handler trackConfiguration.PicturesSizes.LargePictureWidth, trackConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } + + async Task PublishTranscodeEvent(Guid trackId, string audioKeyName, CancellationToken cancellationToken) + { + var destinationAudioKeyName = Path.Combine( + trackConfiguration.Routes.Audios, + trackId.ToString()); + + await eventBus.PublishAsync(new TranscodeAudioFromTrackEvent( + storageConfiguration.BucketName, + audioKeyName, + storageConfiguration.BucketName, + destinationAudioKeyName), cancellationToken); + } } } diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Audio/AudioTranscoder.cs index 979166f..9ae77ac 100644 --- a/Infrastructure/Audio/AudioTranscoder.cs +++ b/Infrastructure/Audio/AudioTranscoder.cs @@ -1,31 +1,28 @@ using Ardalis.Result; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.Configuration; using System.Diagnostics; namespace Musify.Infrastructure.Audio { - public class AudioTranscoder(ILogger logger, AudioTranscoderConfiguration transcoderConfiguration) + public class AudioTranscoder(ILogger logger) : IAudioTranscoder { const string FfmpegProcessName = "ffmpeg"; - public async Task TranscodeToDash(Stream audioStream, string folderName, CancellationToken cancellationToken) + public async Task TranscodeToDashAsync(Stream audioStream, string destinationPath, TimeSpan timeOut, CancellationToken cancellationToken) { - var workingDirectory = Path.Combine(transcoderConfiguration.Routes.WorkingDirectory, folderName); - try { - Directory.CreateDirectory(workingDirectory); + Directory.CreateDirectory(destinationPath); using var process = new Process { StartInfo = new ProcessStartInfo { FileName = FfmpegProcessName, - WorkingDirectory = workingDirectory, - Arguments = BuildDashArguments(workingDirectory), + WorkingDirectory = destinationPath, + Arguments = BuildDashArguments(destinationPath), RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, @@ -44,7 +41,7 @@ namespace Musify.Infrastructure.Audio var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); using var linkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - linkedCancellationTokens.CancelAfter(transcoderConfiguration.TranscodingTimeout); + linkedCancellationTokens.CancelAfter(timeOut); await process.WaitForExitAsync(linkedCancellationTokens.Token); var standardError = await errorTask; diff --git a/Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs b/Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs new file mode 100644 index 0000000..3b4ed66 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs @@ -0,0 +1,7 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record DownloadFileFromBucketArguments( + string BucketName, + string KeyName, + string DestinationPath); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs b/Infrastructure/Messaging/Activities/Arguments/RemoveFileFromBucketArguments.cs similarity index 71% rename from Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs rename to Infrastructure/Messaging/Activities/Arguments/RemoveFileFromBucketArguments.cs index a0e8851..23e7058 100644 --- a/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs +++ b/Infrastructure/Messaging/Activities/Arguments/RemoveFileFromBucketArguments.cs @@ -1,6 +1,6 @@ namespace Musify.Infrastructure.Messaging.Activities.Arguments { - public record RemoveFileArguments( + public record RemoveFileFromBucketArguments( string BucketName, string KeyName); } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs new file mode 100644 index 0000000..ddbd8c4 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record TranscodeDashAudioArguments(string SourceFilePath, string DestinationFolderName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs b/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs new file mode 100644 index 0000000..8f39ecd --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs @@ -0,0 +1,7 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record TransferFilesToBucketArguments( + string FolderPath, + string DestinationBucketName, + string DestinationKeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/CopyFilesActivity.cs b/Infrastructure/Messaging/Activities/CopyFilesActivity.cs deleted file mode 100644 index 64499c2..0000000 --- a/Infrastructure/Messaging/Activities/CopyFilesActivity.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class CopyFilesActivity(IStorageHandler storageHandler, ILogger logger) - : IExecuteActivity - { - public async Task Execute(ExecuteContext executeContext) - { - foreach (var file in executeContext.Arguments) - { - var copyFileResult = await storageHandler.CopyFileAsync( - file.SourceBucketName, - file.SourceKeyName, - file.DestinationBucketName, - file.SourceKeyName, - executeContext.CancellationToken); - - if (!copyFileResult.IsSuccess) - { - string errorMessage = string.Join("; ", copyFileResult.Errors); - logger.LogError( - "Failed to copy file from {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}. Error: {ErrorMessage}", - file.SourceBucketName, - file.SourceKeyName, - file.DestinationBucketName, - file.DestinationKeyName, - errorMessage); - throw new Exception(errorMessage); - } - } - - return executeContext.Completed(); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs new file mode 100644 index 0000000..37bf996 --- /dev/null +++ b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs @@ -0,0 +1,41 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class DownloadFileFromBucketActivity(IStorageHandler storageHandler, ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "Download-File-From-Bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + var getFile = await storageHandler.GetFileAsync( + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + executeContext.CancellationToken); + + if (!getFile.IsSuccess) + { + var error = string.Join("; ", getFile.Errors); + logger.LogError("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}", + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName); + throw new Exception(error); + } + + using var fileStream = getFile.Value; + if (fileStream.CanSeek) + fileStream.Position = 0; + + var fileName = Path.GetFileName(executeContext.Arguments.KeyName); + using var destinationStream = File.Create($"D:\\tempsFilesDev\\{fileName}"); + await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs b/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs similarity index 89% rename from Infrastructure/Messaging/Activities/RemoveFilesActivity.cs rename to Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs index fd0b6b8..185260e 100644 --- a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs +++ b/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs @@ -6,15 +6,15 @@ using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class RemoveFilesActivity( + public class RemoveFileFromBucketActivity( IStorageHandler storageHandler, - ILogger logger, + ILogger logger, IProcessTrackingStore processTrackingStore) - : IExecuteActivity + : IExecuteActivity { public const string ExecuteEndpointName = "Remove-File"; - public async Task Execute(ExecuteContext executeContext) + public async Task Execute(ExecuteContext executeContext) { var processId = await processTrackingStore.GetOrCreateProcessAsync( "RoutingSlip", @@ -25,7 +25,7 @@ namespace Musify.Infrastructure.Messaging.Activities var stepId = await processTrackingStore.StartStepAsync( processId, - nameof(RemoveFilesActivity), + nameof(RemoveFileFromBucketActivity), ProcessStepComponentType.Activity, 0, executeContext.CancellationToken); diff --git a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs new file mode 100644 index 0000000..659afbb --- /dev/null +++ b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs @@ -0,0 +1,49 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class TranscodeDashAudioActivity(IAudioTranscoder audioTranscoder, ILogger logger, + AudioTranscoderConfiguration audioTranscoderConfiguration) + : IExecuteActivity + { + public const string ExecuteEndpointName = "Transcode-Dash-Audio"; + + public async Task Execute(ExecuteContext executeContext) + { + var workingDirectory = Path.Combine( + audioTranscoderConfiguration.Routes.WorkingDirectory, + executeContext.Arguments.DestinationFolderName); + + try + { + await using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); + + var result = await audioTranscoder.TranscodeToDashAsync( + fileStream, + executeContext.Arguments.DestinationFolderName, + audioTranscoderConfiguration.TranscodingTimeout, + executeContext.CancellationToken); + + if (!result.IsSuccess) + { + var errors = string.Join("; ", result.Errors); + logger.LogError("transcoding failed for file {SourceFilePath}. Error: {ErrorMessage}", + executeContext.Arguments.SourceFilePath, errors); + throw new Exception(errors); + } + + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error during transcoding activity."); + throw; + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs new file mode 100644 index 0000000..76c97a8 --- /dev/null +++ b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs @@ -0,0 +1,31 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class TransferFilesToBucket(IStorageHandler storageHandler, ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "Transfer-Files-To-Bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + var transferFilesResult = await storageHandler.TransferFilesAsync( + executeContext.Arguments.FolderPath, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + executeContext.CancellationToken); + + if (!transferFilesResult.IsSuccess) + { + var errors = string.Join("; ", transferFilesResult.Errors); + logger.LogError($"An error occurred while transferring files to bucket: {errors}"); + throw new Exception(errors); + } + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index a057f45..79a2650 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -16,8 +16,8 @@ namespace Musify.Infrastructure.Messaging.Consumers routingSlipBuilder.AddActivity( "RemoveFile", - BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), - new RemoveFileArguments( + BuildExecuteUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( consumeContext.Message.BucketName, consumeContext.Message.KeyName)); diff --git a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs new file mode 100644 index 0000000..a50204b --- /dev/null +++ b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -0,0 +1,54 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class TranscodeAudioFromTrackConsumer(IBus bus, AudioTranscoderConfiguration audioTranscoderConfiguration) + : IConsumer + { + public const string QueueName = "Transcode-Audio-From-Track"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + var folderName = Guid.NewGuid().ToString(); + var workingDirectory = Path.Combine("D:\\tempsFilesDev\\"); + + routingSlipBuilder.AddActivity( + "DownloadFile", + BuildExecuteUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( + consumeContext.Message.SourceBucketName, + consumeContext.Message.SourceKeyName, + workingDirectory) + ); + var fileName = Path.GetFileName(consumeContext.Message.SourceKeyName); + + routingSlipBuilder.AddActivity( + "TranscodeAudio", + BuildExecuteUri(TranscodeDashAudioActivity.ExecuteEndpointName), + new TranscodeDashAudioArguments( + $"D:\\tempsFilesDev\\{fileName}", + "D:\\tempsFilesDev\\") + ); + + routingSlipBuilder.AddActivity( + "UploadFile", + BuildExecuteUri(TransferFilesToBucket.ExecuteEndpointName), + new TransferFilesToBucketArguments( + workingDirectory, + consumeContext.Message.DestinationBucketName, + consumeContext.Message.DestinationKeyName) + ); + + var routingSlip = routingSlipBuilder.Build(); + await bus.Execute(routingSlip); + } + + static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index e542bc0..b9271a1 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -43,8 +43,8 @@ namespace Musify.Infrastructure.Messaging.Consumers routingSlipBuilder.AddActivity( "DeleteOriginal", - BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), - new RemoveFileArguments( + BuildExecuteUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( consumeContext.Message.BucketName, consumeContext.Message.OriginalPictureKeyName) ); diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs index 00e0911..74a4fdd 100644 --- a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -43,8 +43,8 @@ namespace Musify.Infrastructure.Messaging.Consumers routingSlipBuilder.AddActivity( "DeleteOriginal", - BuildExecuteUri(RemoveFilesActivity.ExecuteEndpointName), - new RemoveFileArguments( + BuildExecuteUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( consumeContext.Message.BucketName, consumeContext.Message.OriginalPictureKeyName) ); diff --git a/Infrastructure/Messaging/MessagingDependencyInjection.cs b/Infrastructure/Messaging/MessagingDependencyInjection.cs index 0613f72..d43bc7e 100644 --- a/Infrastructure/Messaging/MessagingDependencyInjection.cs +++ b/Infrastructure/Messaging/MessagingDependencyInjection.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; using Musify.Infrastructure.Messaging.Consumers; using Musify.Infrastructure.Messaging.Filters; @@ -29,9 +30,15 @@ namespace Musify.Infrastructure.Messaging { options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + options.AddExecuteActivity(); - options.AddExecuteActivity(); + options.AddExecuteActivity(); options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -51,20 +58,45 @@ namespace Musify.Infrastructure.Messaging endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); + busFactoryConfigurator.ReceiveEndpoint(TranscodeAudioFromTrackConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + + busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + ConfigureExecuteActivityEndpoint( busFactoryConfigurator, busRegistrationContext, ResizePictureActivity.ExecuteEndpointName); - ConfigureExecuteActivityEndpoint( + ConfigureExecuteActivityEndpoint( busFactoryConfigurator, busRegistrationContext, - RemoveFilesActivity.ExecuteEndpointName); + RemoveFileFromBucketActivity.ExecuteEndpointName); ConfigureExecuteActivityEndpoint( busFactoryConfigurator, busRegistrationContext, UpdatePlayListPictureActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackPictureActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + DownloadFileFromBucketActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + TranscodeDashAudioActivity.ExecuteEndpointName); } ); } ); @@ -94,5 +126,24 @@ namespace Musify.Infrastructure.Messaging endpointConfigurator.ExecuteActivityHost(busRegistrationContext); } ); } + + static void ConfigureActivityEndpoints( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string endpointName) + where TActivity : class, IActivity + where TArguments : class + where TLog : class + { + busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => + { + endpointConfigurator.ExecuteActivityHost(busRegistrationContext); + } ); + + busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_compensate", endpointConfigurator => + { + endpointConfigurator.CompensateActivityHost(busRegistrationContext); + } ); + } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs b/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs new file mode 100644 index 0000000..da7a421 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs @@ -0,0 +1,382 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260322023145_FirstMigration")] + partial class FirstMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("character varying(254)"); + + b.Property("LargePictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SmallPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("ErrorMessage") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FinishedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ProcessName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StartedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId", "ProcessName"); + + b.ToTable("ProcessExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Attempt") + .HasColumnType("integer"); + + b.Property("ComponentType") + .HasColumnType("integer"); + + b.Property("ErrorMessage") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FinishedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ProcessExecutionId") + .HasColumnType("uuid"); + + b.Property("StartedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StepName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ProcessExecutionId", "StepName"); + + b.ToTable("ProcessStepExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SmallPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("KeyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => + { + b.HasOne("Musify.Domain.Entities.ProcessExecution", "ProcessExecution") + .WithMany("Steps") + .HasForeignKey("ProcessExecutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProcessExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs new file mode 100644 index 0000000..e32ecc2 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs @@ -0,0 +1,247 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class FirstMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ProcessExecution", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CorrelationId = table.Column(type: "uuid", nullable: true), + ConversationId = table.Column(type: "uuid", nullable: true), + MessageId = table.Column(type: "uuid", nullable: true), + ProcessName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false), + StartedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + FinishedDateTime = table.Column(type: "timestamp with time zone", nullable: true), + ErrorMessage = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProcessExecution", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tracks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + NormalizedTitle = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tracks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Upload", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + KeyName = table.Column(type: "text", nullable: false), + BucketName = table.Column(type: "text", nullable: false), + ContentType = table.Column(type: "text", nullable: false), + CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), + State = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Upload", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "User", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_User", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ProcessStepExecution", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProcessExecutionId = table.Column(type: "uuid", nullable: false), + StepName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + ComponentType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Attempt = table.Column(type: "integer", nullable: false), + StartedDateTime = table.Column(type: "timestamp with time zone", nullable: false), + FinishedDateTime = table.Column(type: "timestamp with time zone", nullable: true), + ErrorMessage = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProcessStepExecution", x => x.Id); + table.ForeignKey( + name: "FK_ProcessStepExecution_ProcessExecution_ProcessExecutionId", + column: x => x.ProcessExecutionId, + principalTable: "ProcessExecution", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PlayLists", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + NormalizedName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Description = table.Column(type: "character varying(254)", maxLength: 254, nullable: false), + SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayLists", x => x.Id); + table.ForeignKey( + name: "FK_PlayLists_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserHasTrack", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + TrackId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserHasTrack", x => x.Id); + table.ForeignKey( + name: "FK_UserHasTrack_Tracks_TrackId", + column: x => x.TrackId, + principalTable: "Tracks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserHasTrack_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PlayListHasTrack", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PlayListId = table.Column(type: "uuid", nullable: false), + TrackId = table.Column(type: "uuid", nullable: false), + Position = table.Column(type: "integer", nullable: false), + AddedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayListHasTrack", x => x.Id); + table.ForeignKey( + name: "FK_PlayListHasTrack_PlayLists_PlayListId", + column: x => x.PlayListId, + principalTable: "PlayLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlayListHasTrack_Tracks_TrackId", + column: x => x.TrackId, + principalTable: "Tracks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PlayListHasTrack_PlayListId", + table: "PlayListHasTrack", + column: "PlayListId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayListHasTrack_TrackId", + table: "PlayListHasTrack", + column: "TrackId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayLists_UserId", + table: "PlayLists", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_ProcessExecution_CorrelationId_ProcessName", + table: "ProcessExecution", + columns: new[] { "CorrelationId", "ProcessName" }); + + migrationBuilder.CreateIndex( + name: "IX_ProcessStepExecution_ProcessExecutionId_StepName", + table: "ProcessStepExecution", + columns: new[] { "ProcessExecutionId", "StepName" }); + + migrationBuilder.CreateIndex( + name: "IX_UserHasTrack_TrackId", + table: "UserHasTrack", + column: "TrackId"); + + migrationBuilder.CreateIndex( + name: "IX_UserHasTrack_UserId", + table: "UserHasTrack", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayListHasTrack"); + + migrationBuilder.DropTable( + name: "ProcessStepExecution"); + + migrationBuilder.DropTable( + name: "Upload"); + + migrationBuilder.DropTable( + name: "UserHasTrack"); + + migrationBuilder.DropTable( + name: "PlayLists"); + + migrationBuilder.DropTable( + name: "ProcessExecution"); + + migrationBuilder.DropTable( + name: "Tracks"); + + migrationBuilder.DropTable( + name: "User"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs new file mode 100644 index 0000000..f859ef0 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -0,0 +1,379 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + partial class DatabaseModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("character varying(254)"); + + b.Property("LargePictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SmallPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("ErrorMessage") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FinishedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ProcessName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StartedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId", "ProcessName"); + + b.ToTable("ProcessExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Attempt") + .HasColumnType("integer"); + + b.Property("ComponentType") + .HasColumnType("integer"); + + b.Property("ErrorMessage") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FinishedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ProcessExecutionId") + .HasColumnType("uuid"); + + b.Property("StartedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StepName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ProcessExecutionId", "StepName"); + + b.ToTable("ProcessStepExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SmallPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("KeyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => + { + b.HasOne("Musify.Domain.Entities.ProcessExecution", "ProcessExecution") + .WithMany("Steps") + .HasForeignKey("ProcessExecutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProcessExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} From 27ec670e3a98b6fd44da699aa40fdd68494024e6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 22 Mar 2026 04:02:50 +0100 Subject: [PATCH 066/172] Missing file --- .../Messaging/Activities/Logs/DownloadFileFromBucketLog.cs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs diff --git a/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs new file mode 100644 index 0000000..5c60a7e --- /dev/null +++ b/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs @@ -0,0 +1,7 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record DownloadFileFromBucketLog( + string BucketName, + string KeyName, + string DestinationPath); +} \ No newline at end of file From 4ec4b50c407d177d8934b2ca1f0810b9bcfc5dc1 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 23 Mar 2026 03:13:08 +0100 Subject: [PATCH 067/172] Changes but failing --- Application/Events/UpdateTrackPictureEvent.cs | 4 +- .../Handler/CreateTrackCommandHandler.cs | 16 ++-- .../Arguments/ResizePictureLocalArguments.cs | 8 ++ .../DownloadFileFromBucketActivity.cs | 75 ++++++++++++----- .../RemoveFileFromBucketActivity.cs | 8 +- .../Activities/ResizePictureActivity.cs | 54 ++++++------ .../Activities/TranscodeDashAudioActivity.cs | 40 ++++++--- .../Activities/TransferFilesToBucket.cs | 58 ++++++++++--- .../UpdatePlayListPictureActivity.cs | 21 ++++- .../Activities/UpdateTrackPictureActivity.cs | 21 ++++- .../Messaging/Consumers/MessagingHelper.cs | 7 ++ .../Messaging/Consumers/RemoveFileConsumer.cs | 4 +- .../TranscodeAudioFromTrackConsumer.cs | 28 +++---- .../UpdatePlayListPictureConsumer.cs | 59 +++++++++---- .../Consumers/UpdateTrackPictureConsumer.cs | 82 +++++++++++++------ .../Messaging/MessagingDependencyInjection.cs | 37 +++++---- 16 files changed, 362 insertions(+), 160 deletions(-) create mode 100644 Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs create mode 100644 Infrastructure/Messaging/Consumers/MessagingHelper.cs diff --git a/Application/Events/UpdateTrackPictureEvent.cs b/Application/Events/UpdateTrackPictureEvent.cs index ada8d60..1ba00be 100644 --- a/Application/Events/UpdateTrackPictureEvent.cs +++ b/Application/Events/UpdateTrackPictureEvent.cs @@ -2,8 +2,8 @@ { public record UpdateTrackPictureEvent( Guid TrackId, - string BucketName, - string OriginalPictureKeyName, + string SourceBucketName, + string SourceKeyName, string SmallPictureKeyName, int SmallPictureWidth, int SmallPictureHeight, diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index eeed857..6b0b7b1 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -42,6 +42,14 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to upload picture for track {request.Title}"); } + var audioKeyName = Path.Combine(trackConfiguration.Routes.Audios, $"{Guid.NewGuid() }.{request.Audio.FileType}"); + var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); + if (!audioUploadResult.IsSuccess) + { + logger.LogError("Failed to upload audio for track {TrackTitle}", request.Title); + return Result.Error($"Failed to upload audio for track {request.Title}"); + } + try { await PublishUpdateEvent(originalPictureKeyName, track, cancellationToken); @@ -52,14 +60,6 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to publish resize picture event for Track with id {track.Id}."); } - var audioKeyName = Path.Combine(trackConfiguration.Routes.Audios, $"{Guid.NewGuid() }.{request.Audio.FileType}"); - var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); - if (!audioUploadResult.IsSuccess) - { - logger.LogError("Failed to upload audio for track {TrackTitle}", request.Title); - return Result.Error($"Failed to upload audio for track {request.Title}"); - } - try { await PublishTranscodeEvent(track.Id, audioUploadResult, cancellationToken); diff --git a/Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs b/Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs new file mode 100644 index 0000000..3e7ed42 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record ResizePictureLocalArguments( + string SourceFilePath, + string DestinationFilePath, + int Width, + int Height); +} diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs index 37bf996..b0f33b3 100644 --- a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs @@ -1,41 +1,76 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; namespace Musify.Infrastructure.Messaging.Activities { - public class DownloadFileFromBucketActivity(IStorageHandler storageHandler, ILogger logger) + public class DownloadFileFromBucketActivity( + IStorageHandler storageHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore, + AudioTranscoderConfiguration audioTranscoderConfiguration) : IExecuteActivity { public const string ExecuteEndpointName = "Download-File-From-Bucket"; public async Task Execute(ExecuteContext executeContext) { - var getFile = await storageHandler.GetFileAsync( - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, executeContext.CancellationToken); - if (!getFile.IsSuccess) + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(DownloadFileFromBucketActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try { - var error = string.Join("; ", getFile.Errors); - logger.LogError("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}", + var getFile = await storageHandler.GetFileAsync( executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName); - throw new Exception(error); + executeContext.Arguments.KeyName, + executeContext.CancellationToken); + + if (!getFile.IsSuccess) + { + var errorMessage = string.Join("; ", getFile.Errors); + logger.LogError("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + errorMessage); + throw new Exception(errorMessage); + } + + using var fileStream = getFile.Value; + if (fileStream.CanSeek) + fileStream.Position = 0; + + var destinationPath = Path.Combine( + audioTranscoderConfiguration.Routes.WorkingDirectory, + Path.GetFileName(executeContext.Arguments.KeyName)); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + + using var destinationStream = File.Create(destinationPath); + await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error downloading file from bucket"); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; } - - using var fileStream = getFile.Value; - if (fileStream.CanSeek) - fileStream.Position = 0; - - var fileName = Path.GetFileName(executeContext.Arguments.KeyName); - using var destinationStream = File.Create($"D:\\tempsFilesDev\\{fileName}"); - await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); - - return executeContext.Completed(); } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs index 185260e..28ff4f4 100644 --- a/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs @@ -39,9 +39,8 @@ namespace Musify.Infrastructure.Messaging.Activities if (!removeFile.IsSuccess) { - string errorMessage = string.Join(";", removeFile.Errors); - logger.LogWarning( - "Failed to remove file {KeyName} from bucket {BucketName}: {ErrorMessage}", + var errorMessage = string.Join("; ", removeFile.Errors); + logger.LogWarning("Failed to remove file {KeyName} from bucket {BucketName}. Errors: {Errors}", executeContext.Arguments.KeyName, executeContext.Arguments.BucketName, errorMessage); @@ -52,6 +51,9 @@ namespace Musify.Infrastructure.Messaging.Activities } catch (Exception exception) { + logger.LogError(exception, "Error removing file {KeyName} from bucket {BucketName}", + executeContext.Arguments.KeyName, + executeContext.Arguments.BucketName); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs index d28292e..8e0d0e6 100644 --- a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -1,4 +1,5 @@ using MassTransit; +using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; @@ -6,14 +7,14 @@ using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { public class ResizePictureActivity( - IStorageHandler storageHandler, IPictureHandler pictureHandler, + ILogger logger, IProcessTrackingStore processTrackingStore) - : IExecuteActivity + : IExecuteActivity { public const string ExecuteEndpointName = "Resize-Picture"; - public async Task Execute(ExecuteContext executeContext) + public async Task Execute(ExecuteContext executeContext) { var processId = await processTrackingStore.GetOrCreateProcessAsync( "RoutingSlip", @@ -31,47 +32,54 @@ namespace Musify.Infrastructure.Messaging.Activities try { - var file = await storageHandler.GetFileAsync( - executeContext.Arguments.OriginalBucketName, - executeContext.Arguments.OriginalKeyName, - executeContext.CancellationToken); - - if (!file.IsSuccess) + if (!File.Exists(executeContext.Arguments.SourceFilePath)) { - string errorMessages = string.Join(", ", file.Errors); - throw new Exception(errorMessages); + logger.LogError("Source file not found at {SourceFilePath}", + executeContext.Arguments.SourceFilePath); + throw new FileNotFoundException($"Source file not found: {executeContext.Arguments.SourceFilePath}"); } + await using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); + var resizedPicture = await pictureHandler.ResizePictureAsync( - file.Value, + fileStream, executeContext.Arguments.Width, executeContext.Arguments.Height, executeContext.CancellationToken); if (!resizedPicture.IsSuccess) { - string errorMessage = string.Join(", ", resizedPicture.Errors); + var errorMessage = string.Join("; ", resizedPicture.Errors); + logger.LogError("Failed to resize picture {SourceFilePath} to {Width}x{Height}. Errors: {Errors}", + executeContext.Arguments.SourceFilePath, + executeContext.Arguments.Width, + executeContext.Arguments.Height, + errorMessage); throw new Exception(errorMessage); } - var uploadFile = await storageHandler.UploadFileAsync( - resizedPicture.Value, - pictureHandler.ContentType, - executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, - executeContext.CancellationToken); - - if (!uploadFile.IsSuccess) + var destinationDirectory = Path.GetDirectoryName(executeContext.Arguments.DestinationFilePath); + if (!string.IsNullOrEmpty(destinationDirectory)) { - var errorMessage = string.Join(", ", uploadFile.Errors); - throw new Exception(errorMessage); + Directory.CreateDirectory(destinationDirectory); } + await using var destinationStream = File.Create(executeContext.Arguments.DestinationFilePath); + await resizedPicture.Value.CopyToAsync(destinationStream, executeContext.CancellationToken); + + logger.LogInformation("Picture resized successfully from {SourceFilePath} to {DestinationFilePath} ( {Width}x{Height} )", + executeContext.Arguments.SourceFilePath, + executeContext.Arguments.DestinationFilePath, + executeContext.Arguments.Width, + executeContext.Arguments.Height); + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { + logger.LogError(exception, "Error resizing picture {SourceFilePath}", + executeContext.Arguments.SourceFilePath); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs index 659afbb..ae8ba5e 100644 --- a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs +++ b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs @@ -1,13 +1,16 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; namespace Musify.Infrastructure.Messaging.Activities { - public class TranscodeDashAudioActivity(IAudioTranscoder audioTranscoder, ILogger logger, + public class TranscodeDashAudioActivity( + IAudioTranscoder audioTranscoder, + ILogger logger, + IProcessTrackingStore processTrackingStore, AudioTranscoderConfiguration audioTranscoderConfiguration) : IExecuteActivity { @@ -15,12 +18,26 @@ namespace Musify.Infrastructure.Messaging.Activities public async Task Execute(ExecuteContext executeContext) { - var workingDirectory = Path.Combine( - audioTranscoderConfiguration.Routes.WorkingDirectory, - executeContext.Arguments.DestinationFolderName); + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(TranscodeDashAudioActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); try { + var workingDirectory = Path.Combine( + audioTranscoderConfiguration.Routes.WorkingDirectory, + executeContext.Arguments.DestinationFolderName); + await using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); var result = await audioTranscoder.TranscodeToDashAsync( @@ -31,17 +48,20 @@ namespace Musify.Infrastructure.Messaging.Activities if (!result.IsSuccess) { - var errors = string.Join("; ", result.Errors); - logger.LogError("transcoding failed for file {SourceFilePath}. Error: {ErrorMessage}", - executeContext.Arguments.SourceFilePath, errors); - throw new Exception(errors); + var errorMessage = string.Join("; ", result.Errors); + logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", + executeContext.Arguments.SourceFilePath, errorMessage); + throw new Exception(errorMessage); } + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { - logger.LogError(exception, "Error during transcoding activity."); + logger.LogError(exception, "Error during transcoding activity for file {SourceFilePath}", + executeContext.Arguments.SourceFilePath); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs index 76c97a8..b3f1ea9 100644 --- a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs +++ b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs @@ -1,31 +1,65 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class TransferFilesToBucket(IStorageHandler storageHandler, ILogger logger) + public class TransferFilesToBucket( + IStorageHandler storageHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Transfer-Files-To-Bucket"; public async Task Execute(ExecuteContext executeContext) { - var transferFilesResult = await storageHandler.TransferFilesAsync( - executeContext.Arguments.FolderPath, - executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, executeContext.CancellationToken); - if (!transferFilesResult.IsSuccess) - { - var errors = string.Join("; ", transferFilesResult.Errors); - logger.LogError($"An error occurred while transferring files to bucket: {errors}"); - throw new Exception(errors); - } + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(TransferFilesToBucket), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); - return executeContext.Completed(); + try + { + var transferFilesResult = await storageHandler.TransferFilesAsync( + executeContext.Arguments.FolderPath, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + executeContext.CancellationToken); + + if (!transferFilesResult.IsSuccess) + { + var errorMessage = string.Join("; ", transferFilesResult.Errors); + logger.LogError("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", + executeContext.Arguments.FolderPath, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + errorMessage); + throw new Exception(errorMessage); + } + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error transferring files from {FolderPath} to bucket {DestinationBucketName}", + executeContext.Arguments.FolderPath, + executeContext.Arguments.DestinationBucketName); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } } } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs index cbd4c1b..82ae1c3 100644 --- a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -1,11 +1,15 @@ using MassTransit; +using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class UpdatePlayListPictureActivity(IDatabase database, IProcessTrackingStore processTrackingStore) + public class UpdatePlayListPictureActivity( + IDatabase database, + ILogger logger, + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Update-PlayList-Picture"; @@ -28,10 +32,16 @@ namespace Musify.Infrastructure.Messaging.Activities try { - var playList = await database.PlayLists.FindAsync(executeContext.Arguments.PlayListId, executeContext.CancellationToken); + var playList = await database.PlayLists.FindAsync( + new object[] { executeContext.Arguments.PlayListId }, + cancellationToken: executeContext.CancellationToken); if (playList is null) - throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); + { + logger.LogError("PlayList with id {PlayListId} not found", + executeContext.Arguments.PlayListId); + throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); + } playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; @@ -40,11 +50,16 @@ namespace Musify.Infrastructure.Messaging.Activities database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); + logger.LogInformation("Updated PlayList {PlayListId} pictures", + executeContext.Arguments.PlayListId); + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { + logger.LogError(exception, "Error updating PlayList {PlayListId} pictures", + executeContext.Arguments.PlayListId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs index 63671de..103407e 100644 --- a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs @@ -1,11 +1,15 @@ using MassTransit; +using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities { - public class UpdateTrackPictureActivity(IDatabase database, IProcessTrackingStore processTrackingStore) + public class UpdateTrackPictureActivity( + IDatabase database, + ILogger logger, + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Update-Track-Picture"; @@ -28,10 +32,16 @@ namespace Musify.Infrastructure.Messaging.Activities try { - var track = await database.Tracks.FindAsync(executeContext.Arguments.TrackId, executeContext.CancellationToken); + var track = await database.Tracks.FindAsync( + new object[] { executeContext.Arguments.TrackId }, + cancellationToken: executeContext.CancellationToken); if (track is null) - throw new Exception($"Track with id {executeContext.Arguments.TrackId} not found."); + { + logger.LogError("Track with id {TrackId} not found", + executeContext.Arguments.TrackId); + throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); + } track.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; track.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; @@ -40,11 +50,16 @@ namespace Musify.Infrastructure.Messaging.Activities database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); + logger.LogInformation("Updated Track {TrackId} pictures", + executeContext.Arguments.TrackId); + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { + logger.LogError(exception, "Error updating Track {TrackId} pictures", + executeContext.Arguments.TrackId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/Messaging/Consumers/MessagingHelper.cs b/Infrastructure/Messaging/Consumers/MessagingHelper.cs new file mode 100644 index 0000000..5fd9cbb --- /dev/null +++ b/Infrastructure/Messaging/Consumers/MessagingHelper.cs @@ -0,0 +1,7 @@ +namespace Musify.Infrastructure.Messaging.Consumers +{ + internal static class MessagingHelper + { + internal static Uri BuildExecuteActivityUri(string endpointName) => new($"queue:{endpointName}_execute"); + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 79a2650..8a78a1a 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Messaging.Consumers routingSlipBuilder.AddActivity( "RemoveFile", - BuildExecuteUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( consumeContext.Message.BucketName, consumeContext.Message.KeyName)); @@ -24,7 +24,5 @@ namespace Musify.Infrastructure.Messaging.Consumers var routingSlip = routingSlipBuilder.Build(); await bus.Execute(routingSlip); } - - static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); } } diff --git a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs index a50204b..41bfc49 100644 --- a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs +++ b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -6,7 +6,9 @@ using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Consumers { - public class TranscodeAudioFromTrackConsumer(IBus bus, AudioTranscoderConfiguration audioTranscoderConfiguration) + public class TranscodeAudioFromTrackConsumer( + IBus bus, + AudioTranscoderConfiguration audioTranscoderConfiguration) : IConsumer { public const string QueueName = "Transcode-Audio-From-Track"; @@ -14,41 +16,35 @@ namespace Musify.Infrastructure.Messaging.Consumers public async Task Consume(ConsumeContext consumeContext) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - + var fileName = Path.GetFileName(consumeContext.Message.SourceKeyName); var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine("D:\\tempsFilesDev\\"); + var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); routingSlipBuilder.AddActivity( "DownloadFile", - BuildExecuteUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( consumeContext.Message.SourceBucketName, consumeContext.Message.SourceKeyName, - workingDirectory) - ); - var fileName = Path.GetFileName(consumeContext.Message.SourceKeyName); + workingDirectory)); routingSlipBuilder.AddActivity( "TranscodeAudio", - BuildExecuteUri(TranscodeDashAudioActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), new TranscodeDashAudioArguments( - $"D:\\tempsFilesDev\\{fileName}", - "D:\\tempsFilesDev\\") - ); + Path.Combine(workingDirectory, fileName), + folderName)); routingSlipBuilder.AddActivity( "UploadFile", - BuildExecuteUri(TransferFilesToBucket.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( workingDirectory, consumeContext.Message.DestinationBucketName, - consumeContext.Message.DestinationKeyName) - ); + consumeContext.Message.DestinationKeyName)); var routingSlip = routingSlipBuilder.Build(); await bus.Execute(routingSlip); } - - static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index b9271a1..539b2aa 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -1,11 +1,14 @@ using MassTransit; using Musify.Application.Events; +using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Consumers { - public class UpdatePlayListPictureConsumer(IBus bus) + public class UpdatePlayListPictureConsumer( + IBus bus, + AudioTranscoderConfiguration audioTranscoderConfiguration) : IConsumer { public const string QueueName = "Update-PlayList-Picture"; @@ -13,6 +16,8 @@ namespace Musify.Infrastructure.Messaging.Consumers public async Task Consume(ConsumeContext consumeContext) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + var folderName = Guid.NewGuid().ToString(); + var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); AddResizeActivity( routingSlipBuilder, @@ -21,7 +26,8 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.SmallPictureWidth, consumeContext.Message.SmallPictureHeight, consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName); + consumeContext.Message.BucketName, + workingDirectory); AddResizeActivity( routingSlipBuilder, @@ -30,7 +36,8 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.MediumPictureWidth, consumeContext.Message.MediumPictureHeight, consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName); + consumeContext.Message.BucketName, + workingDirectory); AddResizeActivity( routingSlipBuilder, @@ -39,19 +46,27 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.LargePictureWidth, consumeContext.Message.LargePictureHeight, consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName); + consumeContext.Message.BucketName, + workingDirectory); routingSlipBuilder.AddActivity( "DeleteOriginal", - BuildExecuteUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( consumeContext.Message.BucketName, - consumeContext.Message.OriginalPictureKeyName) - ); + consumeContext.Message.OriginalPictureKeyName)); + + routingSlipBuilder.AddActivity( + "UploadFiles", + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + new TransferFilesToBucketArguments( + workingDirectory, + consumeContext.Message.BucketName, + folderName)); routingSlipBuilder.AddActivity( "UpdatePlayListPicture", - BuildExecuteUri(UpdatePlayListPictureActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), new UpdatePlayListPictureArguments( consumeContext.Message.PlayListId, consumeContext.Message.SmallPictureKeyName, @@ -69,20 +84,32 @@ namespace Musify.Infrastructure.Messaging.Consumers int width, int height, string originalPictureKeyName, - string bucketName) + string bucketName, + string workingDirectory) { + var fileName = Path.GetFileName(originalPictureKeyName); + var sourceFilePath = Path.Combine(workingDirectory, fileName); + var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); + var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); + + // First: Download original file routingSlipBuilder.AddActivity( - activityName, - BuildExecuteUri(ResizePictureActivity.ExecuteEndpointName), - new ResizePictureArgument( + $"{activityName}_Download", + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( bucketName, originalPictureKeyName, - bucketName, - destinationKeyName, + workingDirectory)); + + // Second: Resize picture + routingSlipBuilder.AddActivity( + activityName, + MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), + new ResizePictureLocalArguments( + sourceFilePath, + destinationFilePath, width, height)); } - - static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs index 74a4fdd..8133f6d 100644 --- a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -1,18 +1,31 @@ using MassTransit; using Musify.Application.Events; +using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Consumers { - public class UpdateTrackPictureConsumer(IBus bus) + public class UpdateTrackPictureConsumer( + IBus bus, + AudioTranscoderConfiguration audioTranscoderConfiguration) : IConsumer { - public const string QueueName = "update-track-picture"; + public const string QueueName = "Update-Track-Picture"; public async Task Consume(ConsumeContext consumeContext) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + var folderName = Guid.NewGuid().ToString(); + var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); + + routingSlipBuilder.AddActivity( + "DownloadFile", + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( + consumeContext.Message.SourceBucketName, + consumeContext.Message.SourceKeyName, + workingDirectory)); AddResizeActivity( routingSlipBuilder, @@ -20,38 +33,41 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.SmallPictureKeyName, consumeContext.Message.SmallPictureWidth, consumeContext.Message.SmallPictureHeight, - consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName); + consumeContext.Message.SourceKeyName, + consumeContext.Message.SourceBucketName, + workingDirectory); AddResizeActivity( routingSlipBuilder, "ResizeMedium", - consumeContext.Message.SmallPictureKeyName, + consumeContext.Message.MediumPictureKeyName, consumeContext.Message.MediumPictureWidth, consumeContext.Message.MediumPictureHeight, - consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName); + consumeContext.Message.SourceKeyName, + consumeContext.Message.SourceBucketName, + workingDirectory); AddResizeActivity( routingSlipBuilder, - "ResizeMedium", - consumeContext.Message.SmallPictureKeyName, + "ResizeLarge", + consumeContext.Message.LargePictureKeyName, consumeContext.Message.LargePictureWidth, consumeContext.Message.LargePictureHeight, - consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName); + consumeContext.Message.SourceKeyName, + consumeContext.Message.SourceBucketName, + workingDirectory); routingSlipBuilder.AddActivity( - "DeleteOriginal", - BuildExecuteUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new RemoveFileFromBucketArguments( - consumeContext.Message.BucketName, - consumeContext.Message.OriginalPictureKeyName) - ); + "UploadFiles", + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + new TransferFilesToBucketArguments( + workingDirectory, + consumeContext.Message.SourceBucketName, + folderName)); routingSlipBuilder.AddActivity( - "UpdateTrackPictureActivity", - BuildExecuteUri(UpdateTrackPictureActivity.ExecuteEndpointName), + "UpdateTrackPicture", + MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), new UpdateTrackPictureArguments( consumeContext.Message.TrackId, consumeContext.Message.SmallPictureKeyName, @@ -69,20 +85,32 @@ namespace Musify.Infrastructure.Messaging.Consumers int width, int height, string originalPictureKeyName, - string bucketName) + string bucketName, + string workingDirectory) { + var fileName = Path.GetFileName(originalPictureKeyName); + var sourceFilePath = Path.Combine(workingDirectory, fileName); + var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); + var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); + + // First: Download original file routingSlipBuilder.AddActivity( - activityName, - BuildExecuteUri(ResizePictureActivity.ExecuteEndpointName), - new ResizePictureArgument( + $"{activityName}_Download", + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( bucketName, originalPictureKeyName, - bucketName, - destinationKeyName, + workingDirectory)); + + // Second: Resize picture + routingSlipBuilder.AddActivity( + activityName, + MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), + new ResizePictureLocalArguments( + sourceFilePath, + destinationFilePath, width, height)); } - - static Uri BuildExecuteUri(string endpointName) => new($"queue:{endpointName}_execute"); } } \ No newline at end of file diff --git a/Infrastructure/Messaging/MessagingDependencyInjection.cs b/Infrastructure/Messaging/MessagingDependencyInjection.cs index d43bc7e..4c2324f 100644 --- a/Infrastructure/Messaging/MessagingDependencyInjection.cs +++ b/Infrastructure/Messaging/MessagingDependencyInjection.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; using Musify.Infrastructure.Messaging.Consumers; using Musify.Infrastructure.Messaging.Filters; @@ -19,7 +18,7 @@ namespace Musify.Infrastructure.Messaging ConfigureRabbitMqHost( busFactoryConfigurator, busRegistrationContext.GetRequiredService())); - } ); + }); return serviceDescriptors; } @@ -28,17 +27,20 @@ namespace Musify.Infrastructure.Messaging { serviceDescriptors.AddMassTransit(options => { + // Register consumers options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); - options.AddExecuteActivity(); + // Register execute activities + options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); + options.AddExecuteActivity(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -48,27 +50,29 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); + // Consumer endpoints busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + }); busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + }); busFactoryConfigurator.ReceiveEndpoint(TranscodeAudioFromTrackConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + }); busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + }); - ConfigureExecuteActivityEndpoint( + // Activity endpoints + ConfigureExecuteActivityEndpoint( busFactoryConfigurator, busRegistrationContext, ResizePictureActivity.ExecuteEndpointName); @@ -97,8 +101,13 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator, busRegistrationContext, TranscodeDashAudioActivity.ExecuteEndpointName); - } ); - } ); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + TransferFilesToBucket.ExecuteEndpointName); + }); + }); return serviceDescriptors; } @@ -111,7 +120,7 @@ namespace Musify.Infrastructure.Messaging { options.Username(massTransitConfiguration.Username); options.Password(massTransitConfiguration.Password); - } ); + }); } static void ConfigureExecuteActivityEndpoint( @@ -124,7 +133,7 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => { endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - } ); + }); } static void ConfigureActivityEndpoints( @@ -138,12 +147,12 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => { endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - } ); + }); busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_compensate", endpointConfigurator => { endpointConfigurator.CompensateActivityHost(busRegistrationContext); - } ); + }); } } } \ No newline at end of file From 54d2f946c8752cb4c9b68dc6ad96ffe9d4517f14 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 23 Mar 2026 09:38:02 +0100 Subject: [PATCH 068/172] Comments --- .../Messaging/Consumers/UpdatePlayListPictureConsumer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index 539b2aa..fbaa479 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -92,7 +92,6 @@ namespace Musify.Infrastructure.Messaging.Consumers var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); - // First: Download original file routingSlipBuilder.AddActivity( $"{activityName}_Download", MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), @@ -101,7 +100,6 @@ namespace Musify.Infrastructure.Messaging.Consumers originalPictureKeyName, workingDirectory)); - // Second: Resize picture routingSlipBuilder.AddActivity( activityName, MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), From 1c6233987926bce1e5fa3ef2ae3e8fe021984d03 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 23 Mar 2026 09:48:15 +0100 Subject: [PATCH 069/172] Download once --- .../Events/UpdatePlayListPictureEvent.cs | 4 +-- .../UpdatePlayListPictureConsumer.cs | 34 +++++++++---------- .../Consumers/UpdateTrackPictureConsumer.cs | 17 ++++------ 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs index f5590d9..5441f85 100644 --- a/Application/Events/UpdatePlayListPictureEvent.cs +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -2,8 +2,8 @@ { public record UpdatePlayListPictureEvent( Guid PlayListId, - string BucketName, - string OriginalPictureKeyName, + string SourceBucketName, + string SourceKeyName, string SmallPictureKeyName, int SmallPictureWidth, int SmallPictureHeight, diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index fbaa479..fc72103 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -19,14 +19,22 @@ namespace Musify.Infrastructure.Messaging.Consumers var folderName = Guid.NewGuid().ToString(); var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); + routingSlipBuilder.AddActivity( + "DownloadFile", + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( + consumeContext.Message.SourceBucketName, + consumeContext.Message.SourceKeyName, + workingDirectory)); + AddResizeActivity( routingSlipBuilder, "ResizeSmall", consumeContext.Message.SmallPictureKeyName, consumeContext.Message.SmallPictureWidth, consumeContext.Message.SmallPictureHeight, - consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName, + consumeContext.Message.SourceKeyName, + consumeContext.Message.SourceBucketName, workingDirectory); AddResizeActivity( @@ -35,8 +43,8 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.MediumPictureKeyName, consumeContext.Message.MediumPictureWidth, consumeContext.Message.MediumPictureHeight, - consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName, + consumeContext.Message.SourceKeyName, + consumeContext.Message.SourceBucketName, workingDirectory); AddResizeActivity( @@ -45,23 +53,23 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.LargePictureKeyName, consumeContext.Message.LargePictureWidth, consumeContext.Message.LargePictureHeight, - consumeContext.Message.OriginalPictureKeyName, - consumeContext.Message.BucketName, + consumeContext.Message.SourceKeyName, + consumeContext.Message.SourceBucketName, workingDirectory); routingSlipBuilder.AddActivity( "DeleteOriginal", MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( - consumeContext.Message.BucketName, - consumeContext.Message.OriginalPictureKeyName)); + consumeContext.Message.SourceBucketName, + consumeContext.Message.SourceKeyName)); routingSlipBuilder.AddActivity( "UploadFiles", MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( workingDirectory, - consumeContext.Message.BucketName, + consumeContext.Message.SourceBucketName, folderName)); routingSlipBuilder.AddActivity( @@ -92,14 +100,6 @@ namespace Musify.Infrastructure.Messaging.Consumers var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); - routingSlipBuilder.AddActivity( - $"{activityName}_Download", - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), - new DownloadFileFromBucketArguments( - bucketName, - originalPictureKeyName, - workingDirectory)); - routingSlipBuilder.AddActivity( activityName, MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs index 8133f6d..45f5942 100644 --- a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -57,6 +57,13 @@ namespace Musify.Infrastructure.Messaging.Consumers consumeContext.Message.SourceBucketName, workingDirectory); + routingSlipBuilder.AddActivity( + "DeleteOriginal", + MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + consumeContext.Message.SourceBucketName, + consumeContext.Message.SourceKeyName)); + routingSlipBuilder.AddActivity( "UploadFiles", MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), @@ -93,16 +100,6 @@ namespace Musify.Infrastructure.Messaging.Consumers var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); - // First: Download original file - routingSlipBuilder.AddActivity( - $"{activityName}_Download", - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), - new DownloadFileFromBucketArguments( - bucketName, - originalPictureKeyName, - workingDirectory)); - - // Second: Resize picture routingSlipBuilder.AddActivity( activityName, MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), From 773a4e727cee20bbfa20f897eb28a10dc1f8924b Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Mon, 23 Mar 2026 16:13:26 +0100 Subject: [PATCH 070/172] Collection expression --- .../Messaging/Activities/UpdatePlayListPictureActivity.cs | 2 +- .../Messaging/Activities/UpdateTrackPictureActivity.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs index 82ae1c3..d79acf1 100644 --- a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -33,7 +33,7 @@ namespace Musify.Infrastructure.Messaging.Activities try { var playList = await database.PlayLists.FindAsync( - new object[] { executeContext.Arguments.PlayListId }, + [executeContext.Arguments.PlayListId], cancellationToken: executeContext.CancellationToken); if (playList is null) diff --git a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs index 103407e..882ae4d 100644 --- a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs @@ -33,7 +33,7 @@ namespace Musify.Infrastructure.Messaging.Activities try { var track = await database.Tracks.FindAsync( - new object[] { executeContext.Arguments.TrackId }, + [executeContext.Arguments.TrackId], cancellationToken: executeContext.CancellationToken); if (track is null) From 0b471db6aaddad87f76aff07c947208a106eb62c Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Mon, 23 Mar 2026 16:28:02 +0100 Subject: [PATCH 071/172] Use variables --- .../Arguments/TranscodeDashAudioArguments.cs | 2 +- .../TransferFilesToBucketArguments.cs | 1 - .../DownloadFileFromBucketActivity.cs | 16 ++++++++++----- .../Activities/TranscodeDashAudioActivity.cs | 20 +++++++++++-------- .../Activities/TransferFilesToBucket.cs | 12 ++++++++--- .../TranscodeAudioFromTrackConsumer.cs | 10 ++-------- .../UpdatePlayListPictureConsumer.cs | 1 - .../Consumers/UpdateTrackPictureConsumer.cs | 1 - 8 files changed, 35 insertions(+), 28 deletions(-) diff --git a/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs index ddbd8c4..a983378 100644 --- a/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs +++ b/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs @@ -1,4 +1,4 @@ namespace Musify.Infrastructure.Messaging.Activities.Arguments { - public record TranscodeDashAudioArguments(string SourceFilePath, string DestinationFolderName); + public record TranscodeDashAudioArguments(); } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs b/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs index 8f39ecd..b08f97b 100644 --- a/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs +++ b/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs @@ -1,7 +1,6 @@ namespace Musify.Infrastructure.Messaging.Activities.Arguments { public record TransferFilesToBucketArguments( - string FolderPath, string DestinationBucketName, string DestinationKeyName); } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs index b0f33b3..139cd9a 100644 --- a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs @@ -53,17 +53,23 @@ namespace Musify.Infrastructure.Messaging.Activities if (fileStream.CanSeek) fileStream.Position = 0; - var destinationPath = Path.Combine( - audioTranscoderConfiguration.Routes.WorkingDirectory, - Path.GetFileName(executeContext.Arguments.KeyName)); + var fileName = Path.GetFileName(executeContext.Arguments.KeyName); + var folderName = Guid.NewGuid().ToString(); + var workingDirectory = Path.Combine(executeContext.Arguments.DestinationPath, folderName); + var destinationPath = Path.Combine(workingDirectory, fileName); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + Directory.CreateDirectory(workingDirectory); using var destinationStream = File.Create(destinationPath); await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); + return executeContext.CompletedWithVariables(new + { + SourceFilePath = destinationPath, + WorkingDirectory = workingDirectory, + DestinationFolderName = folderName + } ); } catch (Exception exception) { diff --git a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs index ae8ba5e..3cd59b4 100644 --- a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs +++ b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs @@ -32,17 +32,21 @@ namespace Musify.Infrastructure.Messaging.Activities 0, executeContext.CancellationToken); + var sourceFilePath = executeContext.GetVariable("SourceFilePath"); + var destinationFolderName = executeContext.GetVariable("DestinationFolderName"); + + if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(destinationFolderName)) + { + throw new InvalidOperationException("Transcode activity requires SourceFilePath and DestinationFolderName variables."); + } + try { - var workingDirectory = Path.Combine( - audioTranscoderConfiguration.Routes.WorkingDirectory, - executeContext.Arguments.DestinationFolderName); - - await using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); + await using var fileStream = File.OpenRead(sourceFilePath); var result = await audioTranscoder.TranscodeToDashAsync( fileStream, - executeContext.Arguments.DestinationFolderName, + destinationFolderName, audioTranscoderConfiguration.TranscodingTimeout, executeContext.CancellationToken); @@ -50,7 +54,7 @@ namespace Musify.Infrastructure.Messaging.Activities { var errorMessage = string.Join("; ", result.Errors); logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", - executeContext.Arguments.SourceFilePath, errorMessage); + sourceFilePath, errorMessage); throw new Exception(errorMessage); } @@ -60,7 +64,7 @@ namespace Musify.Infrastructure.Messaging.Activities catch (Exception exception) { logger.LogError(exception, "Error during transcoding activity for file {SourceFilePath}", - executeContext.Arguments.SourceFilePath); + sourceFilePath); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs index b3f1ea9..4907734 100644 --- a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs +++ b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs @@ -30,10 +30,16 @@ namespace Musify.Infrastructure.Messaging.Activities 0, executeContext.CancellationToken); + var folderPath = executeContext.GetVariable("WorkingDirectory"); + if (string.IsNullOrWhiteSpace(folderPath)) + { + throw new InvalidOperationException("Transfer activity requires WorkingDirectory variable."); + } + try { var transferFilesResult = await storageHandler.TransferFilesAsync( - executeContext.Arguments.FolderPath, + folderPath, executeContext.Arguments.DestinationBucketName, executeContext.Arguments.DestinationKeyName, executeContext.CancellationToken); @@ -42,7 +48,7 @@ namespace Musify.Infrastructure.Messaging.Activities { var errorMessage = string.Join("; ", transferFilesResult.Errors); logger.LogError("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", - executeContext.Arguments.FolderPath, + folderPath, executeContext.Arguments.DestinationBucketName, executeContext.Arguments.DestinationKeyName, errorMessage); @@ -55,7 +61,7 @@ namespace Musify.Infrastructure.Messaging.Activities catch (Exception exception) { logger.LogError(exception, "Error transferring files from {FolderPath} to bucket {DestinationBucketName}", - executeContext.Arguments.FolderPath, + folderPath, executeContext.Arguments.DestinationBucketName); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; diff --git a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs index 41bfc49..e373565 100644 --- a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs +++ b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -16,9 +16,6 @@ namespace Musify.Infrastructure.Messaging.Consumers public async Task Consume(ConsumeContext consumeContext) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - var fileName = Path.GetFileName(consumeContext.Message.SourceKeyName); - var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); routingSlipBuilder.AddActivity( "DownloadFile", @@ -26,20 +23,17 @@ namespace Musify.Infrastructure.Messaging.Consumers new DownloadFileFromBucketArguments( consumeContext.Message.SourceBucketName, consumeContext.Message.SourceKeyName, - workingDirectory)); + audioTranscoderConfiguration.Routes.WorkingDirectory)); routingSlipBuilder.AddActivity( "TranscodeAudio", MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), - new TranscodeDashAudioArguments( - Path.Combine(workingDirectory, fileName), - folderName)); + new TranscodeDashAudioArguments()); routingSlipBuilder.AddActivity( "UploadFile", MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( - workingDirectory, consumeContext.Message.DestinationBucketName, consumeContext.Message.DestinationKeyName)); diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index fc72103..6af894e 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -68,7 +68,6 @@ namespace Musify.Infrastructure.Messaging.Consumers "UploadFiles", MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( - workingDirectory, consumeContext.Message.SourceBucketName, folderName)); diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs index 45f5942..22d050c 100644 --- a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -68,7 +68,6 @@ namespace Musify.Infrastructure.Messaging.Consumers "UploadFiles", MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( - workingDirectory, consumeContext.Message.SourceBucketName, folderName)); From 0aa628064373b70f766108b4d3ffd3c050147a37 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Mon, 23 Mar 2026 16:53:23 +0100 Subject: [PATCH 072/172] Cambios --- .../Configuration/PlayListConfiguration.cs | 8 ++++---- .../Configuration/TrackConfiguration.cs | 8 ++++---- .../AudioTranscoderConfiguration.cs | 10 ---------- .../ConfigurationDependencyInjection.cs | 3 ++- .../Configuration/DatabaseConfiguration.cs | 3 +-- .../Configuration/KeycloakConfiguration.cs | 9 ++++----- .../Configuration/WorkerConfiguration.cs | 18 ++++++++++++++++++ .../DownloadFileFromBucketActivity.cs | 4 +--- .../TranscodeAudioFromTrackConsumer.cs | 4 ++-- .../Consumers/UpdatePlayListPictureConsumer.cs | 4 ++-- .../Consumers/UpdateTrackPictureConsumer.cs | 4 ++-- 11 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 Infrastructure/Configuration/WorkerConfiguration.cs diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 4effc04..2eccd32 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -49,15 +49,15 @@ namespace Musify.Application.Configuration public int SmallPictureHeight { get; set; } = 128; [Range(1, 1024)] - public int MediumPictureWidth { get; private set; } = 256; + public int MediumPictureWidth { get; set; } = 256; [Range(1, 1024)] - public int MediumPictureHeight { get; private set; } = 256; + public int MediumPictureHeight { get; set; } = 256; [Range(1, 1024)] - public int LargePictureWidth { get; private set; } = 512; + public int LargePictureWidth { get; set; } = 512; [Range(1, 1024)] - public int LargePictureHeight { get; private set; } = 512; + public int LargePictureHeight { get; set; } = 512; } } \ No newline at end of file diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index d352048..dc135f7 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -52,15 +52,15 @@ namespace Musify.Application.Configuration public int SmallPictureHeight { get; set; } = 128; [Range(1, 1024)] - public int MediumPictureWidth { get; private set; } = 256; + public int MediumPictureWidth { get; set; } = 256; [Range(1, 1024)] - public int MediumPictureHeight { get; private set; } = 256; + public int MediumPictureHeight { get; set; } = 256; [Range(1, 1024)] - public int LargePictureWidth { get; private set; } = 512; + public int LargePictureWidth { get; set; } = 512; [Range(1, 1024)] - public int LargePictureHeight { get; private set; } = 512; + public int LargePictureHeight { get; set; } = 512; } } \ No newline at end of file diff --git a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs index c19ecca..affc4a3 100644 --- a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs +++ b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs @@ -2,21 +2,11 @@ namespace Musify.Infrastructure.Configuration { -#pragma warning disable CS8618 public class AudioTranscoderConfiguration { public const string SectionName = "AudioTranscoder"; - [Required] - public AudioTranscoderRoutes Routes { get; set; } = new AudioTranscoderRoutes(); - [Required] public TimeSpan TranscodingTimeout { get; set; } = TimeSpan.FromMinutes(5); } - - public class AudioTranscoderRoutes - { - [Required] - public string WorkingDirectory { get; set; } - } } \ No newline at end of file diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index f275aa4..b89a9eb 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -14,7 +14,8 @@ namespace Musify.Infrastructure.Configuration .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName) .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName) .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName) - .AddValidatedConfiguration(configuration, AudioTranscoderConfiguration.SectionName); + .AddValidatedConfiguration(configuration, AudioTranscoderConfiguration.SectionName) + .AddValidatedConfiguration(configuration, WorkerConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/DatabaseConfiguration.cs b/Infrastructure/Configuration/DatabaseConfiguration.cs index 5996e49..e79f5ed 100644 --- a/Infrastructure/Configuration/DatabaseConfiguration.cs +++ b/Infrastructure/Configuration/DatabaseConfiguration.cs @@ -2,12 +2,11 @@ namespace Musify.Infrastructure.Configuration { -#pragma warning disable CS8618 public class DatabaseConfiguration { public const string SectionName = "Database"; [Required] - public string ConnectionString { get; set; } + public required string ConnectionString { get; set; } } } \ No newline at end of file diff --git a/Infrastructure/Configuration/KeycloakConfiguration.cs b/Infrastructure/Configuration/KeycloakConfiguration.cs index 635dc07..e370097 100644 --- a/Infrastructure/Configuration/KeycloakConfiguration.cs +++ b/Infrastructure/Configuration/KeycloakConfiguration.cs @@ -2,21 +2,20 @@ namespace Musify.Infrastructure.Configuration { -#pragma warning disable CS8618 public class KeycloakConfiguration { public const string SectionName = "Keycloak"; [Required] - public string BaseUrl { get; set; } + public required string BaseUrl { get; set; } [Required] - public string Realm { get; set; } + public required string Realm { get; set; } [Required] - public string Username { get; set; } + public required string Username { get; set; } [Required] - public string Password { get; set; } + public required string Password { get; set; } } } \ No newline at end of file diff --git a/Infrastructure/Configuration/WorkerConfiguration.cs b/Infrastructure/Configuration/WorkerConfiguration.cs new file mode 100644 index 0000000..f399b81 --- /dev/null +++ b/Infrastructure/Configuration/WorkerConfiguration.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class WorkerConfiguration + { + public const string SectionName = "Workers"; + + [Required] + public WorkerRoutes Routes { get; set; } = new WorkerRoutes(); + } + + public class WorkerRoutes + { + [Required] + public string TemporaryFilesDirectory { get; set; } = string.Empty; + } +} diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs index 139cd9a..86e9b19 100644 --- a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities @@ -10,8 +9,7 @@ namespace Musify.Infrastructure.Messaging.Activities public class DownloadFileFromBucketActivity( IStorageHandler storageHandler, ILogger logger, - IProcessTrackingStore processTrackingStore, - AudioTranscoderConfiguration audioTranscoderConfiguration) + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Download-File-From-Bucket"; diff --git a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs index e373565..faf5853 100644 --- a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs +++ b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -8,7 +8,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { public class TranscodeAudioFromTrackConsumer( IBus bus, - AudioTranscoderConfiguration audioTranscoderConfiguration) + WorkerConfiguration workerConfiguration) : IConsumer { public const string QueueName = "Transcode-Audio-From-Track"; @@ -23,7 +23,7 @@ namespace Musify.Infrastructure.Messaging.Consumers new DownloadFileFromBucketArguments( consumeContext.Message.SourceBucketName, consumeContext.Message.SourceKeyName, - audioTranscoderConfiguration.Routes.WorkingDirectory)); + workerConfiguration.Routes.TemporaryFilesDirectory)); routingSlipBuilder.AddActivity( "TranscodeAudio", diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index 6af894e..0ba7a98 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -8,7 +8,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { public class UpdatePlayListPictureConsumer( IBus bus, - AudioTranscoderConfiguration audioTranscoderConfiguration) + WorkerConfiguration workerConfiguration) : IConsumer { public const string QueueName = "Update-PlayList-Picture"; @@ -17,7 +17,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); + var workingDirectory = Path.Combine(workerConfiguration.Routes.TemporaryFilesDirectory, folderName); routingSlipBuilder.AddActivity( "DownloadFile", diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs index 22d050c..f019285 100644 --- a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -8,7 +8,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { public class UpdateTrackPictureConsumer( IBus bus, - AudioTranscoderConfiguration audioTranscoderConfiguration) + WorkerConfiguration workerConfiguration) : IConsumer { public const string QueueName = "Update-Track-Picture"; @@ -17,7 +17,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(audioTranscoderConfiguration.Routes.WorkingDirectory, folderName); + var workingDirectory = Path.Combine(workerConfiguration.Routes.TemporaryFilesDirectory, folderName); routingSlipBuilder.AddActivity( "DownloadFile", From db1ae0055729febb8f0e04d8fcc8b39066d16a6c Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Mon, 23 Mar 2026 17:14:49 +0100 Subject: [PATCH 073/172] Refactor audioTranscoder --- .../Infrastructure/IAudioTranscoder.cs | 4 +- Infrastructure/Audio/AudioTranscoder.cs | 253 ++++++++++++++---- .../AudioTranscoderConfiguration.cs | 38 +++ .../Activities/TranscodeDashAudioActivity.cs | 13 +- 4 files changed, 248 insertions(+), 60 deletions(-) diff --git a/Application/Contracts/Infrastructure/IAudioTranscoder.cs b/Application/Contracts/Infrastructure/IAudioTranscoder.cs index 81f8941..f302d1b 100644 --- a/Application/Contracts/Infrastructure/IAudioTranscoder.cs +++ b/Application/Contracts/Infrastructure/IAudioTranscoder.cs @@ -4,6 +4,8 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IAudioTranscoder { - Task TranscodeToDashAsync(Stream audioStream, string destinationPath, TimeSpan timeOut, CancellationToken cancellationToken); + Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); + + Task> IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Audio/AudioTranscoder.cs index 9ae77ac..140c88b 100644 --- a/Infrastructure/Audio/AudioTranscoder.cs +++ b/Infrastructure/Audio/AudioTranscoder.cs @@ -1,89 +1,240 @@ using Ardalis.Result; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Configuration; using System.Diagnostics; namespace Musify.Infrastructure.Audio { - public class AudioTranscoder(ILogger logger) + public class AudioTranscoder( + ILogger logger, + AudioTranscoderConfiguration audioTranscoderConfiguration) : IAudioTranscoder { - const string FfmpegProcessName = "ffmpeg"; - - public async Task TranscodeToDashAsync(Stream audioStream, string destinationPath, TimeSpan timeOut, CancellationToken cancellationToken) + public async Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken) { try { Directory.CreateDirectory(destinationPath); - using var process = new Process + logger.LogInformation( + "Starting audio transcoding to DASH. DestinationPath: {DestinationPath}, Timeout: {Timeout}, Executable: {Executable}", + destinationPath, + audioTranscoderConfiguration.TranscodingTimeout, + audioTranscoderConfiguration.Ffmpeg.ExecutableName); + + var executionResult = await ExecuteFfmpegWithInputAsync( + BuildDashArguments(destinationPath, audioTranscoderConfiguration.Ffmpeg), + destinationPath, + audioStream, + "TranscodeToDash", + cancellationToken); + + if (executionResult.ExitCode != 0) { - StartInfo = new ProcessStartInfo - { - FileName = FfmpegProcessName, - WorkingDirectory = destinationPath, - Arguments = BuildDashArguments(destinationPath), - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - } - }; - process.Start(); - - if (audioStream.CanSeek) - audioStream.Position = 0; - await audioStream.CopyToAsync(process.StandardInput.BaseStream, cancellationToken); - process.StandardInput.Close(); - - var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); - var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); - - using var linkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - linkedCancellationTokens.CancelAfter(timeOut); - await process.WaitForExitAsync(linkedCancellationTokens.Token); - - var standardError = await errorTask; - var standardOutput = await outputTask; - - if (process.ExitCode != 0) - { - logger.LogError("Audio transcoding to DASH format failed with exit code {ExitCode}. Standard Output: {StandardOutput}, Standard Error: {StandardError}", process.ExitCode, standardOutput, standardError); - return Result.Error($"Audio transcoding failed with exit code {process.ExitCode}. See logs for details."); + logger.LogError( + "Audio transcoding to DASH format failed with exit code {ExitCode}. Standard Output: {StandardOutput}, Standard Error: {StandardError}", + executionResult.ExitCode, + executionResult.StandardOutput, + executionResult.StandardError); + return Result.Error($"Audio transcoding failed with exit code {executionResult.ExitCode}. See logs for details."); } - logger.LogInformation("Audio transcoding to DASH format completed successfully. Uploading to storage..."); + logger.LogInformation("Audio transcoding to DASH format completed successfully. DestinationPath: {DestinationPath}", destinationPath); return Result.Success(); } + catch (TimeoutException timeoutException) + { + logger.LogError(timeoutException, "Audio transcoding timed out after {Timeout}. DestinationPath: {DestinationPath}", audioTranscoderConfiguration.TranscodingTimeout, destinationPath); + return Result.Error("Audio transcoding timed out."); + } catch (Exception exception) { - logger.LogError(exception, "An error occurred while transcoding audio to DASH format."); + logger.LogError(exception, "An error occurred while transcoding audio to DASH format. DestinationPath: {DestinationPath}", destinationPath); return Result.Error(exception.Message); } } - static string BuildDashArguments(string outputDirectory) + public async Task> IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken) { - var manifestPath = Path.Combine(outputDirectory, "manifest.mpd"); + if (!File.Exists(filePath)) + { + logger.LogWarning("Audio validation requested for a non-existing file. FilePath: {FilePath}", filePath); + return Result.Error($"Audio file not found: {filePath}"); + } + + try + { + logger.LogInformation("Starting audio file validation. FilePath: {FilePath}, Timeout: {Timeout}", filePath, audioTranscoderConfiguration.TranscodingTimeout); + + var executionResult = await ExecuteFfmpegAsync( + BuildValidateAudioArguments(filePath), + Path.GetDirectoryName(filePath) ?? Environment.CurrentDirectory, + "ValidateAudioFile", + cancellationToken); + + var isValid = executionResult.ExitCode == 0; + if (!isValid) + { + logger.LogWarning( + "Audio validation failed for file {FilePath}. Standard Error: {StandardError}", + filePath, + executionResult.StandardError); + } + else + { + logger.LogInformation("Audio validation completed successfully. FilePath: {FilePath}", filePath); + } + + return Result.Success(isValid); + } + catch (TimeoutException timeoutException) + { + logger.LogError(timeoutException, "Audio validation timed out after {Timeout} for file {FilePath}.", audioTranscoderConfiguration.TranscodingTimeout, filePath); + return Result.Error("Audio validation timed out."); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while validating audio file {FilePath}.", filePath); + return Result.Error(exception.Message); + } + } + + async Task ExecuteFfmpegWithInputAsync( + string arguments, + string workingDirectory, + Stream inputStream, + string operationName, + CancellationToken cancellationToken) + { + using var process = BuildProcess(arguments, workingDirectory, redirectStandardInput: true); + process.Start(); + + logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId: {ProcessId}, WorkingDirectory: {WorkingDirectory}", operationName, process.Id, workingDirectory); + + if (inputStream.CanSeek) + inputStream.Position = 0; + + await inputStream.CopyToAsync(process.StandardInput.BaseStream, cancellationToken); + process.StandardInput.Close(); + + return await WaitForProcessResultAsync(process, operationName, cancellationToken); + } + + async Task ExecuteFfmpegAsync( + string arguments, + string workingDirectory, + string operationName, + CancellationToken cancellationToken) + { + using var process = BuildProcess(arguments, workingDirectory, redirectStandardInput: false); + process.Start(); + + logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId: {ProcessId}, WorkingDirectory: {WorkingDirectory}", operationName, process.Id, workingDirectory); + + return await WaitForProcessResultAsync(process, operationName, cancellationToken); + } + + Process BuildProcess(string arguments, string workingDirectory, bool redirectStandardInput) + => new() + { + StartInfo = new ProcessStartInfo + { + FileName = audioTranscoderConfiguration.Ffmpeg.ExecutableName, + WorkingDirectory = workingDirectory, + Arguments = arguments, + RedirectStandardInput = redirectStandardInput, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + + async Task WaitForProcessResultAsync( + Process process, + string operationName, + CancellationToken cancellationToken) + { + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + + using var linkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkedCancellationTokens.CancelAfter(audioTranscoderConfiguration.TranscodingTimeout); + + try + { + await process.WaitForExitAsync(linkedCancellationTokens.Token); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + logger.LogError( + "[{Operation}] ffmpeg timed out after {Timeout}. ProcessId: {ProcessId}", + operationName, + audioTranscoderConfiguration.TranscodingTimeout, + process.Id); + + TryKillProcess(process); + throw new TimeoutException($"ffmpeg execution exceeded timeout {audioTranscoderConfiguration.TranscodingTimeout}."); + } + + var standardError = await errorTask; + var standardOutput = await outputTask; + + logger.LogDebug( + "[{Operation}] ffmpeg finished. ProcessId: {ProcessId}, ExitCode: {ExitCode}, StdOutLength: {StdOutLength}, StdErrLength: {StdErrLength}", + operationName, + process.Id, + process.ExitCode, + standardOutput.Length, + standardError.Length); + + return new FfmpegExecutionResult(process.ExitCode, standardOutput, standardError); + } + + void TryKillProcess(Process process) + { + try + { + if (!process.HasExited) + process.Kill(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to kill ffmpeg process. ProcessId: {ProcessId}", process.Id); + } + } + + static string BuildDashArguments(string outputDirectory, FfmpegConfiguration ffmpegConfiguration) + { + var manifestPath = Path.Combine(outputDirectory, ffmpegConfiguration.ManifestFileName); + var additionalArguments = string.IsNullOrWhiteSpace(ffmpegConfiguration.AdditionalArguments) + ? string.Empty + : $" {ffmpegConfiguration.AdditionalArguments.Trim()}"; return "-y -hide_banner -loglevel error " + "-i pipe:0 " + "-vn " + "-map 0:a " + - "-c:a aac " + - "-b:a 128k " + - "-ac 2 " + - "-ar 48000 " + - "-profile:a aac_low " + + $"-c:a {ffmpegConfiguration.AudioCodec} " + + $"-b:a {ffmpegConfiguration.AudioBitrate} " + + $"-ac {ffmpegConfiguration.AudioChannels} " + + $"-ar {ffmpegConfiguration.AudioSampleRate} " + + $"-profile:a {ffmpegConfiguration.AudioProfile} " + "-f dash " + - "-seg_duration 4 " + + $"-seg_duration {ffmpegConfiguration.SegmentDurationSeconds} " + "-streaming 1 " + "-use_template 1 " + "-use_timeline 0 " + - "-init_seg_name \"init-stream$RepresentationID$.m4s\" " + - "-media_seg_name \"chunk-stream$RepresentationID$-$Number$.m4s\" " + - $"\"{manifestPath}\""; + $"-init_seg_name \"{ffmpegConfiguration.InitSegmentName}\" " + + $"-media_seg_name \"{ffmpegConfiguration.MediaSegmentName}\"" + + additionalArguments + + $" \"{manifestPath}\""; } + + static string BuildValidateAudioArguments(string filePath) + => $"-v error -i \"{filePath}\" -map 0:a:0 -f null -"; + + sealed record FfmpegExecutionResult(int ExitCode, string StandardOutput, string StandardError); } } \ No newline at end of file diff --git a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs index affc4a3..e4ef863 100644 --- a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs +++ b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs @@ -8,5 +8,43 @@ namespace Musify.Infrastructure.Configuration [Required] public TimeSpan TranscodingTimeout { get; set; } = TimeSpan.FromMinutes(5); + + [Required] + public FfmpegConfiguration Ffmpeg { get; set; } = new FfmpegConfiguration(); + } + + public class FfmpegConfiguration + { + [Required] + public string ExecutableName { get; set; } = "ffmpeg"; + + [Required] + public string ManifestFileName { get; set; } = "manifest.mpd"; + + [Required] + public string AudioCodec { get; set; } = "aac"; + + [Required] + public string AudioBitrate { get; set; } = "128k"; + + [Range(1, 8)] + public int AudioChannels { get; set; } = 2; + + [Range(8000, 192000)] + public int AudioSampleRate { get; set; } = 48000; + + [Required] + public string AudioProfile { get; set; } = "aac_low"; + + [Range(1, 60)] + public int SegmentDurationSeconds { get; set; } = 4; + + [Required] + public string InitSegmentName { get; set; } = "init-stream$RepresentationID$.m4s"; + + [Required] + public string MediaSegmentName { get; set; } = "chunk-stream$RepresentationID$-$Number$.m4s"; + + public string AdditionalArguments { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs index 3cd59b4..8094999 100644 --- a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs +++ b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities.Arguments; namespace Musify.Infrastructure.Messaging.Activities @@ -10,8 +9,7 @@ namespace Musify.Infrastructure.Messaging.Activities public class TranscodeDashAudioActivity( IAudioTranscoder audioTranscoder, ILogger logger, - IProcessTrackingStore processTrackingStore, - AudioTranscoderConfiguration audioTranscoderConfiguration) + IProcessTrackingStore processTrackingStore) : IExecuteActivity { public const string ExecuteEndpointName = "Transcode-Dash-Audio"; @@ -33,11 +31,11 @@ namespace Musify.Infrastructure.Messaging.Activities executeContext.CancellationToken); var sourceFilePath = executeContext.GetVariable("SourceFilePath"); - var destinationFolderName = executeContext.GetVariable("DestinationFolderName"); + var workingDirectory = executeContext.GetVariable("WorkingDirectory"); - if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(destinationFolderName)) + if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(workingDirectory)) { - throw new InvalidOperationException("Transcode activity requires SourceFilePath and DestinationFolderName variables."); + throw new InvalidOperationException("Transcode activity requires SourceFilePath and WorkingDirectory variables."); } try @@ -46,8 +44,7 @@ namespace Musify.Infrastructure.Messaging.Activities var result = await audioTranscoder.TranscodeToDashAsync( fileStream, - destinationFolderName, - audioTranscoderConfiguration.TranscodingTimeout, + workingDirectory, executeContext.CancellationToken); if (!result.IsSuccess) From 7ffeeb464aa52f769823ca2d8145798159adeb38 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Mon, 23 Mar 2026 17:16:01 +0100 Subject: [PATCH 074/172] Changes --- .../Messaging/MessagingDependencyInjection.cs | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/Infrastructure/Messaging/MessagingDependencyInjection.cs b/Infrastructure/Messaging/MessagingDependencyInjection.cs index 4c2324f..59e9c11 100644 --- a/Infrastructure/Messaging/MessagingDependencyInjection.cs +++ b/Infrastructure/Messaging/MessagingDependencyInjection.cs @@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Messaging ConfigureRabbitMqHost( busFactoryConfigurator, busRegistrationContext.GetRequiredService())); - }); + } ); return serviceDescriptors; } @@ -54,22 +54,22 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - }); + } ); busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - }); + } ); busFactoryConfigurator.ReceiveEndpoint(TranscodeAudioFromTrackConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - }); + } ); busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => { endpointConfigurator.ConfigureConsumer(busRegistrationContext); - }); + } ); // Activity endpoints ConfigureExecuteActivityEndpoint( @@ -106,8 +106,8 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator, busRegistrationContext, TransferFilesToBucket.ExecuteEndpointName); - }); - }); + } ); + } ); return serviceDescriptors; } @@ -120,7 +120,7 @@ namespace Musify.Infrastructure.Messaging { options.Username(massTransitConfiguration.Username); options.Password(massTransitConfiguration.Password); - }); + } ); } static void ConfigureExecuteActivityEndpoint( @@ -133,26 +133,7 @@ namespace Musify.Infrastructure.Messaging busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => { endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - }); - } - - static void ConfigureActivityEndpoints( - IRabbitMqBusFactoryConfigurator busFactoryConfigurator, - IBusRegistrationContext busRegistrationContext, - string endpointName) - where TActivity : class, IActivity - where TArguments : class - where TLog : class - { - busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => - { - endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - }); - - busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_compensate", endpointConfigurator => - { - endpointConfigurator.CompensateActivityHost(busRegistrationContext); - }); + } ); } } } \ No newline at end of file From c6c5515ebd99623c6e56731be8afd51762cf29e8 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Mon, 23 Mar 2026 17:22:47 +0100 Subject: [PATCH 075/172] Logger --- .../DownloadFileFromBucketActivity.cs | 2 +- .../Activities/ResizePictureActivity.cs | 5 ++-- .../Activities/TransferFilesToBucket.cs | 2 +- .../UpdatePlayListPictureActivity.cs | 2 +- .../Activities/UpdateTrackPictureActivity.cs | 2 +- Infrastructure/Pictures/PictureHandler.cs | 4 +++- Infrastructure/Storage/StorageHandler.cs | 23 +++++++++++-------- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs index 86e9b19..5cf5e45 100644 --- a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs @@ -40,7 +40,7 @@ namespace Musify.Infrastructure.Messaging.Activities if (!getFile.IsSuccess) { var errorMessage = string.Join("; ", getFile.Errors); - logger.LogError("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", + logger.LogWarning("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", executeContext.Arguments.BucketName, executeContext.Arguments.KeyName, errorMessage); diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs index 8e0d0e6..6ea330f 100644 --- a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -34,8 +34,7 @@ namespace Musify.Infrastructure.Messaging.Activities { if (!File.Exists(executeContext.Arguments.SourceFilePath)) { - logger.LogError("Source file not found at {SourceFilePath}", - executeContext.Arguments.SourceFilePath); + logger.LogWarning("Source file not found at {SourceFilePath}", executeContext.Arguments.SourceFilePath); throw new FileNotFoundException($"Source file not found: {executeContext.Arguments.SourceFilePath}"); } @@ -67,7 +66,7 @@ namespace Musify.Infrastructure.Messaging.Activities await using var destinationStream = File.Create(executeContext.Arguments.DestinationFilePath); await resizedPicture.Value.CopyToAsync(destinationStream, executeContext.CancellationToken); - logger.LogInformation("Picture resized successfully from {SourceFilePath} to {DestinationFilePath} ( {Width}x{Height} )", + logger.LogDebug("Picture resized from {SourceFilePath} to {DestinationFilePath} ({Width}x{Height})", executeContext.Arguments.SourceFilePath, executeContext.Arguments.DestinationFilePath, executeContext.Arguments.Width, diff --git a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs index 4907734..13ca1e4 100644 --- a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs +++ b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs @@ -47,7 +47,7 @@ namespace Musify.Infrastructure.Messaging.Activities if (!transferFilesResult.IsSuccess) { var errorMessage = string.Join("; ", transferFilesResult.Errors); - logger.LogError("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", + logger.LogWarning("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", folderPath, executeContext.Arguments.DestinationBucketName, executeContext.Arguments.DestinationKeyName, diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs index d79acf1..b369773 100644 --- a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -38,7 +38,7 @@ namespace Musify.Infrastructure.Messaging.Activities if (playList is null) { - logger.LogError("PlayList with id {PlayListId} not found", + logger.LogWarning("PlayList with id {PlayListId} not found", executeContext.Arguments.PlayListId); throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); } diff --git a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs index 882ae4d..9b7291d 100644 --- a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs @@ -38,7 +38,7 @@ namespace Musify.Infrastructure.Messaging.Activities if (track is null) { - logger.LogError("Track with id {TrackId} not found", + logger.LogWarning("Track with id {TrackId} not found", executeContext.Arguments.TrackId); throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); } diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index 4185761..1181fac 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -17,6 +17,7 @@ namespace Musify.Infrastructure.Pictures { try { + logger.LogDebug("Resizing picture to {Width}x{Height}", width, height); if (pictureStream.CanSeek && pictureStream.Position > 0) pictureStream.Position = 0; @@ -34,11 +35,12 @@ namespace Musify.Infrastructure.Pictures memoryStream.Position = 0; + logger.LogDebug("Picture resized successfully. OutputLength: {Length}", memoryStream.Length); return Result.Success(memoryStream); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while resizing the picture."); + logger.LogError(exception, "An error occurred while resizing the picture to {Width}x{Height}.", width, height); return Result.Error("An error occurred while resizing the picture."); } } diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index edec90c..cfc0d53 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -20,7 +20,7 @@ namespace Musify.Infrastructure.Storage Key = keyName, }; - logger.LogDebug("Getting file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogDebug("Getting file from S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); try { @@ -31,7 +31,7 @@ namespace Musify.Infrastructure.Storage } catch (Exception exception) { - logger.LogDebug(exception, "Failed to get file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogWarning(exception, "Failed to get file from S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); return Result.NotFound($"File not found in S3 with bucket name {bucketName} and key name {keyName}"); } } @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.Storage Protocol = storageClientConfiguration.UseHttp ? Protocol.HTTP : Protocol.HTTPS }; - logger.LogDebug("Getting pre-signed URL from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogDebug("Getting pre-signed URL from S3. Bucket: {BucketName}, Key: {KeyName}, Expires: {Expires}", bucketName, keyName, request.Expires); var url = await amazonS3.GetPreSignedURLAsync(request); return url; @@ -60,7 +60,7 @@ namespace Musify.Infrastructure.Storage Key = keyName, }; - logger.LogDebug("Removing file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogDebug("Removing file from S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); var response = await amazonS3.DeleteObjectAsync(request, cancellationToken); return Result.NoContent(); @@ -72,6 +72,9 @@ namespace Musify.Infrastructure.Storage var files = Directory.GetFiles(sourceDirectory, "*"); + var successCount = 0; + var failedCount = 0; + foreach (var file in files) { string fileName = Path.GetFileName(file); @@ -90,17 +93,19 @@ namespace Musify.Infrastructure.Storage { 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); + successCount++; + logger.LogDebug("Transferred file to S3. Bucket: {BucketName}, Route: {Route}, File: {FileName}", bucketName, route, fileName); } catch (Exception exception) { - logger.LogError(exception, "Failed to transfer file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); + failedCount++; upload.State = UploadState.Failed; + logger.LogError(exception, "Failed to transfer file to S3. Bucket: {BucketName}, Route: {Route}, File: {FileName}", bucketName, route, fileName); } } 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); + logger.LogInformation("TransferFiles completed. Bucket: {BucketName}, Route: {Route}, Success: {SuccessCount}, Failed: {FailedCount}", bucketName, route, successCount, failedCount); return Result.Success(); } @@ -127,7 +132,7 @@ namespace Musify.Infrastructure.Storage try { var response = await amazonS3.PutObjectAsync(request, cancellationToken); - logger.LogInformation("Successfully uploaded file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogDebug("Uploaded file to S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); } catch (Exception exception) { @@ -138,7 +143,7 @@ namespace Musify.Infrastructure.Storage 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); + logger.LogDebug("Saved upload record to database. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); return Result.Success(keyName); } From 537296ec4c4962c8098c4d3e804cd83b2093b739 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 23 Mar 2026 19:17:15 +0100 Subject: [PATCH 076/172] Change on routes --- .../Handlers/CreatePlayListCommandHandler.cs | 12 ++++++------ .../Handlers/UpdatePlayListCommandHandler.cs | 2 +- .../Tracks/Handler/CreateTrackCommandHandler.cs | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index f4f8ff4..7554c14 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -39,7 +39,7 @@ namespace Musify.Application.PlayLists.Handlers var setPictureResult = request.Picture is null ? Result.Success() - : await SetPicture(playList, request.Picture, cancellationToken); + : await UploadPicture(playList, request.Picture, cancellationToken); if (!setPictureResult.IsSuccess) return setPictureResult; @@ -51,13 +51,13 @@ namespace Musify.Application.PlayLists.Handlers return Result.Created(PlayListResponse.FromEntity(playList)); } - async Task SetPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) + async Task UploadPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) { var imageId = Guid.NewGuid(); - var pictureName = $"{imageId}{picture.FileType}"; - var originalImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); + var pictureName = imageId + picture.FileType; + var pictureStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); - var uploadResult = await UploadFile(picture, originalImageStorageKey, cancellationToken); + var uploadResult = await UploadFile(picture, pictureStorageKey, cancellationToken); if (!uploadResult.IsSuccess) { @@ -69,7 +69,7 @@ namespace Musify.Application.PlayLists.Handlers try { - await PublishUpdateEvent(originalImageStorageKey, playList, cancellationToken); + await PublishUpdateEvent(pictureStorageKey, playList, cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index b85f857..b682bbe 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -65,7 +65,7 @@ namespace Musify.Application.PlayLists.Handlers } var newImageId = Guid.NewGuid(); - var pictureName = $"{newImageId}{request.NewPicture.FileType}"; + var pictureName = newImageId + request.NewPicture.FileType; var newImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); var uploadResult = await UploadFile(request.NewPicture, newImageStorageKey, cancellationToken); diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 6b0b7b1..2270ddc 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -25,7 +25,7 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User with id {request.UserId} not found"); } - var originalPictureKeyName = $"{Guid.NewGuid() }.{request.Picture.FileType}"; + var originalPictureKeyName = Path.Combine(trackConfiguration.Routes.OriginalPictures, Guid.NewGuid() + request.Picture.FileType); var track = new Track { Title = request.Title, @@ -42,7 +42,8 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to upload picture for track {request.Title}"); } - var audioKeyName = Path.Combine(trackConfiguration.Routes.Audios, $"{Guid.NewGuid() }.{request.Audio.FileType}"); + + var audioKeyName = Path.Combine(trackConfiguration.Routes.Audios, Guid.NewGuid() + request.Audio.FileType); var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); if (!audioUploadResult.IsSuccess) { From 44b088380b00e9b9fd6b67a7f1c8fba4c0d09687 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 23 Mar 2026 19:30:20 +0100 Subject: [PATCH 077/172] TempPath --- Infrastructure/Configuration/WorkerConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/Configuration/WorkerConfiguration.cs b/Infrastructure/Configuration/WorkerConfiguration.cs index f399b81..fa33cae 100644 --- a/Infrastructure/Configuration/WorkerConfiguration.cs +++ b/Infrastructure/Configuration/WorkerConfiguration.cs @@ -13,6 +13,6 @@ namespace Musify.Infrastructure.Configuration public class WorkerRoutes { [Required] - public string TemporaryFilesDirectory { get; set; } = string.Empty; + public string TemporaryFilesDirectory { get; set; } = Path.GetTempPath(); } } From da4b76f499458800972377a6763fb4ff0e2164c0 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 24 Mar 2026 14:45:04 +0100 Subject: [PATCH 078/172] Changes --- .../MassTransit/Logs/DownloadFileFromBucketLog.cs | 7 +++++++ .../MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs | 4 ++++ .../MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs | 4 ++++ Infrastructure/MassTransit/Logs/ResizePictureLog.cs | 4 ++++ Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs | 4 ++++ .../MassTransit/Logs/TransferFilesToBucketLog.cs | 4 ++++ .../MassTransit/Logs/UpdatePlayListPictureLog.cs | 8 ++++++++ Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs | 8 ++++++++ 8 files changed, 43 insertions(+) create mode 100644 Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs create mode 100644 Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs create mode 100644 Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs create mode 100644 Infrastructure/MassTransit/Logs/ResizePictureLog.cs create mode 100644 Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs create mode 100644 Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs create mode 100644 Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs create mode 100644 Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs diff --git a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs new file mode 100644 index 0000000..8b383f6 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs @@ -0,0 +1,7 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record DownloadFileFromBucketLog( + string BucketName, + string KeyName, + string DestinationFilePath); +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs b/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs new file mode 100644 index 0000000..c012bd9 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record GenerateAudioWorkflowPathsLog(string WorkingDirectory); +} diff --git a/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs b/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs new file mode 100644 index 0000000..0bd6f43 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record GeneratePictureWorkflowPathsLog(string WorkingDirectory); +} diff --git a/Infrastructure/MassTransit/Logs/ResizePictureLog.cs b/Infrastructure/MassTransit/Logs/ResizePictureLog.cs new file mode 100644 index 0000000..053a4dd --- /dev/null +++ b/Infrastructure/MassTransit/Logs/ResizePictureLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record ResizePictureLog(string DestinationFilePath); +} diff --git a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs new file mode 100644 index 0000000..2345392 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record TranscodeDashAudioLog(string WorkingDirectory); +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs new file mode 100644 index 0000000..c4f59af --- /dev/null +++ b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record TransferFilesToBucketLog(string DestinationBucketName, string[] UploadedKeys); +} diff --git a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs new file mode 100644 index 0000000..b36e487 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record UpdatePlayListPictureLog( + Guid PlayListId, + string? PreviousSmallPictureKeyName, + string? PreviousMediumPictureKeyName, + string? PreviousLargePictureKeyName); +} diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs new file mode 100644 index 0000000..0a9da77 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Logs +{ + public record UpdateTrackPictureLog( + Guid TrackId, + string? PreviousSmallPictureKeyName, + string? PreviousMediumPictureKeyName, + string? PreviousLargePictureKeyName); +} From 27d68986c5310ae5ee3c758bbafac138df644628 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 24 Mar 2026 14:45:20 +0100 Subject: [PATCH 079/172] Changes --- .../Configuration/PlayListConfiguration.cs | 3 + .../Configuration/TrackConfiguration.cs | 3 + .../Handlers/CreatePlayListCommandHandler.cs | 3 + .../Handler/CreateTrackCommandHandler.cs | 1 + Domain/Entities/PlayList.cs | 4 + Domain/Entities/Track.cs | 4 + Infrastructure/Infrastructure.csproj | 21 + .../GenerateAudioWorkflowPathsActivity.cs | 63 +++ .../Audio/TranscodeDashAudioActivity.cs | 89 ++++ .../Files/DownloadFileFromBucketActivity.cs | 100 +++++ .../Files}/RemoveFileFromBucketActivity.cs | 0 .../Activities/Files/TransferFilesToBucket.cs | 109 +++++ .../GeneratePictureWorkflowPathsActivity.cs | 76 ++++ .../Pictures/ResizePictureActivity.cs | 113 +++++ .../Pictures/UpdatePlayListPictureActivity.cs | 107 +++++ .../Pictures/UpdateTrackPictureActivity.cs | 107 +++++ .../DownloadFileFromBucketArguments.cs | 2 +- .../GenerateAudioWorkflowPathsArguments.cs | 6 + .../GeneratePictureWorkflowPathsArguments.cs | 9 + .../RemoveFileFromBucketArguments.cs | 0 .../Arguments/ResizePictureLocalArguments.cs | 4 +- .../Arguments/TranscodeDashAudioArguments.cs | 6 + .../TransferFilesToBucketArguments.cs | 4 +- .../UpdatePlayListPictureArguments.cs | 1 + .../Arguments/UpdateTrackPictureArguments.cs | 1 + .../Consumers/MessagingHelper.cs | 2 + .../Consumers/RemoveFileConsumer.cs | 23 + .../TranscodeAudioFromTrackConsumer.cs | 23 + .../UpdatePlayListPictureConsumer.cs | 23 + .../Consumers/UpdateTrackPictureConsumer.cs | 23 + .../MessagingDependencyInjection.cs | 173 ++++++++ .../EventBus}/MassTransitEventBus.cs | 0 .../Filters/ProcessTrackingConsumeFilter.cs | 0 .../Logs/UpdatePlayListPictureLog.cs | 7 +- .../MassTransit/Logs/UpdateTrackPictureLog.cs | 7 +- .../MassTransit/RoutingSlip/ActivityNames.cs | 18 + .../AudioWorkflowRoutingSlipBuilder.cs | 50 +++ .../PictureWorkflowRoutingSlipBuilder.cs | 159 +++++++ .../Builders/RemoveFileRoutingSlipBuilder.cs | 25 ++ .../Variables/RoutingSlipVariableNames.cs | 27 ++ .../Activities/Arguments/CopyFileArguments.cs | 8 - .../Arguments/ResizePictureArgument.cs | 10 - .../Arguments/TranscodeDashAudioArguments.cs | 4 - .../DownloadFileFromBucketActivity.cs | 80 ---- .../GenerateAudioWorkflowPathsActivity.cs | 0 .../GeneratePictureWorkflowPathsActivity.cs | 0 .../Logs/DownloadFileFromBucketLog.cs | 7 - .../Activities/Logs/TranscodeDashAudioLog.cs | 4 - .../Activities/ResizePictureActivity.cs | 87 ---- .../Activities/TranscodeDashAudioActivity.cs | 70 ---- .../Activities/TransferFilesToBucket.cs | 71 ---- .../UpdatePlayListPictureActivity.cs | 68 --- .../Activities/UpdateTrackPictureActivity.cs | 68 --- .../Messaging/Consumers/RemoveFileConsumer.cs | 28 -- .../TranscodeAudioFromTrackConsumer.cs | 44 -- .../UpdatePlayListPictureConsumer.cs | 112 ----- .../Consumers/UpdateTrackPictureConsumer.cs | 112 ----- .../Messaging/MessagingDependencyInjection.cs | 139 ------- .../Messaging/RoutingSlip/ActivityNames.cs | 0 .../AudioWorkflowRoutingSlipBuilder.cs | 0 .../PictureWorkflowRoutingSlipBuilder.cs | 0 .../Builders/RemoveFileRoutingSlipBuilder.cs | 0 .../Messaging/RoutingSlipVariableNames.cs | 0 ...ctureKeyNameToTrackAndPlayList.Designer.cs | 392 ++++++++++++++++++ ...riginalPictureKeyNameToTrackAndPlayList.cs | 42 ++ .../Migrations/DatabaseModelSnapshot.cs | 10 + 66 files changed, 1830 insertions(+), 922 deletions(-) create mode 100644 Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs rename Infrastructure/{Messaging/Activities => MassTransit/Activities/Files}/RemoveFileFromBucketActivity.cs (100%) create mode 100644 Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs create mode 100644 Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs rename Infrastructure/{Messaging/Activities => MassTransit}/Arguments/DownloadFileFromBucketArguments.cs (77%) create mode 100644 Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs create mode 100644 Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs rename Infrastructure/{Messaging/Activities => MassTransit}/Arguments/RemoveFileFromBucketArguments.cs (100%) rename Infrastructure/{Messaging/Activities => MassTransit}/Arguments/ResizePictureLocalArguments.cs (62%) create mode 100644 Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs rename Infrastructure/{Messaging/Activities => MassTransit}/Arguments/TransferFilesToBucketArguments.cs (52%) rename Infrastructure/{Messaging/Activities => MassTransit}/Arguments/UpdatePlayListPictureArguments.cs (86%) rename Infrastructure/{Messaging/Activities => MassTransit}/Arguments/UpdateTrackPictureArguments.cs (86%) rename Infrastructure/{Messaging => MassTransit}/Consumers/MessagingHelper.cs (64%) create mode 100644 Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs create mode 100644 Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs create mode 100644 Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs create mode 100644 Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs create mode 100644 Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs rename Infrastructure/{Messaging => MassTransit/EventBus}/MassTransitEventBus.cs (100%) rename Infrastructure/{Messaging => MassTransit}/Filters/ProcessTrackingConsumeFilter.cs (100%) create mode 100644 Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs delete mode 100644 Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs delete mode 100644 Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs delete mode 100644 Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs create mode 100644 Infrastructure/Messaging/Activities/GenerateAudioWorkflowPathsActivity.cs create mode 100644 Infrastructure/Messaging/Activities/GeneratePictureWorkflowPathsActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs create mode 100644 Infrastructure/Messaging/RoutingSlip/ActivityNames.cs create mode 100644 Infrastructure/Messaging/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs create mode 100644 Infrastructure/Messaging/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs create mode 100644 Infrastructure/Messaging/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs create mode 100644 Infrastructure/Messaging/RoutingSlipVariableNames.cs create mode 100644 Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 2eccd32..7992eaa 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -30,6 +30,9 @@ namespace Musify.Application.Configuration [Required] public string LargePictures { get; set; } = "LargePictures"; + [Required] + public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp"; + [Required] public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp"; diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index dc135f7..4d9d7a8 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -30,6 +30,9 @@ namespace Musify.Application.Configuration [Required] public string LargePictures { get; set; } = "LargeTracks"; + [Required] + public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp"; + [Required] public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp"; diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 7554c14..510e1f3 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -32,6 +32,7 @@ namespace Musify.Application.PlayLists.Handlers Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, + OriginalPictureKeyName = playListConfiguration.Routes.PresetOriginalPicture, SmallPictureKeyName = playListConfiguration.Routes.PresetSmallPicture, MediumPictureKeyName = playListConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, @@ -65,6 +66,8 @@ namespace Musify.Application.PlayLists.Handlers playList.Id, string.Join("; ", uploadResult.Errors)); return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } + + playList.OriginalPictureKeyName = pictureStorageKey; logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); try diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 2270ddc..8124cd1 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -30,6 +30,7 @@ namespace Musify.Application.Tracks.Handler { Title = request.Title, NormalizedTitle = request.Title.Trim().ToUpperInvariant(), + OriginalPictureKeyName = originalPictureKeyName, SmallPictureKeyName = trackConfiguration.Routes.PresetSmallPicture, MediumPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 44d751f..876854e 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -25,6 +25,10 @@ namespace Musify.Domain.Entities [MaxLength(254)] public required string Description { get; set; } = "No description was provided."; + [Required] + [MaxLength(64)] + public required string OriginalPictureKeyName { get; set; } + [Required] [MaxLength(64)] public required string SmallPictureKeyName { get; set; } diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index 8986fdd..888dfd6 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -16,6 +16,10 @@ namespace Musify.Domain.Entities [MaxLength(50)] public required string NormalizedTitle { get; set; } + [Required] + [MaxLength(64)] + public required string OriginalPictureKeyName { get; set; } + [Required] [MaxLength(64)] public required string SmallPictureKeyName { get; set; } diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index d26a09e..61aa300 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -32,4 +32,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs new file mode 100644 index 0000000..aecdd9a --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -0,0 +1,63 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class GenerateAudioWorkflowPathsActivity( + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IExecuteActivity + { + public const string ExecuteEndpointName = "Generate-Audio-Workflow-Paths"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(GenerateAudioWorkflowPathsActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + var folderName = Guid.NewGuid().ToString(); + var sourceFileName = Path.GetFileName(executeContext.Arguments.SourceKeyName); + var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, folderName); + var sourceFilePath = Path.Combine(workingDirectory, sourceFileName); + + Directory.CreateDirectory(workingDirectory); + + logger.LogDebug("Generated audio workflow paths. WorkingDirectory: {WorkingDirectory}, SourceFilePath: {SourceFilePath}", + workingDirectory, + sourceFilePath); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.CompletedWithVariables(new Dictionary + { + [RoutingSlipVariableNames.Workflow.TempDirectory] = workingDirectory, + [RoutingSlipVariableNames.Audio.WorkingDirectory] = workingDirectory, + [RoutingSlipVariableNames.Audio.SourceFilePath] = sourceFilePath, + [RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory + }); + } + catch (Exception exception) + { + logger.LogError(exception, "Error generating audio workflow paths for key {SourceKeyName}", + executeContext.Arguments.SourceKeyName); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + } +} diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs new file mode 100644 index 0000000..4856b8b --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -0,0 +1,89 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class TranscodeDashAudioActivity( + IAudioTranscoder audioTranscoder, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "Transcode-Dash-Audio"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(TranscodeDashAudioActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); + var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariableName); + + if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(workingDirectory)) + { + throw new InvalidOperationException("Transcode activity requires source file path and working directory variables."); + } + + try + { + await using var fileStream = File.OpenRead(sourceFilePath); + + var result = await audioTranscoder.TranscodeToDashAsync( + fileStream, + workingDirectory, + executeContext.CancellationToken); + + if (!result.IsSuccess) + { + var errorMessage = string.Join("; ", result.Errors); + logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", + sourceFilePath, errorMessage); + throw new Exception(errorMessage); + } + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error during transcoding activity for file {SourceFilePath}", + sourceFilePath); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public Task Compensate(CompensateContext compensateContext) + { + try + { + if (Directory.Exists(compensateContext.Log.WorkingDirectory)) + { + Directory.Delete(compensateContext.Log.WorkingDirectory, true); + } + + return Task.FromResult(compensateContext.Compensated()); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating transcoded audio directory {WorkingDirectory}", compensateContext.Log.WorkingDirectory); + return Task.FromResult(compensateContext.Failed(exception)); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs new file mode 100644 index 0000000..d64b602 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -0,0 +1,100 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class DownloadFileFromBucketActivity( + IStorageHandler storageHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "Download-File-From-Bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(DownloadFileFromBucketActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); + if (string.IsNullOrWhiteSpace(destinationPath)) + { + throw new InvalidOperationException( + $"Download activity requires variable '{executeContext.Arguments.DestinationFilePathVariableName}' with destination file path."); + } + + try + { + var getFile = await storageHandler.GetFileAsync( + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + executeContext.CancellationToken); + + if (!getFile.IsSuccess) + { + var errorMessage = string.Join("; ", getFile.Errors); + logger.LogWarning("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + 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; + + using var destinationStream = File.Create(destinationPath); + await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error downloading file from bucket"); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public Task Compensate(CompensateContext compensateContext) + { + try + { + if (File.Exists(compensateContext.Log.DestinationFilePath)) + { + File.Delete(compensateContext.Log.DestinationFilePath); + } + + return Task.FromResult(compensateContext.Compensated()); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating downloaded file {DestinationFilePath}", compensateContext.Log.DestinationFilePath); + return Task.FromResult(compensateContext.Failed(exception)); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs similarity index 100% rename from Infrastructure/Messaging/Activities/RemoveFileFromBucketActivity.cs rename to Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs new file mode 100644 index 0000000..ace2c4e --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs @@ -0,0 +1,109 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class TransferFilesToBucket( + IStorageHandler storageHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "Transfer-Files-To-Bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(TransferFilesToBucket), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + var folderPath = executeContext.GetVariable(executeContext.Arguments.SourceDirectoryVariableName); + if (string.IsNullOrWhiteSpace(folderPath)) + { + throw new InvalidOperationException( + $"Transfer activity requires variable '{executeContext.Arguments.SourceDirectoryVariableName}' with source directory path."); + } + + var destinationKeyName = executeContext.Arguments.DestinationKeyName; + if (string.IsNullOrWhiteSpace(destinationKeyName) && !string.IsNullOrWhiteSpace(executeContext.Arguments.DestinationKeyNameVariableName)) + { + destinationKeyName = executeContext.GetVariable(executeContext.Arguments.DestinationKeyNameVariableName); + } + + if (string.IsNullOrWhiteSpace(destinationKeyName)) + { + throw new InvalidOperationException("Transfer activity requires a destination key name from arguments or variables."); + } + + try + { + var uploadedKeys = Directory.GetFiles(folderPath) + .Select(file => $"{destinationKeyName}/{Path.GetFileName(file)}") + .ToArray(); + + var transferFilesResult = await storageHandler.TransferFilesAsync( + folderPath, + executeContext.Arguments.DestinationBucketName, + destinationKeyName, + executeContext.CancellationToken); + + if (!transferFilesResult.IsSuccess) + { + var errorMessage = string.Join("; ", transferFilesResult.Errors); + logger.LogWarning("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", + folderPath, + executeContext.Arguments.DestinationBucketName, + destinationKeyName, + errorMessage); + throw new Exception(errorMessage); + } + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error transferring files from {FolderPath} to bucket {DestinationBucketName}", + folderPath, + executeContext.Arguments.DestinationBucketName); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public async Task Compensate(CompensateContext compensateContext) + { + try + { + foreach (var uploadedKey in compensateContext.Log.UploadedKeys) + { + await storageHandler.RemoveFileAsync( + compensateContext.Log.DestinationBucketName, + uploadedKey, + compensateContext.CancellationToken); + } + + return compensateContext.Compensated(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating transferred files to bucket {DestinationBucketName}", compensateContext.Log.DestinationBucketName); + return compensateContext.Failed(exception); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs new file mode 100644 index 0000000..51dfc21 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -0,0 +1,76 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class GeneratePictureWorkflowPathsActivity( + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IExecuteActivity + { + public const string ExecuteEndpointName = "Generate-Picture-Workflow-Paths"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(GeneratePictureWorkflowPathsActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + var destinationFolderName = Guid.NewGuid().ToString(); + var originalFileName = Path.GetFileName(executeContext.Arguments.SourceKeyName); + var sourceFileExtension = Path.GetExtension(originalFileName); + var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, destinationFolderName); + + + var sourceFileName = Guid.NewGuid().ToString() + sourceFileExtension; + var smallPictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; + var mediumPictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; + var largePictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; + + var sourceFilePath = Path.Combine(workingDirectory, sourceFileName); + var smallPictureFilePath = Path.Combine(workingDirectory, smallPictureFileName); + var mediumPictureFilePath = Path.Combine(workingDirectory, mediumPictureFileName); + var largePictureFilePath = Path.Combine(workingDirectory, largePictureFileName); + + Directory.CreateDirectory(workingDirectory); + + logger.LogDebug("Generated picture workflow paths. WorkingDirectory: {WorkingDirectory}, SourceFilePath: {SourceFilePath}", + workingDirectory, + sourceFilePath); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.CompletedWithVariables(new Dictionary + { + [RoutingSlipVariableNames.Workflow.TempDirectory] = workingDirectory, + [RoutingSlipVariableNames.Picture.OriginalFilePath] = sourceFilePath, + [RoutingSlipVariableNames.Picture.SmallResizedFilePath] = smallPictureFilePath, + [RoutingSlipVariableNames.Picture.MediumResizedFilePath] = mediumPictureFilePath, + [RoutingSlipVariableNames.Picture.LargeResizedFilePath] = largePictureFilePath, + [RoutingSlipVariableNames.Picture.DestinationFolderName] = destinationFolderName + } ); + } + catch (Exception exception) + { + logger.LogError(exception, "Error generating picture workflow paths for key {SourceKeyName}", + executeContext.Arguments.SourceKeyName); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs new file mode 100644 index 0000000..975d52b --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -0,0 +1,113 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class ResizePictureActivity( + IPictureHandler pictureHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "Resize-Picture"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(ResizePictureActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); + var destinationFilePath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); + + if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(destinationFilePath)) + { + throw new InvalidOperationException("Resize activity requires source and destination file path variables."); + } + + try + { + if (!File.Exists(sourceFilePath)) + { + logger.LogWarning("Source file not found at {SourceFilePath}", sourceFilePath); + throw new FileNotFoundException($"Source file not found: {sourceFilePath}"); + } + + await using var fileStream = File.OpenRead(sourceFilePath); + + var resizedPicture = await pictureHandler.ResizePictureAsync( + fileStream, + executeContext.Arguments.Width, + 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: {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); + + logger.LogDebug("Picture resized from {SourceFilePath} to {DestinationFilePath} ({Width}x{Height})", + sourceFilePath, + destinationFilePath, + executeContext.Arguments.Width, + executeContext.Arguments.Height); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error resizing picture {SourceFilePath}", sourceFilePath); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public Task Compensate(CompensateContext compensateContext) + { + try + { + if (File.Exists(compensateContext.Log.DestinationFilePath)) + { + File.Delete(compensateContext.Log.DestinationFilePath); + } + + return Task.FromResult(compensateContext.Compensated()); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating resized file {DestinationFilePath}", compensateContext.Log.DestinationFilePath); + return Task.FromResult(compensateContext.Failed(exception)); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs new file mode 100644 index 0000000..bf44932 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -0,0 +1,107 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class UpdatePlayListPictureActivity( + IDatabase database, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "Update-PlayList-Picture"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(UpdatePlayListPictureActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + var playList = await database.PlayLists.FindAsync( + [executeContext.Arguments.PlayListId], + cancellationToken: executeContext.CancellationToken); + + if (playList is null) + { + logger.LogWarning("PlayList with id {PlayListId} not found", + executeContext.Arguments.PlayListId); + throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); + } + + var log = new UpdatePlayListPictureLog( + playList.Id, + playList.OriginalPictureKeyName, + playList.SmallPictureKeyName, + playList.MediumPictureKeyName, + playList.LargePictureKeyName); + + playList.OriginalPictureKeyName = executeContext.Arguments.OriginalPictureKeyName; + playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + + database.PlayLists.Update(playList); + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Updated PlayList {PlayListId} pictures", + executeContext.Arguments.PlayListId); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error updating PlayList {PlayListId} pictures", + executeContext.Arguments.PlayListId); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public async Task Compensate(CompensateContext compensateContext) + { + try + { + var playList = await database.PlayLists.FindAsync( + [compensateContext.Log.PlayListId], + cancellationToken: compensateContext.CancellationToken); + + if (playList is null) + { + return compensateContext.Compensated(); + } + + playList.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKeyName; + playList.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKeyName; + playList.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKeyName; + playList.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKeyName; + + database.PlayLists.Update(playList); + await database.SaveChangesAsync(compensateContext.CancellationToken); + + return compensateContext.Compensated(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating PlayList {PlayListId} pictures", compensateContext.Log.PlayListId); + return compensateContext.Failed(exception); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs new file mode 100644 index 0000000..88a0832 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -0,0 +1,107 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class UpdateTrackPictureActivity( + IDatabase database, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "Update-Track-Picture"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(UpdateTrackPictureActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + var track = await database.Tracks.FindAsync( + [executeContext.Arguments.TrackId], + cancellationToken: executeContext.CancellationToken); + + if (track is null) + { + logger.LogWarning("Track with id {TrackId} not found", + executeContext.Arguments.TrackId); + throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); + } + + var log = new UpdateTrackPictureLog( + track.Id, + track.OriginalPictureKeyName, + track.SmallPictureKeyName, + track.MediumPictureKeyName, + track.LargePictureKeyName); + + track.OriginalPictureKeyName = executeContext.Arguments.OriginalPictureKeyName; + track.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + track.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + track.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + + database.Tracks.Update(track); + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Updated Track {TrackId} pictures", + executeContext.Arguments.TrackId); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error updating Track {TrackId} pictures", + executeContext.Arguments.TrackId); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public async Task Compensate(CompensateContext compensateContext) + { + try + { + var track = await database.Tracks.FindAsync( + [compensateContext.Log.TrackId], + cancellationToken: compensateContext.CancellationToken); + + if (track is null) + { + return compensateContext.Compensated(); + } + + track.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKeyName; + track.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKeyName; + track.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKeyName; + track.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKeyName; + + database.Tracks.Update(track); + await database.SaveChangesAsync(compensateContext.CancellationToken); + + return compensateContext.Compensated(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating Track {TrackId} pictures", compensateContext.Log.TrackId); + return compensateContext.Failed(exception); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs similarity index 77% rename from Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs rename to Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs index 3b4ed66..ff0769d 100644 --- a/Infrastructure/Messaging/Activities/Arguments/DownloadFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs @@ -3,5 +3,5 @@ public record DownloadFileFromBucketArguments( string BucketName, string KeyName, - string DestinationPath); + string DestinationFilePathVariableName); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs new file mode 100644 index 0000000..1c74687 --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record GenerateAudioWorkflowPathsArguments( + string TemporaryRootDirectory, + string SourceKeyName); +} diff --git a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs new file mode 100644 index 0000000..3fcd2b8 --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs @@ -0,0 +1,9 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record GeneratePictureWorkflowPathsArguments( + string TemporaryRootDirectory, + string SourceKeyName, + string SmallPictureKeyName, + string MediumPictureKeyName, + string LargePictureKeyName); +} diff --git a/Infrastructure/Messaging/Activities/Arguments/RemoveFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs similarity index 100% rename from Infrastructure/Messaging/Activities/Arguments/RemoveFileFromBucketArguments.cs rename to Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs diff --git a/Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs similarity index 62% rename from Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs rename to Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs index 3e7ed42..bfd86e8 100644 --- a/Infrastructure/Messaging/Activities/Arguments/ResizePictureLocalArguments.cs +++ b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs @@ -1,8 +1,8 @@ namespace Musify.Infrastructure.Messaging.Activities.Arguments { public record ResizePictureLocalArguments( - string SourceFilePath, - string DestinationFilePath, + string SourceFilePathVariableName, + string DestinationFilePathVariableName, int Width, int Height); } diff --git a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs new file mode 100644 index 0000000..00190b8 --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record TranscodeDashAudioArguments( + string SourceFilePathVariableName, + string WorkingDirectoryVariableName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs similarity index 52% rename from Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs rename to Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs index b08f97b..5782e6c 100644 --- a/Infrastructure/Messaging/Activities/Arguments/TransferFilesToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs @@ -2,5 +2,7 @@ { public record TransferFilesToBucketArguments( string DestinationBucketName, - string DestinationKeyName); + string SourceDirectoryVariableName, + string? DestinationKeyName = null, + string? DestinationKeyNameVariableName = null); } \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs similarity index 86% rename from Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs rename to Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs index 7fbd4bc..8267f58 100644 --- a/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs @@ -2,6 +2,7 @@ { public record UpdatePlayListPictureArguments( Guid PlayListId, + string OriginalPictureKeyName, string SmallPictureKeyName, string MediumPictureKeyName, string LargePictureKeyName); diff --git a/Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs similarity index 86% rename from Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs rename to Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs index be1106f..797173d 100644 --- a/Infrastructure/Messaging/Activities/Arguments/UpdateTrackPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs @@ -2,6 +2,7 @@ { public record UpdateTrackPictureArguments( Guid TrackId, + string OriginalPictureKeyName, string SmallPictureKeyName, string MediumPictureKeyName, string LargePictureKeyName); diff --git a/Infrastructure/Messaging/Consumers/MessagingHelper.cs b/Infrastructure/MassTransit/Consumers/MessagingHelper.cs similarity index 64% rename from Infrastructure/Messaging/Consumers/MessagingHelper.cs rename to Infrastructure/MassTransit/Consumers/MessagingHelper.cs index 5fd9cbb..d08d82a0 100644 --- a/Infrastructure/Messaging/Consumers/MessagingHelper.cs +++ b/Infrastructure/MassTransit/Consumers/MessagingHelper.cs @@ -3,5 +3,7 @@ namespace Musify.Infrastructure.Messaging.Consumers internal static class MessagingHelper { internal static Uri BuildExecuteActivityUri(string endpointName) => new($"queue:{endpointName}_execute"); + + internal static Uri BuildCompensateActivityUri(string endpointName) => new($"queue:{endpointName}_compensate"); } } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs new file mode 100644 index 0000000..eb9f79f --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.RoutingSlip.Builders; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class RemoveFileConsumer( + IBus bus, + RemoveFileRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "Remove-File"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} diff --git a/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs new file mode 100644 index 0000000..f4908cc --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.RoutingSlip.Builders; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class TranscodeAudioFromTrackConsumer( + IBus bus, + AudioWorkflowRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "Transcode-Audio-From-Track"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs new file mode 100644 index 0000000..92b9792 --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.RoutingSlip.Builders; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class UpdatePlayListPictureConsumer( + IBus bus, + PictureWorkflowRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "Update-PlayList-Picture"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs new file mode 100644 index 0000000..8fba974 --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.RoutingSlip.Builders; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class UpdateTrackPictureConsumer( + IBus bus, + PictureWorkflowRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "Update-Track-Picture"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs new file mode 100644 index 0000000..4879893 --- /dev/null +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -0,0 +1,173 @@ +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.Messaging.Consumers; +using Musify.Infrastructure.Messaging.Filters; +using Musify.Infrastructure.Messaging.RoutingSlip.Builders; + +namespace Musify.Infrastructure.Messaging +{ + public static class MessagingDependencyInjection + { + public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddMassTransit(options => + { + options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => + ConfigureRabbitMqHost( + busFactoryConfigurator, + busRegistrationContext.GetRequiredService())); + } ); + + return serviceDescriptors; + } + + public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddScoped(); + serviceDescriptors.AddScoped(); + serviceDescriptors.AddScoped(); + + serviceDescriptors.AddMassTransit(options => + { + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + + options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => + { + ConfigureRabbitMqHost( + busFactoryConfigurator, + busRegistrationContext.GetRequiredService()); + + busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); + + busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + + busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + + busFactoryConfigurator.ReceiveEndpoint(TranscodeAudioFromTrackConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + + busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + RemoveFileFromBucketActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + GenerateAudioWorkflowPathsActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + GeneratePictureWorkflowPathsActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + ResizePictureActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdatePlayListPictureActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackPictureActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + DownloadFileFromBucketActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + TranscodeDashAudioActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + TransferFilesToBucket.ExecuteEndpointName); + } ); + } ); + + return serviceDescriptors; + } + + static void ConfigureRabbitMqHost( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + MessagingConfiguration massTransitConfiguration) + { + busFactoryConfigurator.Host(massTransitConfiguration.Host, options => + { + options.Username(massTransitConfiguration.Username); + options.Password(massTransitConfiguration.Password); + } ); + } + + static void ConfigureExecuteActivityEndpoint( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string endpointName) + where TActivity : class, IExecuteActivity + where TArguments : class + { + busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => + { + endpointConfigurator.ExecuteActivityHost(busRegistrationContext); + } ); + } + + static void ConfigureActivityEndpoint( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string endpointName) + where TActivity : class, IActivity + where TArguments : class + where TLog : class + { + busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => + { + endpointConfigurator.ExecuteActivityHost(busRegistrationContext); + } ); + + busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_compensate", endpointConfigurator => + { + endpointConfigurator.CompensateActivityHost(busRegistrationContext); + } ); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitEventBus.cs b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs similarity index 100% rename from Infrastructure/Messaging/MassTransitEventBus.cs rename to Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs diff --git a/Infrastructure/Messaging/Filters/ProcessTrackingConsumeFilter.cs b/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs similarity index 100% rename from Infrastructure/Messaging/Filters/ProcessTrackingConsumeFilter.cs rename to Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs diff --git a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs index b36e487..9b7ceb6 100644 --- a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs @@ -2,7 +2,8 @@ namespace Musify.Infrastructure.Messaging.Activities.Logs { public record UpdatePlayListPictureLog( Guid PlayListId, - string? PreviousSmallPictureKeyName, - string? PreviousMediumPictureKeyName, - string? PreviousLargePictureKeyName); + string PreviousOriginalPictureKeyName, + string PreviousSmallPictureKeyName, + string PreviousMediumPictureKeyName, + string PreviousLargePictureKeyName); } diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs index 0a9da77..bff1857 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs @@ -2,7 +2,8 @@ namespace Musify.Infrastructure.Messaging.Activities.Logs { public record UpdateTrackPictureLog( Guid TrackId, - string? PreviousSmallPictureKeyName, - string? PreviousMediumPictureKeyName, - string? PreviousLargePictureKeyName); + string PreviousOriginalPictureKeyName, + string PreviousSmallPictureKeyName, + string PreviousMediumPictureKeyName, + string PreviousLargePictureKeyName); } diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs new file mode 100644 index 0000000..6f52fa2 --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -0,0 +1,18 @@ +namespace Musify.Infrastructure.Messaging.RoutingSlip +{ + internal static class ActivityNames + { + internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths"; + internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths"; + internal const string DownloadFile = "DownloadFile"; + internal const string TranscodeAudio = "TranscodeAudio"; + internal const string ResizeSmall = "ResizeSmall"; + internal const string ResizeMedium = "ResizeMedium"; + internal const string ResizeLarge = "ResizeLarge"; + internal const string UploadFiles = "UploadFiles"; + internal const string RemoveFile = "RemoveFile"; + internal const string DeleteOriginal = "DeleteOriginal"; + internal const string UpdateTrackPicture = "UpdateTrackPicture"; + internal const string UpdatePlayListPicture = "UpdatePlayListPicture"; + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs new file mode 100644 index 0000000..262e2fc --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -0,0 +1,50 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders +{ + public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration) + { + public RoutingSlipBuilder Build(TranscodeAudioFromTrackEvent message, Guid? correlationId) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + + routingSlipBuilder.AddActivity( + ActivityNames.GenerateAudioWorkflowPaths, + Consumers.MessagingHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), + new GenerateAudioWorkflowPathsArguments( + workerConfiguration.Routes.TemporaryFilesDirectory, + message.SourceKeyName)); + + routingSlipBuilder.AddActivity( + ActivityNames.DownloadFile, + Consumers.MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( + message.SourceBucketName, + message.SourceKeyName, + RoutingSlipVariableNames.Audio.SourceFilePath)); + + routingSlipBuilder.AddActivity( + ActivityNames.TranscodeAudio, + Consumers.MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), + new TranscodeDashAudioArguments( + RoutingSlipVariableNames.Audio.SourceFilePath, + RoutingSlipVariableNames.Workflow.TempDirectory)); + + routingSlipBuilder.AddActivity( + ActivityNames.UploadFiles, + Consumers.MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + new TransferFilesToBucketArguments( + message.DestinationBucketName, + RoutingSlipVariableNames.Audio.TranscodedDirectory, + message.DestinationKeyName)); + + return routingSlipBuilder; + } + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs new file mode 100644 index 0000000..6abff35 --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -0,0 +1,159 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders +{ + public class PictureWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration) + { + public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) + { + var routingSlipBuilder = BuildPictureWorkflow( + message.SourceBucketName, + message.SourceKeyName, + message.SmallPictureKeyName, + message.SmallPictureWidth, + message.SmallPictureHeight, + message.MediumPictureKeyName, + message.MediumPictureWidth, + message.MediumPictureHeight, + message.LargePictureKeyName, + message.LargePictureWidth, + message.LargePictureHeight, + correlationId); + + routingSlipBuilder.AddActivity( + ActivityNames.UpdateTrackPicture, + Consumers.MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), + new UpdateTrackPictureArguments( + message.TrackId, + message.SourceKeyName, + message.SmallPictureKeyName, + message.MediumPictureKeyName, + message.LargePictureKeyName)); + + return routingSlipBuilder; + } + + public RoutingSlipBuilder Build(UpdatePlayListPictureEvent message, Guid? correlationId) + { + var routingSlipBuilder = BuildPictureWorkflow( + message.SourceBucketName, + message.SourceKeyName, + message.SmallPictureKeyName, + message.SmallPictureWidth, + message.SmallPictureHeight, + message.MediumPictureKeyName, + message.MediumPictureWidth, + message.MediumPictureHeight, + message.LargePictureKeyName, + message.LargePictureWidth, + message.LargePictureHeight, + correlationId); + + routingSlipBuilder.AddActivity( + ActivityNames.UpdatePlayListPicture, + Consumers.MessagingHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), + new UpdatePlayListPictureArguments( + message.PlayListId, + message.SourceKeyName, + message.SmallPictureKeyName, + message.MediumPictureKeyName, + message.LargePictureKeyName)); + + return routingSlipBuilder; + } + + RoutingSlipBuilder BuildPictureWorkflow( + string sourceBucketName, + string sourceKeyName, + string smallPictureKeyName, + int smallPictureWidth, + int smallPictureHeight, + string mediumPictureKeyName, + int mediumPictureWidth, + int mediumPictureHeight, + string largePictureKeyName, + int largePictureWidth, + int largePictureHeight, + Guid? correlationId) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + + routingSlipBuilder.AddActivity( + ActivityNames.GeneratePictureWorkflowPaths, + Consumers.MessagingHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), + new GeneratePictureWorkflowPathsArguments( + workerConfiguration.Routes.TemporaryFilesDirectory, + sourceKeyName, + smallPictureKeyName, + mediumPictureKeyName, + largePictureKeyName)); + + routingSlipBuilder.AddActivity( + ActivityNames.DownloadFile, + Consumers.MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + new DownloadFileFromBucketArguments( + sourceBucketName, + sourceKeyName, + RoutingSlipVariableNames.Picture.OriginalFilePath)); + + AddResizeActivity( + routingSlipBuilder, + ActivityNames.ResizeSmall, + RoutingSlipVariableNames.Picture.SmallResizedFilePath, + smallPictureWidth, + smallPictureHeight); + + AddResizeActivity( + routingSlipBuilder, + ActivityNames.ResizeMedium, + RoutingSlipVariableNames.Picture.MediumResizedFilePath, + mediumPictureWidth, + mediumPictureHeight); + + AddResizeActivity( + routingSlipBuilder, + ActivityNames.ResizeLarge, + RoutingSlipVariableNames.Picture.LargeResizedFilePath, + largePictureWidth, + largePictureHeight); + + routingSlipBuilder.AddActivity( + ActivityNames.DeleteOriginal, + Consumers.MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments(sourceBucketName, sourceKeyName)); + + routingSlipBuilder.AddActivity( + ActivityNames.UploadFiles, + Consumers.MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + new TransferFilesToBucketArguments( + sourceBucketName, + RoutingSlipVariableNames.Workflow.TempDirectory, + DestinationKeyNameVariableName: RoutingSlipVariableNames.Picture.DestinationFolderName)); + + return routingSlipBuilder; + } + + static void AddResizeActivity( + RoutingSlipBuilder routingSlipBuilder, + string activityName, + string destinationFilePathVariableName, + int width, + int height) + { + routingSlipBuilder.AddActivity( + activityName, + Consumers.MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), + new ResizePictureLocalArguments( + RoutingSlipVariableNames.Picture.OriginalFilePath, + destinationFilePathVariableName, + width, + height)); + } + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs new file mode 100644 index 0000000..3728465 --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs @@ -0,0 +1,25 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.Activities; + +namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders +{ + public class RemoveFileRoutingSlipBuilder + { + public RoutingSlipBuilder Build(RemoveFileEvent message, Guid? correlationId) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + + routingSlipBuilder.AddActivity( + ActivityNames.RemoveFile, + Consumers.MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new Activities.Arguments.RemoveFileFromBucketArguments( + message.BucketName, + message.KeyName)); + + return routingSlipBuilder; + } + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs new file mode 100644 index 0000000..3b9bbb0 --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -0,0 +1,27 @@ +namespace Musify.Infrastructure.Messaging +{ + internal static class RoutingSlipVariableNames + { + internal static class Workflow + { + internal const string TempDirectory = "Workflow.TempDirectory"; + internal const string CorrelationId = "Workflow.CorrelationId"; + } + + internal static class Audio + { + internal const string SourceFilePath = "Audio.SourceFilePath"; + internal const string WorkingDirectory = "Audio.WorkingDirectory"; + internal const string TranscodedDirectory = "Audio.TranscodedDirectory"; + } + + 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 const string DestinationFolderName = "Picture.DestinationFolderName"; + } + } +} diff --git a/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs b/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs deleted file mode 100644 index b5bbc96..0000000 --- a/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments -{ - public record CopyFileArguments( - string SourceBucketName, - string SourceKeyName, - string DestinationBucketName, - string DestinationKeyName); -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs b/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs deleted file mode 100644 index 8d54d7e..0000000 --- a/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments -{ - public record ResizePictureArgument( - string OriginalBucketName, - string OriginalKeyName, - string DestinationBucketName, - string DestinationKeyName, - int Width, - int Height); -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs deleted file mode 100644 index a983378..0000000 --- a/Infrastructure/Messaging/Activities/Arguments/TranscodeDashAudioArguments.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments -{ - public record TranscodeDashAudioArguments(); -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs index 5cf5e45..e69de29 100644 --- a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs @@ -1,80 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class DownloadFileFromBucketActivity( - IStorageHandler storageHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) - : IExecuteActivity - { - public const string ExecuteEndpointName = "Download-File-From-Bucket"; - - public async Task Execute(ExecuteContext executeContext) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(DownloadFileFromBucketActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - - try - { - var getFile = await storageHandler.GetFileAsync( - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, - executeContext.CancellationToken); - - if (!getFile.IsSuccess) - { - var errorMessage = string.Join("; ", getFile.Errors); - logger.LogWarning("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, - errorMessage); - throw new Exception(errorMessage); - } - - using var fileStream = getFile.Value; - if (fileStream.CanSeek) - fileStream.Position = 0; - - var fileName = Path.GetFileName(executeContext.Arguments.KeyName); - var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(executeContext.Arguments.DestinationPath, folderName); - var destinationPath = Path.Combine(workingDirectory, fileName); - - Directory.CreateDirectory(workingDirectory); - - using var destinationStream = File.Create(destinationPath); - await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.CompletedWithVariables(new - { - SourceFilePath = destinationPath, - WorkingDirectory = workingDirectory, - DestinationFolderName = folderName - } ); - } - catch (Exception exception) - { - logger.LogError(exception, "Error downloading file from bucket"); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/Messaging/Activities/GenerateAudioWorkflowPathsActivity.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/Messaging/Activities/GeneratePictureWorkflowPathsActivity.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs index 5c60a7e..e69de29 100644 --- a/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs +++ b/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs @@ -1,7 +0,0 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs -{ - public record DownloadFileFromBucketLog( - string BucketName, - string KeyName, - string DestinationPath); -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs b/Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs deleted file mode 100644 index 2345392..0000000 --- a/Infrastructure/Messaging/Activities/Logs/TranscodeDashAudioLog.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs -{ - public record TranscodeDashAudioLog(string WorkingDirectory); -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs index 6ea330f..e69de29 100644 --- a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -1,87 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class ResizePictureActivity( - IPictureHandler pictureHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) - : IExecuteActivity - { - public const string ExecuteEndpointName = "Resize-Picture"; - - public async Task Execute(ExecuteContext executeContext) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(ResizePictureActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - - try - { - if (!File.Exists(executeContext.Arguments.SourceFilePath)) - { - logger.LogWarning("Source file not found at {SourceFilePath}", executeContext.Arguments.SourceFilePath); - throw new FileNotFoundException($"Source file not found: {executeContext.Arguments.SourceFilePath}"); - } - - await using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); - - var resizedPicture = await pictureHandler.ResizePictureAsync( - fileStream, - executeContext.Arguments.Width, - 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: {Errors}", - executeContext.Arguments.SourceFilePath, - executeContext.Arguments.Width, - executeContext.Arguments.Height, - errorMessage); - throw new Exception(errorMessage); - } - - var destinationDirectory = Path.GetDirectoryName(executeContext.Arguments.DestinationFilePath); - if (!string.IsNullOrEmpty(destinationDirectory)) - { - Directory.CreateDirectory(destinationDirectory); - } - - await using var destinationStream = File.Create(executeContext.Arguments.DestinationFilePath); - await resizedPicture.Value.CopyToAsync(destinationStream, executeContext.CancellationToken); - - logger.LogDebug("Picture resized from {SourceFilePath} to {DestinationFilePath} ({Width}x{Height})", - executeContext.Arguments.SourceFilePath, - executeContext.Arguments.DestinationFilePath, - executeContext.Arguments.Width, - executeContext.Arguments.Height); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Error resizing picture {SourceFilePath}", - executeContext.Arguments.SourceFilePath); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs index 8094999..e69de29 100644 --- a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs +++ b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs @@ -1,70 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class TranscodeDashAudioActivity( - IAudioTranscoder audioTranscoder, - ILogger logger, - IProcessTrackingStore processTrackingStore) - : IExecuteActivity - { - public const string ExecuteEndpointName = "Transcode-Dash-Audio"; - - public async Task Execute(ExecuteContext executeContext) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(TranscodeDashAudioActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - - var sourceFilePath = executeContext.GetVariable("SourceFilePath"); - var workingDirectory = executeContext.GetVariable("WorkingDirectory"); - - if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(workingDirectory)) - { - throw new InvalidOperationException("Transcode activity requires SourceFilePath and WorkingDirectory variables."); - } - - try - { - await using var fileStream = File.OpenRead(sourceFilePath); - - var result = await audioTranscoder.TranscodeToDashAsync( - fileStream, - workingDirectory, - executeContext.CancellationToken); - - if (!result.IsSuccess) - { - var errorMessage = string.Join("; ", result.Errors); - logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", - sourceFilePath, errorMessage); - throw new Exception(errorMessage); - } - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Error during transcoding activity for file {SourceFilePath}", - sourceFilePath); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs index 13ca1e4..e69de29 100644 --- a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs +++ b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs @@ -1,71 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class TransferFilesToBucket( - IStorageHandler storageHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) - : IExecuteActivity - { - public const string ExecuteEndpointName = "Transfer-Files-To-Bucket"; - - public async Task Execute(ExecuteContext executeContext) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(TransferFilesToBucket), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - - var folderPath = executeContext.GetVariable("WorkingDirectory"); - if (string.IsNullOrWhiteSpace(folderPath)) - { - throw new InvalidOperationException("Transfer activity requires WorkingDirectory variable."); - } - - try - { - var transferFilesResult = await storageHandler.TransferFilesAsync( - folderPath, - executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, - executeContext.CancellationToken); - - if (!transferFilesResult.IsSuccess) - { - var errorMessage = string.Join("; ", transferFilesResult.Errors); - logger.LogWarning("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", - folderPath, - executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, - errorMessage); - throw new Exception(errorMessage); - } - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Error transferring files from {FolderPath} to bucket {DestinationBucketName}", - folderPath, - executeContext.Arguments.DestinationBucketName); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs index b369773..e69de29 100644 --- a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -1,68 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class UpdatePlayListPictureActivity( - IDatabase database, - ILogger logger, - IProcessTrackingStore processTrackingStore) - : IExecuteActivity - { - public const string ExecuteEndpointName = "Update-PlayList-Picture"; - - public async Task Execute(ExecuteContext executeContext) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(UpdatePlayListPictureActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - - try - { - var playList = await database.PlayLists.FindAsync( - [executeContext.Arguments.PlayListId], - cancellationToken: executeContext.CancellationToken); - - if (playList is null) - { - logger.LogWarning("PlayList with id {PlayListId} not found", - executeContext.Arguments.PlayListId); - throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); - } - - playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; - playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; - playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; - - database.PlayLists.Update(playList); - await database.SaveChangesAsync(executeContext.CancellationToken); - - logger.LogInformation("Updated PlayList {PlayListId} pictures", - executeContext.Arguments.PlayListId); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Error updating PlayList {PlayListId} pictures", - executeContext.Arguments.PlayListId); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs index 9b7291d..e69de29 100644 --- a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs +++ b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs @@ -1,68 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Activities -{ - public class UpdateTrackPictureActivity( - IDatabase database, - ILogger logger, - IProcessTrackingStore processTrackingStore) - : IExecuteActivity - { - public const string ExecuteEndpointName = "Update-Track-Picture"; - - public async Task Execute(ExecuteContext executeContext) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(UpdateTrackPictureActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - - try - { - var track = await database.Tracks.FindAsync( - [executeContext.Arguments.TrackId], - cancellationToken: executeContext.CancellationToken); - - if (track is null) - { - logger.LogWarning("Track with id {TrackId} not found", - executeContext.Arguments.TrackId); - throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); - } - - track.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; - track.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; - track.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; - - database.Tracks.Update(track); - await database.SaveChangesAsync(executeContext.CancellationToken); - - logger.LogInformation("Updated Track {TrackId} pictures", - executeContext.Arguments.TrackId); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Error updating Track {TrackId} pictures", - executeContext.Arguments.TrackId); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 8a78a1a..e69de29 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -1,28 +0,0 @@ -using MassTransit; -using Musify.Application.Events; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class RemoveFileConsumer(IBus bus) - : IConsumer - { - public const string QueueName = "Remove-File"; - - public async Task Consume(ConsumeContext consumeContext) - { - var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - - routingSlipBuilder.AddActivity( - "RemoveFile", - MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new RemoveFileFromBucketArguments( - consumeContext.Message.BucketName, - consumeContext.Message.KeyName)); - - var routingSlip = routingSlipBuilder.Build(); - await bus.Execute(routingSlip); - } - } -} diff --git a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs index faf5853..e69de29 100644 --- a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs +++ b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -1,44 +0,0 @@ -using MassTransit; -using Musify.Application.Events; -using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class TranscodeAudioFromTrackConsumer( - IBus bus, - WorkerConfiguration workerConfiguration) - : IConsumer - { - public const string QueueName = "Transcode-Audio-From-Track"; - - public async Task Consume(ConsumeContext consumeContext) - { - var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - - routingSlipBuilder.AddActivity( - "DownloadFile", - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), - new DownloadFileFromBucketArguments( - consumeContext.Message.SourceBucketName, - consumeContext.Message.SourceKeyName, - workerConfiguration.Routes.TemporaryFilesDirectory)); - - routingSlipBuilder.AddActivity( - "TranscodeAudio", - MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), - new TranscodeDashAudioArguments()); - - routingSlipBuilder.AddActivity( - "UploadFile", - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), - new TransferFilesToBucketArguments( - consumeContext.Message.DestinationBucketName, - consumeContext.Message.DestinationKeyName)); - - var routingSlip = routingSlipBuilder.Build(); - await bus.Execute(routingSlip); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs index 0ba7a98..e69de29 100644 --- a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -1,112 +0,0 @@ -using MassTransit; -using Musify.Application.Events; -using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class UpdatePlayListPictureConsumer( - IBus bus, - WorkerConfiguration workerConfiguration) - : IConsumer - { - public const string QueueName = "Update-PlayList-Picture"; - - public async Task Consume(ConsumeContext consumeContext) - { - var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(workerConfiguration.Routes.TemporaryFilesDirectory, folderName); - - routingSlipBuilder.AddActivity( - "DownloadFile", - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), - new DownloadFileFromBucketArguments( - consumeContext.Message.SourceBucketName, - consumeContext.Message.SourceKeyName, - workingDirectory)); - - AddResizeActivity( - routingSlipBuilder, - "ResizeSmall", - consumeContext.Message.SmallPictureKeyName, - consumeContext.Message.SmallPictureWidth, - consumeContext.Message.SmallPictureHeight, - consumeContext.Message.SourceKeyName, - consumeContext.Message.SourceBucketName, - workingDirectory); - - AddResizeActivity( - routingSlipBuilder, - "ResizeMedium", - consumeContext.Message.MediumPictureKeyName, - consumeContext.Message.MediumPictureWidth, - consumeContext.Message.MediumPictureHeight, - consumeContext.Message.SourceKeyName, - consumeContext.Message.SourceBucketName, - workingDirectory); - - AddResizeActivity( - routingSlipBuilder, - "ResizeLarge", - consumeContext.Message.LargePictureKeyName, - consumeContext.Message.LargePictureWidth, - consumeContext.Message.LargePictureHeight, - consumeContext.Message.SourceKeyName, - consumeContext.Message.SourceBucketName, - workingDirectory); - - routingSlipBuilder.AddActivity( - "DeleteOriginal", - MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new RemoveFileFromBucketArguments( - consumeContext.Message.SourceBucketName, - consumeContext.Message.SourceKeyName)); - - routingSlipBuilder.AddActivity( - "UploadFiles", - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), - new TransferFilesToBucketArguments( - consumeContext.Message.SourceBucketName, - folderName)); - - routingSlipBuilder.AddActivity( - "UpdatePlayListPicture", - MessagingHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), - new UpdatePlayListPictureArguments( - consumeContext.Message.PlayListId, - consumeContext.Message.SmallPictureKeyName, - consumeContext.Message.MediumPictureKeyName, - consumeContext.Message.LargePictureKeyName)); - - var routingSlip = routingSlipBuilder.Build(); - await bus.Execute(routingSlip); - } - - void AddResizeActivity( - RoutingSlipBuilder routingSlipBuilder, - string activityName, - string destinationKeyName, - int width, - int height, - string originalPictureKeyName, - string bucketName, - string workingDirectory) - { - var fileName = Path.GetFileName(originalPictureKeyName); - var sourceFilePath = Path.Combine(workingDirectory, fileName); - var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); - var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); - - routingSlipBuilder.AddActivity( - activityName, - MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), - new ResizePictureLocalArguments( - sourceFilePath, - destinationFilePath, - width, - height)); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs index f019285..e69de29 100644 --- a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs @@ -1,112 +0,0 @@ -using MassTransit; -using Musify.Application.Events; -using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class UpdateTrackPictureConsumer( - IBus bus, - WorkerConfiguration workerConfiguration) - : IConsumer - { - public const string QueueName = "Update-Track-Picture"; - - public async Task Consume(ConsumeContext consumeContext) - { - var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - var folderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(workerConfiguration.Routes.TemporaryFilesDirectory, folderName); - - routingSlipBuilder.AddActivity( - "DownloadFile", - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), - new DownloadFileFromBucketArguments( - consumeContext.Message.SourceBucketName, - consumeContext.Message.SourceKeyName, - workingDirectory)); - - AddResizeActivity( - routingSlipBuilder, - "ResizeSmall", - consumeContext.Message.SmallPictureKeyName, - consumeContext.Message.SmallPictureWidth, - consumeContext.Message.SmallPictureHeight, - consumeContext.Message.SourceKeyName, - consumeContext.Message.SourceBucketName, - workingDirectory); - - AddResizeActivity( - routingSlipBuilder, - "ResizeMedium", - consumeContext.Message.MediumPictureKeyName, - consumeContext.Message.MediumPictureWidth, - consumeContext.Message.MediumPictureHeight, - consumeContext.Message.SourceKeyName, - consumeContext.Message.SourceBucketName, - workingDirectory); - - AddResizeActivity( - routingSlipBuilder, - "ResizeLarge", - consumeContext.Message.LargePictureKeyName, - consumeContext.Message.LargePictureWidth, - consumeContext.Message.LargePictureHeight, - consumeContext.Message.SourceKeyName, - consumeContext.Message.SourceBucketName, - workingDirectory); - - routingSlipBuilder.AddActivity( - "DeleteOriginal", - MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new RemoveFileFromBucketArguments( - consumeContext.Message.SourceBucketName, - consumeContext.Message.SourceKeyName)); - - routingSlipBuilder.AddActivity( - "UploadFiles", - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), - new TransferFilesToBucketArguments( - consumeContext.Message.SourceBucketName, - folderName)); - - routingSlipBuilder.AddActivity( - "UpdateTrackPicture", - MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), - new UpdateTrackPictureArguments( - consumeContext.Message.TrackId, - consumeContext.Message.SmallPictureKeyName, - consumeContext.Message.MediumPictureKeyName, - consumeContext.Message.LargePictureKeyName)); - - var routingSlip = routingSlipBuilder.Build(); - await bus.Execute(routingSlip); - } - - void AddResizeActivity( - RoutingSlipBuilder routingSlipBuilder, - string activityName, - string destinationKeyName, - int width, - int height, - string originalPictureKeyName, - string bucketName, - string workingDirectory) - { - var fileName = Path.GetFileName(originalPictureKeyName); - var sourceFilePath = Path.Combine(workingDirectory, fileName); - var destinationFileName = Path.GetFileNameWithoutExtension(destinationKeyName) + Path.GetExtension(fileName); - var destinationFilePath = Path.Combine(workingDirectory, destinationFileName); - - routingSlipBuilder.AddActivity( - activityName, - MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), - new ResizePictureLocalArguments( - sourceFilePath, - destinationFilePath, - width, - height)); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/MessagingDependencyInjection.cs b/Infrastructure/Messaging/MessagingDependencyInjection.cs index 59e9c11..e69de29 100644 --- a/Infrastructure/Messaging/MessagingDependencyInjection.cs +++ b/Infrastructure/Messaging/MessagingDependencyInjection.cs @@ -1,139 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Consumers; -using Musify.Infrastructure.Messaging.Filters; - -namespace Musify.Infrastructure.Messaging -{ - public static class MessagingDependencyInjection - { - public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors) - { - serviceDescriptors.AddMassTransit(options => - { - options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => - ConfigureRabbitMqHost( - busFactoryConfigurator, - busRegistrationContext.GetRequiredService())); - } ); - - return serviceDescriptors; - } - - public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors) - { - serviceDescriptors.AddMassTransit(options => - { - // Register consumers - options.AddConsumer(); - options.AddConsumer(); - options.AddConsumer(); - options.AddConsumer(); - - // Register execute activities - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - - options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => - { - ConfigureRabbitMqHost( - busFactoryConfigurator, - busRegistrationContext.GetRequiredService()); - - busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); - - // Consumer endpoints - busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); - - busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); - - busFactoryConfigurator.ReceiveEndpoint(TranscodeAudioFromTrackConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); - - busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); - - // Activity endpoints - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - ResizePictureActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - RemoveFileFromBucketActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - UpdatePlayListPictureActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - UpdateTrackPictureActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - DownloadFileFromBucketActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - TranscodeDashAudioActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - TransferFilesToBucket.ExecuteEndpointName); - } ); - } ); - - return serviceDescriptors; - } - - static void ConfigureRabbitMqHost( - IRabbitMqBusFactoryConfigurator busFactoryConfigurator, - MessagingConfiguration massTransitConfiguration) - { - busFactoryConfigurator.Host(massTransitConfiguration.Host, options => - { - options.Username(massTransitConfiguration.Username); - options.Password(massTransitConfiguration.Password); - } ); - } - - static void ConfigureExecuteActivityEndpoint( - IRabbitMqBusFactoryConfigurator busFactoryConfigurator, - IBusRegistrationContext busRegistrationContext, - string endpointName) - where TActivity : class, IExecuteActivity - where TArguments : class - { - busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => - { - endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - } ); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/RoutingSlip/ActivityNames.cs b/Infrastructure/Messaging/RoutingSlip/ActivityNames.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/Messaging/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/Messaging/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs b/Infrastructure/Messaging/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/RoutingSlipVariableNames.cs b/Infrastructure/Messaging/RoutingSlipVariableNames.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs b/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs new file mode 100644 index 0000000..34c7024 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs @@ -0,0 +1,392 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList")] + partial class AddOriginalPictureKeyNameToTrackAndPlayList + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("character varying(254)"); + + b.Property("LargePictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("ErrorMessage") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FinishedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ProcessName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StartedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId", "ProcessName"); + + b.ToTable("ProcessExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Attempt") + .HasColumnType("integer"); + + b.Property("ComponentType") + .HasColumnType("integer"); + + b.Property("ErrorMessage") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("FinishedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ProcessExecutionId") + .HasColumnType("uuid"); + + b.Property("StartedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StepName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ProcessExecutionId", "StepName"); + + b.ToTable("ProcessStepExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("KeyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => + { + b.HasOne("Musify.Domain.Entities.ProcessExecution", "ProcessExecution") + .WithMany("Steps") + .HasForeignKey("ProcessExecutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProcessExecution"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs b/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs new file mode 100644 index 0000000..983bcd9 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddOriginalPictureKeyNameToTrackAndPlayList : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OriginalPictureKeyName", + table: "Tracks", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "OriginalPictureKeyName", + table: "PlayLists", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OriginalPictureKeyName", + table: "Tracks"); + + migrationBuilder.DropColumn( + name: "OriginalPictureKeyName", + table: "PlayLists"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index f859ef0..73fc038 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -56,6 +56,11 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("OriginalPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("SmallPictureKeyName") .IsRequired() .HasMaxLength(64) @@ -205,6 +210,11 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("OriginalPictureKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("SmallPictureKeyName") .IsRequired() .HasMaxLength(64) From 7223da54b4bbdfa933501e2af327fffe329cc414 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 24 Mar 2026 19:32:43 +0100 Subject: [PATCH 080/172] Changes --- .../Audio/GenerateAudioWorkflowPathsActivity.cs | 2 +- .../Audio/TranscodeDashAudioActivity.cs | 8 +------- .../Files/DownloadFileFromBucketActivity.cs | 8 +------- .../Activities/Files/TransferFilesToBucket.cs | 6 ------ .../Activities/Pictures/ResizePictureActivity.cs | 8 +------- .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 9 +++++---- .../Builders/PictureWorkflowRoutingSlipBuilder.cs | 15 ++++++++------- .../Builders/RemoveFileRoutingSlipBuilder.cs | 3 ++- 8 files changed, 19 insertions(+), 40 deletions(-) diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index aecdd9a..fd0b8f8 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -49,7 +49,7 @@ namespace Musify.Infrastructure.Messaging.Activities [RoutingSlipVariableNames.Audio.WorkingDirectory] = workingDirectory, [RoutingSlipVariableNames.Audio.SourceFilePath] = sourceFilePath, [RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory - }); + } ); } catch (Exception exception) { diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 4856b8b..0fc18ad 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -35,9 +35,7 @@ namespace Musify.Infrastructure.Messaging.Activities var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariableName); if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(workingDirectory)) - { throw new InvalidOperationException("Transcode activity requires source file path and working directory variables."); - } try { @@ -72,11 +70,7 @@ namespace Musify.Infrastructure.Messaging.Activities { try { - if (Directory.Exists(compensateContext.Log.WorkingDirectory)) - { - Directory.Delete(compensateContext.Log.WorkingDirectory, true); - } - + Directory.Delete(compensateContext.Log.WorkingDirectory, recursive: true); return Task.FromResult(compensateContext.Compensated()); } catch (Exception exception) diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index d64b602..15ce560 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -57,9 +57,7 @@ namespace Musify.Infrastructure.Messaging.Activities var destinationDirectory = Path.GetDirectoryName(destinationPath); if (!string.IsNullOrWhiteSpace(destinationDirectory)) - { Directory.CreateDirectory(destinationDirectory); - } using var fileStream = getFile.Value; if (fileStream.CanSeek) @@ -83,11 +81,7 @@ namespace Musify.Infrastructure.Messaging.Activities { try { - if (File.Exists(compensateContext.Log.DestinationFilePath)) - { - File.Delete(compensateContext.Log.DestinationFilePath); - } - + File.Delete(compensateContext.Log.DestinationFilePath); return Task.FromResult(compensateContext.Compensated()); } catch (Exception exception) diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs index ace2c4e..4fb8380 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs @@ -33,21 +33,15 @@ namespace Musify.Infrastructure.Messaging.Activities var folderPath = executeContext.GetVariable(executeContext.Arguments.SourceDirectoryVariableName); if (string.IsNullOrWhiteSpace(folderPath)) - { throw new InvalidOperationException( $"Transfer activity requires variable '{executeContext.Arguments.SourceDirectoryVariableName}' with source directory path."); - } var destinationKeyName = executeContext.Arguments.DestinationKeyName; if (string.IsNullOrWhiteSpace(destinationKeyName) && !string.IsNullOrWhiteSpace(executeContext.Arguments.DestinationKeyNameVariableName)) - { destinationKeyName = executeContext.GetVariable(executeContext.Arguments.DestinationKeyNameVariableName); - } if (string.IsNullOrWhiteSpace(destinationKeyName)) - { throw new InvalidOperationException("Transfer activity requires a destination key name from arguments or variables."); - } try { diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 975d52b..9438005 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -35,9 +35,7 @@ namespace Musify.Infrastructure.Messaging.Activities var destinationFilePath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(destinationFilePath)) - { throw new InvalidOperationException("Resize activity requires source and destination file path variables."); - } try { @@ -96,11 +94,7 @@ namespace Musify.Infrastructure.Messaging.Activities { try { - if (File.Exists(compensateContext.Log.DestinationFilePath)) - { - File.Delete(compensateContext.Log.DestinationFilePath); - } - + File.Delete(compensateContext.Log.DestinationFilePath); return Task.FromResult(compensateContext.Compensated()); } catch (Exception exception) diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 262e2fc..5ca26af 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -3,6 +3,7 @@ using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Consumers; namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders { @@ -16,14 +17,14 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.GenerateAudioWorkflowPaths, - Consumers.MessagingHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), new GenerateAudioWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKeyName)); routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, - Consumers.MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( message.SourceBucketName, message.SourceKeyName, @@ -31,14 +32,14 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.TranscodeAudio, - Consumers.MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), new TranscodeDashAudioArguments( RoutingSlipVariableNames.Audio.SourceFilePath, RoutingSlipVariableNames.Workflow.TempDirectory)); routingSlipBuilder.AddActivity( ActivityNames.UploadFiles, - Consumers.MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( message.DestinationBucketName, RoutingSlipVariableNames.Audio.TranscodedDirectory, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 6abff35..9334473 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -3,6 +3,7 @@ using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.Messaging.Consumers; namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders { @@ -26,7 +27,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UpdateTrackPicture, - Consumers.MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), new UpdateTrackPictureArguments( message.TrackId, message.SourceKeyName, @@ -55,7 +56,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UpdatePlayListPicture, - Consumers.MessagingHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), new UpdatePlayListPictureArguments( message.PlayListId, message.SourceKeyName, @@ -86,7 +87,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.GeneratePictureWorkflowPaths, - Consumers.MessagingHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), new GeneratePictureWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, sourceKeyName, @@ -96,7 +97,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, - Consumers.MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( sourceBucketName, sourceKeyName, @@ -125,12 +126,12 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.DeleteOriginal, - Consumers.MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments(sourceBucketName, sourceKeyName)); routingSlipBuilder.AddActivity( ActivityNames.UploadFiles, - Consumers.MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), new TransferFilesToBucketArguments( sourceBucketName, RoutingSlipVariableNames.Workflow.TempDirectory, @@ -148,7 +149,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders { routingSlipBuilder.AddActivity( activityName, - Consumers.MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), new ResizePictureLocalArguments( RoutingSlipVariableNames.Picture.OriginalFilePath, destinationFilePathVariableName, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs index 3728465..b6aa05a 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs @@ -1,6 +1,7 @@ using MassTransit; using Musify.Application.Events; using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Consumers; namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders { @@ -14,7 +15,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.RemoveFile, - Consumers.MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new Activities.Arguments.RemoveFileFromBucketArguments( message.BucketName, message.KeyName)); From f79183ec2463dfce10280bc70a84d99b48f1ae40 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 24 Mar 2026 19:47:32 +0100 Subject: [PATCH 081/172] Renames and migrations --- Infrastructure/Infrastructure.csproj | 2 - .../Files/RemoveFileFromBucketActivity.cs | 2 +- ...et.cs => TransferFilesToBucketActivity.cs} | 6 +- .../MessagingDependencyInjection.cs | 6 +- .../AudioWorkflowRoutingSlipBuilder.cs | 2 +- .../PictureWorkflowRoutingSlipBuilder.cs | 2 +- .../20260322023145_FirstMigration.Designer.cs | 382 ------------------ ...riginalPictureKeyNameToTrackAndPlayList.cs | 42 -- ...20260324183733_FirstMigration.Designer.cs} | 4 +- ...on.cs => 20260324183733_FirstMigration.cs} | 2 + 10 files changed, 13 insertions(+), 437 deletions(-) rename Infrastructure/MassTransit/Activities/Files/{TransferFilesToBucket.cs => TransferFilesToBucketActivity.cs} (96%) delete mode 100644 Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs delete mode 100644 Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs rename Infrastructure/Persistence/Migrations/{20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs => 20260324183733_FirstMigration.Designer.cs} (99%) rename Infrastructure/Persistence/Migrations/{20260322023145_FirstMigration.cs => 20260324183733_FirstMigration.cs} (97%) diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 61aa300..e59fe3f 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -38,10 +38,8 @@ - - diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 28ff4f4..82a5f8b 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -12,7 +12,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IExecuteActivity { - public const string ExecuteEndpointName = "Remove-File"; + public const string ExecuteEndpointName = "Remove-File-From-Bucket"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs similarity index 96% rename from Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs rename to Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 4fb8380..8ee6c9d 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucket.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -7,9 +7,9 @@ using Musify.Infrastructure.Messaging.Activities.Logs; namespace Musify.Infrastructure.Messaging.Activities { - public class TransferFilesToBucket( + public class TransferFilesToBucketActivity( IStorageHandler storageHandler, - ILogger logger, + ILogger logger, IProcessTrackingStore processTrackingStore) : IActivity { @@ -26,7 +26,7 @@ namespace Musify.Infrastructure.Messaging.Activities var stepId = await processTrackingStore.StartStepAsync( processId, - nameof(TransferFilesToBucket), + nameof(TransferFilesToBucketActivity), ProcessStepComponentType.Activity, 0, executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs index 4879893..1fff97a 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -47,7 +47,7 @@ namespace Musify.Infrastructure.Messaging options.AddActivity(); options.AddActivity(); options.AddActivity(); - options.AddActivity(); + options.AddActivity(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -117,10 +117,10 @@ namespace Musify.Infrastructure.Messaging busRegistrationContext, TranscodeDashAudioActivity.ExecuteEndpointName); - ConfigureActivityEndpoint( + ConfigureActivityEndpoint( busFactoryConfigurator, busRegistrationContext, - TransferFilesToBucket.ExecuteEndpointName); + TransferFilesToBucketActivity.ExecuteEndpointName); } ); } ); diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 5ca26af..b3cf18a 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -39,7 +39,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UploadFiles, - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), new TransferFilesToBucketArguments( message.DestinationBucketName, RoutingSlipVariableNames.Audio.TranscodedDirectory, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 9334473..95d7b5c 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -131,7 +131,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UploadFiles, - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucket.ExecuteEndpointName), + MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), new TransferFilesToBucketArguments( sourceBucketName, RoutingSlipVariableNames.Workflow.TempDirectory, diff --git a/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs b/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs deleted file mode 100644 index da7a421..0000000 --- a/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.Designer.cs +++ /dev/null @@ -1,382 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Musify.Infrastructure.Persistence; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(Database))] - [Migration("20260322023145_FirstMigration")] - partial class FirstMigration - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(254) - .HasColumnType("character varying(254)"); - - b.Property("LargePictureKeyName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MediumPictureKeyName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SmallPictureKeyName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("PlayLists"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PlayListId") - .HasColumnType("uuid"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("PlayListId"); - - b.HasIndex("TrackId"); - - b.ToTable("PlayListHasTrack"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConversationId") - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("ErrorMessage") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("FinishedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ProcessName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("StartedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CorrelationId", "ProcessName"); - - b.ToTable("ProcessExecution"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Attempt") - .HasColumnType("integer"); - - b.Property("ComponentType") - .HasColumnType("integer"); - - b.Property("ErrorMessage") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("FinishedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("ProcessExecutionId") - .HasColumnType("uuid"); - - b.Property("StartedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StepName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("ProcessExecutionId", "StepName"); - - b.ToTable("ProcessStepExecution"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LargePictureKeyName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MediumPictureKeyName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("SmallPictureKeyName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Upload", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BucketName") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("KeyName") - .IsRequired() - .HasColumnType("text"); - - b.Property("State") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("Upload"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.HasIndex("UserId"); - - b.ToTable("UserHasTrack"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("PlayLists") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") - .WithMany("PlayListTracks") - .HasForeignKey("PlayListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("PlayListTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("PlayList"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => - { - b.HasOne("Musify.Domain.Entities.ProcessExecution", "ProcessExecution") - .WithMany("Steps") - .HasForeignKey("ProcessExecutionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ProcessExecution"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("UserTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("UserTracks") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Track"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Navigation("PlayListTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => - { - b.Navigation("Steps"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Navigation("PlayListTracks"); - - b.Navigation("UserTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Navigation("PlayLists"); - - b.Navigation("UserTracks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs b/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs deleted file mode 100644 index 983bcd9..0000000 --- a/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddOriginalPictureKeyNameToTrackAndPlayList : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "OriginalPictureKeyName", - table: "Tracks", - type: "character varying(64)", - maxLength: 64, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "OriginalPictureKeyName", - table: "PlayLists", - type: "character varying(64)", - maxLength: 64, - nullable: false, - defaultValue: ""); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "OriginalPictureKeyName", - table: "Tracks"); - - migrationBuilder.DropColumn( - name: "OriginalPictureKeyName", - table: "PlayLists"); - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs b/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.Designer.cs similarity index 99% rename from Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs rename to Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.Designer.cs index 34c7024..3eea4a7 100644 --- a/Infrastructure/Persistence/Migrations/20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Musify.Infrastructure.Persistence.Migrations { [DbContext(typeof(Database))] - [Migration("20260323233603_AddOriginalPictureKeyNameToTrackAndPlayList")] - partial class AddOriginalPictureKeyNameToTrackAndPlayList + [Migration("20260324183733_FirstMigration")] + partial class FirstMigration { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.cs similarity index 97% rename from Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs rename to Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.cs index e32ecc2..43e95a3 100644 --- a/Infrastructure/Persistence/Migrations/20260322023145_FirstMigration.cs +++ b/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.cs @@ -37,6 +37,7 @@ namespace Musify.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), Title = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), NormalizedTitle = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + OriginalPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), @@ -111,6 +112,7 @@ namespace Musify.Infrastructure.Persistence.Migrations Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), NormalizedName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), Description = table.Column(type: "character varying(254)", maxLength: 254, nullable: false), + OriginalPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), From ecb544bbd277d1bb1759ed8ca6b29865766b2470 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 24 Mar 2026 19:55:31 +0100 Subject: [PATCH 082/172] Format --- .../Activities/Files/DownloadFileFromBucketActivity.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 15ce560..2c91593 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -33,10 +33,8 @@ namespace Musify.Infrastructure.Messaging.Activities var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); if (string.IsNullOrWhiteSpace(destinationPath)) - { throw new InvalidOperationException( $"Download activity requires variable '{executeContext.Arguments.DestinationFilePathVariableName}' with destination file path."); - } try { From 97f832530a5475a51dc672ac96cdff6a65c8bc95 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 24 Mar 2026 20:52:44 +0100 Subject: [PATCH 083/172] Delete temporal folders and some changes --- Infrastructure/Infrastructure.csproj | 4 -- .../GenerateAudioWorkflowPathsActivity.cs | 2 +- .../Files/TransferFilesToBucketActivity.cs | 2 +- .../GeneratePictureWorkflowPathsActivity.cs | 2 +- .../MassTransit/Consumers/MessagingHelper.cs | 2 + .../Consumers/RemoveFileConsumer.cs | 2 +- .../Consumers/RoutingSlipCleanUpConsumer.cs | 49 +++++++++++++++++++ .../MessagingDependencyInjection.cs | 9 +++- .../AudioWorkflowRoutingSlipBuilder.cs | 8 ++- .../PictureWorkflowRoutingSlipBuilder.cs | 8 ++- ...RemoveFileFromBucketRoutingSlipBuilder.cs} | 11 ++++- .../Variables/RoutingSlipVariableNames.cs | 2 +- 12 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs rename Infrastructure/MassTransit/RoutingSlip/Builders/{RemoveFileRoutingSlipBuilder.cs => RemoveFileFromBucketRoutingSlipBuilder.cs} (64%) diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index e59fe3f..98ee6a2 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -40,12 +40,8 @@ - - - - diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index fd0b8f8..1269dd4 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -45,7 +45,7 @@ namespace Musify.Infrastructure.Messaging.Activities await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new Dictionary { - [RoutingSlipVariableNames.Workflow.TempDirectory] = workingDirectory, + [RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory, [RoutingSlipVariableNames.Audio.WorkingDirectory] = workingDirectory, [RoutingSlipVariableNames.Audio.SourceFilePath] = sourceFilePath, [RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 8ee6c9d..2a2df4a 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.Messaging.Activities try { var uploadedKeys = Directory.GetFiles(folderPath) - .Select(file => $"{destinationKeyName}/{Path.GetFileName(file)}") + .Select(file => $"{destinationKeyName}/{Path.GetFileName(file) }") .ToArray(); var transferFilesResult = await storageHandler.TransferFilesAsync( diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 51dfc21..8899d00 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.Messaging.Activities await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new Dictionary { - [RoutingSlipVariableNames.Workflow.TempDirectory] = workingDirectory, + [RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory, [RoutingSlipVariableNames.Picture.OriginalFilePath] = sourceFilePath, [RoutingSlipVariableNames.Picture.SmallResizedFilePath] = smallPictureFilePath, [RoutingSlipVariableNames.Picture.MediumResizedFilePath] = mediumPictureFilePath, diff --git a/Infrastructure/MassTransit/Consumers/MessagingHelper.cs b/Infrastructure/MassTransit/Consumers/MessagingHelper.cs index d08d82a0..e5a3ec0 100644 --- a/Infrastructure/MassTransit/Consumers/MessagingHelper.cs +++ b/Infrastructure/MassTransit/Consumers/MessagingHelper.cs @@ -5,5 +5,7 @@ namespace Musify.Infrastructure.Messaging.Consumers internal static Uri BuildExecuteActivityUri(string endpointName) => new($"queue:{endpointName}_execute"); internal static Uri BuildCompensateActivityUri(string endpointName) => new($"queue:{endpointName}_compensate"); + + internal static Uri BuildConsumerUri(string endpointName) => new($"queue:{endpointName}"); } } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs index eb9f79f..5e1c644 100644 --- a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs @@ -6,7 +6,7 @@ namespace Musify.Infrastructure.Messaging.Consumers { public class RemoveFileConsumer( IBus bus, - RemoveFileRoutingSlipBuilder routingSlipBuilder) + RemoveFileFromBucketRoutingSlipBuilder routingSlipBuilder) : IConsumer { public const string QueueName = "Remove-File"; diff --git a/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs b/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs new file mode 100644 index 0000000..f98390c --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs @@ -0,0 +1,49 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Microsoft.Extensions.Logging; +using Musify.Infrastructure.Messaging; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class RoutingSlipCleanUpConsumer( + ILogger logger) : + IConsumer, + IConsumer + { + public const string QueueName = "routing-slip-events"; + + public Task Consume(ConsumeContext consumeContext) + => DeleteFolder(consumeContext.Message.Variables); + + public Task Consume(ConsumeContext consumeContext) + => DeleteFolder(consumeContext.Message.Variables); + + Task DeleteFolder(IDictionary variables) + { + if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.TemporalDirectory, out var pathObject)) + 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 directoryNotFoundException) + { + logger.LogWarning(directoryNotFoundException, "Temporal directory at path {Path} was not found for deletion", pathString); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to delete temporal directory at path {Path}", pathString); + throw; + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs index 1fff97a..3c35432 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -1,6 +1,7 @@ using MassTransit; using Microsoft.Extensions.DependencyInjection; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Activities.Logs; @@ -29,7 +30,7 @@ namespace Musify.Infrastructure.Messaging { serviceDescriptors.AddScoped(); serviceDescriptors.AddScoped(); - serviceDescriptors.AddScoped(); + serviceDescriptors.AddScoped(); serviceDescriptors.AddMassTransit(options => { @@ -37,6 +38,7 @@ namespace Musify.Infrastructure.Messaging options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); @@ -77,6 +79,11 @@ namespace Musify.Infrastructure.Messaging endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); + busFactoryConfigurator.ReceiveEndpoint(RoutingSlipCleanUpConsumer.QueueName, endpointConfigurator => + { + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + } ); + ConfigureExecuteActivityEndpoint( busFactoryConfigurator, busRegistrationContext, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index b3cf18a..11ae04b 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -1,6 +1,8 @@ using MassTransit; +using MassTransit.Courier.Contracts; using Musify.Application.Events; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Consumers; @@ -15,6 +17,10 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + routingSlipBuilder.AddSubscription( + MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + routingSlipBuilder.AddActivity( ActivityNames.GenerateAudioWorkflowPaths, MessagingHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), @@ -35,7 +41,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), new TranscodeDashAudioArguments( RoutingSlipVariableNames.Audio.SourceFilePath, - RoutingSlipVariableNames.Workflow.TempDirectory)); + RoutingSlipVariableNames.Workflow.TemporalDirectory)); routingSlipBuilder.AddActivity( ActivityNames.UploadFiles, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 95d7b5c..d81366e 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -1,6 +1,8 @@ using MassTransit; +using MassTransit.Courier.Contracts; using Musify.Application.Events; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Consumers; @@ -25,6 +27,10 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders message.LargePictureHeight, correlationId); + routingSlipBuilder.AddSubscription( + MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + routingSlipBuilder.AddActivity( ActivityNames.UpdateTrackPicture, MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), @@ -134,7 +140,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), new TransferFilesToBucketArguments( sourceBucketName, - RoutingSlipVariableNames.Workflow.TempDirectory, + RoutingSlipVariableNames.Workflow.TemporalDirectory, DestinationKeyNameVariableName: RoutingSlipVariableNames.Picture.DestinationFolderName)); return routingSlipBuilder; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs similarity index 64% rename from Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs rename to Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs index b6aa05a..a45bdb2 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs @@ -1,11 +1,14 @@ using MassTransit; +using MassTransit.Courier.Contracts; using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Consumers; namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders { - public class RemoveFileRoutingSlipBuilder + public class RemoveFileFromBucketRoutingSlipBuilder { public RoutingSlipBuilder Build(RemoveFileEvent message, Guid? correlationId) { @@ -13,10 +16,14 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + routingSlipBuilder.AddSubscription( + MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + routingSlipBuilder.AddActivity( ActivityNames.RemoveFile, MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new Activities.Arguments.RemoveFileFromBucketArguments( + new RemoveFileFromBucketArguments( message.BucketName, message.KeyName)); diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs index 3b9bbb0..760b767 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -4,7 +4,7 @@ namespace Musify.Infrastructure.Messaging { internal static class Workflow { - internal const string TempDirectory = "Workflow.TempDirectory"; + internal const string TemporalDirectory = "Workflow.TemporalDirectory"; internal const string CorrelationId = "Workflow.CorrelationId"; } From 255eb35fd0e2c588bff6424286ac0046d9d2e35d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 25 Mar 2026 00:35:23 +0100 Subject: [PATCH 084/172] Rename --- .../Activities/Audio/GenerateAudioWorkflowPathsActivity.cs | 2 +- .../MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs | 2 +- .../Activities/Files/DownloadFileFromBucketActivity.cs | 2 +- .../Activities/Files/RemoveFileFromBucketActivity.cs | 2 +- .../Activities/Files/TransferFilesToBucketActivity.cs | 2 +- .../Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs | 2 +- .../MassTransit/Activities/Pictures/ResizePictureActivity.cs | 2 +- .../Activities/Pictures/UpdatePlayListPictureActivity.cs | 2 +- .../Activities/Pictures/UpdateTrackPictureActivity.cs | 2 +- Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs | 2 +- .../MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs | 2 +- .../MassTransit/Consumers/UpdatePlayListPictureConsumer.cs | 2 +- .../MassTransit/Consumers/UpdateTrackPictureConsumer.cs | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 1269dd4..d5f807e 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -11,7 +11,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IExecuteActivity { - public const string ExecuteEndpointName = "Generate-Audio-Workflow-Paths"; + public const string ExecuteEndpointName = "generate-audio-workflow-paths"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 0fc18ad..123092f 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IActivity { - public const string ExecuteEndpointName = "Transcode-Dash-Audio"; + public const string ExecuteEndpointName = "transcode-dash-audio"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 2c91593..ecc3e34 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IActivity { - public const string ExecuteEndpointName = "Download-File-From-Bucket"; + public const string ExecuteEndpointName = "download-file-from-bucket"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 82a5f8b..ee68e77 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -12,7 +12,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IExecuteActivity { - public const string ExecuteEndpointName = "Remove-File-From-Bucket"; + public const string ExecuteEndpointName = "remove-file-from-bucket"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 2a2df4a..0c955eb 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IActivity { - public const string ExecuteEndpointName = "Transfer-Files-To-Bucket"; + public const string ExecuteEndpointName = "transfer-files-to-bucket"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 8899d00..a8bd767 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -11,7 +11,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IExecuteActivity { - public const string ExecuteEndpointName = "Generate-Picture-Workflow-Paths"; + public const string ExecuteEndpointName = "generate-picture-workflow-paths"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 9438005..9321df0 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IActivity { - public const string ExecuteEndpointName = "Resize-Picture"; + public const string ExecuteEndpointName = "resize-picture"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index bf44932..47a1c0f 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IActivity { - public const string ExecuteEndpointName = "Update-PlayList-Picture"; + public const string ExecuteEndpointName = "update-playlist-picture"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 88a0832..dd88b29 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.Activities IProcessTrackingStore processTrackingStore) : IActivity { - public const string ExecuteEndpointName = "Update-Track-Picture"; + public const string ExecuteEndpointName = "update-track-picture"; public async Task Execute(ExecuteContext executeContext) { diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs index 5e1c644..2a2e83a 100644 --- a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs @@ -9,7 +9,7 @@ namespace Musify.Infrastructure.Messaging.Consumers RemoveFileFromBucketRoutingSlipBuilder routingSlipBuilder) : IConsumer { - public const string QueueName = "Remove-File"; + public const string QueueName = "remove-file"; public async Task Consume(ConsumeContext consumeContext) { diff --git a/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs index f4908cc..9ce7347 100644 --- a/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs @@ -9,7 +9,7 @@ namespace Musify.Infrastructure.Messaging.Consumers AudioWorkflowRoutingSlipBuilder routingSlipBuilder) : IConsumer { - public const string QueueName = "Transcode-Audio-From-Track"; + public const string QueueName = "transcode-audio-from-track"; public async Task Consume(ConsumeContext consumeContext) { diff --git a/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs index 92b9792..75a849a 100644 --- a/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs @@ -9,7 +9,7 @@ namespace Musify.Infrastructure.Messaging.Consumers PictureWorkflowRoutingSlipBuilder routingSlipBuilder) : IConsumer { - public const string QueueName = "Update-PlayList-Picture"; + public const string QueueName = "update-playList-picture"; public async Task Consume(ConsumeContext consumeContext) { diff --git a/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs index 8fba974..9e138f3 100644 --- a/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs @@ -9,7 +9,7 @@ namespace Musify.Infrastructure.Messaging.Consumers PictureWorkflowRoutingSlipBuilder routingSlipBuilder) : IConsumer { - public const string QueueName = "Update-Track-Picture"; + public const string QueueName = "update-track-picture"; public async Task Consume(ConsumeContext consumeContext) { From cf5286a3194af3c9f290356a11ccbfe6d5c8fdf8 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 25 Mar 2026 02:13:47 +0100 Subject: [PATCH 085/172] Fixes --- .../Configuration/TrackConfiguration.cs | 13 ++- ...TrackEvent.cs => UpdateTrackAudioEvent.cs} | 5 +- .../Handlers/CreatePlayListCommandHandler.cs | 8 +- .../Handlers/DeletePlayListCommandHandler.cs | 31 ++++-- .../Handlers/UpdatePlayListCommandHandler.cs | 22 +++-- .../Handler/CreateTrackCommandHandler.cs | 30 ++++-- Domain/Entities/Track.cs | 7 ++ Infrastructure/Infrastructure.csproj | 2 - .../Audio/TranscodeDashAudioActivity.cs | 26 ++--- .../Audio/UpdateTrackAudioActivity.cs | 99 +++++++++++++++++++ .../Files/TransferFilesToBucketActivity.cs | 2 +- .../Pictures/UpdatePlayListPictureActivity.cs | 8 +- .../Pictures/UpdateTrackPictureActivity.cs | 8 +- .../Arguments/UpdateTrackAudioArguments.cs | 6 ++ ...onsumer.cs => UpdateTrackAudioConsumer.cs} | 10 +- .../MessagingDependencyInjection.cs | 10 +- .../MassTransit/RoutingSlip/ActivityNames.cs | 1 + .../AudioWorkflowRoutingSlipBuilder.cs | 13 ++- .../PictureWorkflowRoutingSlipBuilder.cs | 5 - ...20260325003221_FirstMigration.Designer.cs} | 12 ++- ...on.cs => 20260325003221_FirstMigration.cs} | 2 + .../Migrations/DatabaseModelSnapshot.cs | 10 ++ Infrastructure/Storage/StorageHandler.cs | 9 +- 23 files changed, 260 insertions(+), 79 deletions(-) rename Application/Events/{TranscodeAudioFromTrackEvent.cs => UpdateTrackAudioEvent.cs} (57%) create mode 100644 Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs create mode 100644 Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs rename Infrastructure/MassTransit/Consumers/{TranscodeAudioFromTrackConsumer.cs => UpdateTrackAudioConsumer.cs} (57%) rename Infrastructure/Persistence/Migrations/{20260324183733_FirstMigration.Designer.cs => 20260325003221_FirstMigration.Designer.cs} (96%) rename Infrastructure/Persistence/Migrations/{20260324183733_FirstMigration.cs => 20260325003221_FirstMigration.cs} (97%) diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index 4d9d7a8..f72dd79 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -19,16 +19,16 @@ namespace Musify.Application.Configuration public string ParentFolder { get; set; } = "Tracks"; [Required] - public string OriginalPictures { get; set; } = "OriginalTracks"; + public string OriginalPictures { get; set; } = "OriginalPictures"; [Required] - public string SmallPictures { get; set; } = "SmallTracks"; + public string SmallPictures { get; set; } = "SmallPictures"; [Required] - public string MediumPictures { get; set; } = "MediumTracks"; + public string MediumPictures { get; set; } = "MediumPictures"; [Required] - public string LargePictures { get; set; } = "LargeTracks"; + public string LargePictures { get; set; } = "LargePictures"; [Required] public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp"; @@ -43,7 +43,10 @@ namespace Musify.Application.Configuration public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; [Required] - public string Audios { get; set; } = "Audios"; + public string OriginalAudios { get; set; } = "OriginalAudios"; + + [Required] + public string ProcessedAudio { get; set; } = "ProcessedAudios"; } public class TrackPicturesSizes diff --git a/Application/Events/TranscodeAudioFromTrackEvent.cs b/Application/Events/UpdateTrackAudioEvent.cs similarity index 57% rename from Application/Events/TranscodeAudioFromTrackEvent.cs rename to Application/Events/UpdateTrackAudioEvent.cs index 69cd150..6891d9a 100644 --- a/Application/Events/TranscodeAudioFromTrackEvent.cs +++ b/Application/Events/UpdateTrackAudioEvent.cs @@ -1,8 +1,9 @@ namespace Musify.Application.Events { - public record TranscodeAudioFromTrackEvent( + public record UpdateTrackAudioEvent( + Guid TrackId, string SourceBucketName, string SourceKeyName, string DestinationBucketName, - string DestinationKeyName); + string DestinationFolderKeyName); } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 510e1f3..2c12c83 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -56,7 +56,10 @@ namespace Musify.Application.PlayLists.Handlers { var imageId = Guid.NewGuid(); var pictureName = imageId + picture.FileType; - var pictureStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); + var pictureStorageKey = Path.Join( + playListConfiguration.Routes.ParentFolders, + playListConfiguration.Routes.OriginalPictures, + pictureName); var uploadResult = await UploadFile(picture, pictureStorageKey, cancellationToken); @@ -97,12 +100,15 @@ namespace Musify.Application.PlayLists.Handlers async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) { var smallPictureKeyName = Path.Combine( + playListConfiguration.Routes.ParentFolders, playListConfiguration.Routes.SmallPictures, $"{playList.Id}.{pictureHandler.FileExtension}"); var mediumPictureKeyName = Path.Combine( + playListConfiguration.Routes.ParentFolders, playListConfiguration.Routes.MediumPictures, $"{playList.Id}.{pictureHandler.FileExtension}"); var largePictureKeyName = Path.Combine( + playListConfiguration.Routes.ParentFolders, playListConfiguration.Routes.LargePictures, $"{playList.Id}.{pictureHandler.FileExtension}"); diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index bcd8486..f248f6f 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -31,24 +31,35 @@ namespace Musify.Application.PlayLists.Handlers var keysToRemove = new List(); if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) - { - keysToRemove.Add(playList.SmallPictureKeyName); - } + keysToRemove.Add(Path.Combine( + playListConfiguration.Routes.ParentFolders, + playListConfiguration.Routes.SmallPictures, + playList.SmallPictureKeyName)); if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) - { - keysToRemove.Add(playList.MediumPictureKeyName); - } + keysToRemove.Add(Path.Combine( + playListConfiguration.Routes.ParentFolders, + playListConfiguration.Routes.MediumPictures, + playList.MediumPictureKeyName)); if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - { - keysToRemove.Add(playList.LargePictureKeyName); - } + keysToRemove.Add(Path.Combine( + playListConfiguration.Routes.ParentFolders, + playListConfiguration.Routes.LargePictures, + playList.LargePictureKeyName)); foreach (var key in keysToRemove) { var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); - if (!publishResult.IsSuccess && !publishResult.IsNotFound()) + if (!publishResult.IsSuccess) + { + if (publishResult.IsNotFound()) + { + logger.LogWarning("File with key={KeyName} not found in storage while trying to publish remove file event for PlayList with id={PlayListId}.", key, playList.Id); + continue; + } + return publishResult; + } } database.PlayLists.Remove(playList); diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index b682bbe..803f8ed 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -52,17 +52,17 @@ namespace Musify.Application.PlayLists.Handlers } if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) - { - await PublishRemoveFileEvent(playList.SmallPictureKeyName, cancellationToken); - } + await PublishRemoveFileEvent(Path.Combine( + playListConfiguration.Routes.SmallPictures, + playList.SmallPictureKeyName), cancellationToken); if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) - { - await PublishRemoveFileEvent(playList.MediumPictureKeyName, cancellationToken); - } + await PublishRemoveFileEvent(Path.Combine( + playListConfiguration.Routes.MediumPictures, + playList.MediumPictureKeyName), cancellationToken); if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - { - await PublishRemoveFileEvent(playList.LargePictureKeyName, cancellationToken); - } + await PublishRemoveFileEvent(Path.Combine( + playListConfiguration.Routes.LargePictures, + playList.LargePictureKeyName), cancellationToken); var newImageId = Guid.NewGuid(); var pictureName = newImageId + request.NewPicture.FileType; @@ -99,7 +99,9 @@ namespace Musify.Application.PlayLists.Handlers { await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, - keyName), cancellationToken); + Path.Combine( + playListConfiguration.Routes.ParentFolders, + keyName)), cancellationToken); return Result.NoContent(); } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 8124cd1..ccd74b4 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -25,17 +25,23 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User with id {request.UserId} not found"); } - var originalPictureKeyName = Path.Combine(trackConfiguration.Routes.OriginalPictures, Guid.NewGuid() + request.Picture.FileType); + var originalPictureFileName = Guid.NewGuid() + request.Picture.FileType; + var originalAudioFileName = Guid.NewGuid() + request.Audio.FileType; var track = new Track { Title = request.Title, NormalizedTitle = request.Title.Trim().ToUpperInvariant(), - OriginalPictureKeyName = originalPictureKeyName, + OriginalPictureKeyName = originalPictureFileName, + OriginalAudioKeyName = originalAudioFileName, SmallPictureKeyName = trackConfiguration.Routes.PresetSmallPicture, MediumPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, }; + var originalPictureKeyName = Path.Combine( + trackConfiguration.Routes.ParentFolder, + trackConfiguration.Routes.OriginalPictures, + originalPictureFileName); var pictureUploadResult = await UploadFile(request.Picture, originalPictureKeyName, cancellationToken); if (!pictureUploadResult.IsSuccess) { @@ -44,7 +50,10 @@ namespace Musify.Application.Tracks.Handler } - var audioKeyName = Path.Combine(trackConfiguration.Routes.Audios, Guid.NewGuid() + request.Audio.FileType); + var audioKeyName = Path.Combine( + trackConfiguration.Routes.ParentFolder, + trackConfiguration.Routes.OriginalAudios, + Guid.NewGuid() + request.Audio.FileType); var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); if (!audioUploadResult.IsSuccess) { @@ -93,12 +102,15 @@ namespace Musify.Application.Tracks.Handler async Task PublishUpdateEvent(string originalPictureKeyName, Track track, CancellationToken cancellationToken) { var smallPictureKeyName = Path.Combine( + trackConfiguration.Routes.ParentFolder, trackConfiguration.Routes.SmallPictures, $"{track.Id}.{pictureHandler.FileExtension}"); var mediumPictureKeyName = Path.Combine( + trackConfiguration.Routes.ParentFolder, trackConfiguration.Routes.MediumPictures, $"{track.Id}.{pictureHandler.FileExtension}"); var largePictureKeyName = Path.Combine( + trackConfiguration.Routes.ParentFolder, trackConfiguration.Routes.LargePictures, $"{track.Id}.{pictureHandler.FileExtension}"); @@ -119,15 +131,17 @@ namespace Musify.Application.Tracks.Handler async Task PublishTranscodeEvent(Guid trackId, string audioKeyName, CancellationToken cancellationToken) { - var destinationAudioKeyName = Path.Combine( - trackConfiguration.Routes.Audios, - trackId.ToString()); + var destinationFolderAudio = Path.Combine( + trackConfiguration.Routes.ParentFolder, + trackConfiguration.Routes.ProcessedAudio, + Guid.NewGuid().ToString()); - await eventBus.PublishAsync(new TranscodeAudioFromTrackEvent( + await eventBus.PublishAsync(new UpdateTrackAudioEvent( + trackId, storageConfiguration.BucketName, audioKeyName, storageConfiguration.BucketName, - destinationAudioKeyName), cancellationToken); + destinationFolderAudio), cancellationToken); } } } diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index 888dfd6..ba4f802 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -32,6 +32,13 @@ namespace Musify.Domain.Entities [MaxLength(64)] public required string LargePictureKeyName { get; set; } + [Required] + [MaxLength(64)] + public required string OriginalAudioKeyName { get; set; } + + [MaxLength(64)] + public string AudioFolderKeyName { get; set; } = string.Empty; + [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 98ee6a2..256cc80 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -38,9 +38,7 @@ - - diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 123092f..7665e75 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -39,21 +39,23 @@ namespace Musify.Infrastructure.Messaging.Activities try { - await using var fileStream = File.OpenRead(sourceFilePath); - - var result = await audioTranscoder.TranscodeToDashAsync( - fileStream, - workingDirectory, - executeContext.CancellationToken); - - if (!result.IsSuccess) + await using (var fileStream = File.OpenRead(sourceFilePath)) { - var errorMessage = string.Join("; ", result.Errors); - logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", - sourceFilePath, errorMessage); - throw new Exception(errorMessage); + var result = await audioTranscoder.TranscodeToDashAsync( + fileStream, + workingDirectory, + executeContext.CancellationToken); + + if (!result.IsSuccess) + { + var errorMessage = string.Join("; ", result.Errors); + logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", + sourceFilePath, errorMessage); + throw new Exception(errorMessage); + } } + File.Delete(sourceFilePath); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs new file mode 100644 index 0000000..2530b45 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -0,0 +1,99 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; +using Musify.Infrastructure.Messaging.Activities; + +namespace Musify.Infrastructure.MassTransit.Activities.Audio +{ + public class UpdateTrackAudioActivity( + IDatabase database, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "update-track-audio"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(UpdateTrackAudioActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + var track = await database.Tracks.FindAsync( + [executeContext.Arguments.TrackId], + cancellationToken: executeContext.CancellationToken); + + if (track is null) + { + logger.LogWarning("Track with id {TrackId} not found", + executeContext.Arguments.TrackId); + throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); + } + + var log = new UpdateTrackAudioLog( + track.Id, + track.AudioFolderKeyName); + + track.AudioFolderKeyName = Path.GetFileName(executeContext.Arguments.AudioFolderKeyName); + + database.Tracks.Update(track); + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Updated Track {TrackId} audio", + executeContext.Arguments.TrackId); + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error updating Track {TrackId} audio", + executeContext.Arguments.TrackId); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public async Task Compensate(CompensateContext compensateContext) + { + try + { + var track = await database.Tracks.FindAsync( + [compensateContext.Log.TrackId], + cancellationToken: compensateContext.CancellationToken); + + if (track is null) + { + return compensateContext.Compensated(); + } + + track.AudioFolderKeyName = compensateContext.Log.PreviousFolderAudioKeyName; + + database.Tracks.Update(track); + await database.SaveChangesAsync(compensateContext.CancellationToken); + + return compensateContext.Compensated(); + } + catch (Exception exception) + { + logger.LogError(exception, "Error compensating Track {TrackId} audio", compensateContext.Log.TrackId); + return compensateContext.Failed(exception); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 0c955eb..7af6d04 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.Messaging.Activities try { var uploadedKeys = Directory.GetFiles(folderPath) - .Select(file => $"{destinationKeyName}/{Path.GetFileName(file) }") + .Select(file => Path.Combine(destinationKeyName, Path.GetFileName(file))) .ToArray(); var transferFilesResult = await storageHandler.TransferFilesAsync( diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 47a1c0f..40a68a8 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -51,10 +51,10 @@ namespace Musify.Infrastructure.Messaging.Activities playList.MediumPictureKeyName, playList.LargePictureKeyName); - playList.OriginalPictureKeyName = executeContext.Arguments.OriginalPictureKeyName; - playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; - playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; - playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + playList.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKeyName); + playList.SmallPictureKeyName = Path.GetFileName(executeContext.Arguments.SmallPictureKeyName); + playList.MediumPictureKeyName = Path.GetFileName(executeContext.Arguments.MediumPictureKeyName); + playList.LargePictureKeyName = Path.GetFileName(executeContext.Arguments.LargePictureKeyName); database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index dd88b29..585b0b6 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -51,10 +51,10 @@ namespace Musify.Infrastructure.Messaging.Activities track.MediumPictureKeyName, track.LargePictureKeyName); - track.OriginalPictureKeyName = executeContext.Arguments.OriginalPictureKeyName; - track.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; - track.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; - track.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + track.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKeyName); + track.SmallPictureKeyName = Path.GetFileName(executeContext.Arguments.SmallPictureKeyName); + track.MediumPictureKeyName = Path.GetFileName(executeContext.Arguments.MediumPictureKeyName); + track.LargePictureKeyName = Path.GetFileName(executeContext.Arguments.LargePictureKeyName); database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs new file mode 100644 index 0000000..b3a741f --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record UpdateTrackAudioArguments( + Guid TrackId, + string AudioFolderKeyName); +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs similarity index 57% rename from Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs rename to Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs index 9ce7347..2d44e9c 100644 --- a/Infrastructure/MassTransit/Consumers/TranscodeAudioFromTrackConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs @@ -2,16 +2,16 @@ using Musify.Application.Events; using Musify.Infrastructure.Messaging.RoutingSlip.Builders; -namespace Musify.Infrastructure.Messaging.Consumers +namespace Musify.Infrastructure.MassTransit.Consumers { - public class TranscodeAudioFromTrackConsumer( + public class UpdateTrackAudioConsumer( IBus bus, AudioWorkflowRoutingSlipBuilder routingSlipBuilder) - : IConsumer + : IConsumer { - public const string QueueName = "transcode-audio-from-track"; + public const string QueueName = "update-track-audio"; - public async Task Consume(ConsumeContext consumeContext) + public async Task Consume(ConsumeContext consumeContext) { var routingSlip = routingSlipBuilder .Build(consumeContext.Message, consumeContext.CorrelationId) diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs index 3c35432..b2b4cc4 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -1,7 +1,10 @@ using MassTransit; using Microsoft.Extensions.DependencyInjection; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Activities.Audio; +using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; +using Musify.Infrastructure.MassTransit.Logs; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Activities.Logs; @@ -36,9 +39,9 @@ namespace Musify.Infrastructure.Messaging { options.AddConsumer(); options.AddConsumer(); - options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); @@ -50,6 +53,7 @@ namespace Musify.Infrastructure.Messaging options.AddActivity(); options.AddActivity(); options.AddActivity(); + options.AddActivity(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -69,9 +73,9 @@ namespace Musify.Infrastructure.Messaging endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); - busFactoryConfigurator.ReceiveEndpoint(TranscodeAudioFromTrackConsumer.QueueName, endpointConfigurator => + busFactoryConfigurator.ReceiveEndpoint(UpdateTrackAudioConsumer.QueueName, endpointConfigurator => { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); + endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 6f52fa2..b726249 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -14,5 +14,6 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip internal const string DeleteOriginal = "DeleteOriginal"; internal const string UpdateTrackPicture = "UpdateTrackPicture"; internal const string UpdatePlayListPicture = "UpdatePlayListPicture"; + internal const string UpdateTrackAudio = "UpdateTrackAudio"; } } diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 11ae04b..e93f33b 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -2,6 +2,8 @@ using MassTransit; using MassTransit.Courier.Contracts; using Musify.Application.Events; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Activities.Audio; +using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities.Arguments; @@ -11,7 +13,7 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders { public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration) { - public RoutingSlipBuilder Build(TranscodeAudioFromTrackEvent message, Guid? correlationId) + public RoutingSlipBuilder Build(UpdateTrackAudioEvent message, Guid? correlationId) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); @@ -49,7 +51,14 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders new TransferFilesToBucketArguments( message.DestinationBucketName, RoutingSlipVariableNames.Audio.TranscodedDirectory, - message.DestinationKeyName)); + message.DestinationFolderKeyName)); + + routingSlipBuilder.AddActivity( + ActivityNames.UpdateTrackAudio, + MessagingHelper.BuildExecuteActivityUri(UpdateTrackAudioActivity.ExecuteEndpointName), + new UpdateTrackAudioArguments( + message.TrackId, + message.DestinationFolderKeyName)); return routingSlipBuilder; } diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index d81366e..5ae5f8c 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -130,11 +130,6 @@ namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders largePictureWidth, largePictureHeight); - routingSlipBuilder.AddActivity( - ActivityNames.DeleteOriginal, - MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new RemoveFileFromBucketArguments(sourceBucketName, sourceKeyName)); - routingSlipBuilder.AddActivity( ActivityNames.UploadFiles, MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), diff --git a/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.Designer.cs b/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.Designer.cs similarity index 96% rename from Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.Designer.cs rename to Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.Designer.cs index 3eea4a7..e9a6cc7 100644 --- a/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Musify.Infrastructure.Persistence.Migrations { [DbContext(typeof(Database))] - [Migration("20260324183733_FirstMigration")] + [Migration("20260325003221_FirstMigration")] partial class FirstMigration { /// @@ -195,6 +195,11 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AudioFolderKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); @@ -213,6 +218,11 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("OriginalAudioKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("OriginalPictureKeyName") .IsRequired() .HasMaxLength(64) diff --git a/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.cs similarity index 97% rename from Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.cs rename to Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.cs index 43e95a3..5994892 100644 --- a/Infrastructure/Persistence/Migrations/20260324183733_FirstMigration.cs +++ b/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.cs @@ -41,6 +41,8 @@ namespace Musify.Infrastructure.Persistence.Migrations SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + OriginalAudioKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + AudioFolderKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) }, diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 73fc038..b4994a4 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -192,6 +192,11 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AudioFolderKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); @@ -210,6 +215,11 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("OriginalAudioKeyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("OriginalPictureKeyName") .IsRequired() .HasMaxLength(64) diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index cfc0d53..a61c822 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -80,27 +80,28 @@ namespace Musify.Infrastructure.Storage string fileName = Path.GetFileName(file); string filePath = Path.Combine(sourceDirectory, file); + var keyName = Path.Combine(route, fileName); var upload = new Upload { Id = Guid.NewGuid(), BucketName = bucketName, - KeyName = $"{route}/{fileName}", + KeyName = keyName, ContentType = "Application/Octet-Stream" }; await database.Uploads.AddAsync(upload, cancellationToken); try { - await trasnsferUtility.UploadAsync(filePath, bucketName, fileName, cancellationToken); + await trasnsferUtility.UploadAsync(filePath, bucketName, keyName, cancellationToken); upload.State = UploadState.Successful; successCount++; - logger.LogDebug("Transferred file to S3. Bucket: {BucketName}, Route: {Route}, File: {FileName}", bucketName, route, fileName); + logger.LogDebug("Transferred file to S3. Bucket: {BucketName}, KeyName: {KeyName}", bucketName, keyName); } catch (Exception exception) { failedCount++; upload.State = UploadState.Failed; - logger.LogError(exception, "Failed to transfer file to S3. Bucket: {BucketName}, Route: {Route}, File: {FileName}", bucketName, route, fileName); + logger.LogError(exception, "Failed to transfer file to S3. Bucket: {BucketName}, keyName: {keyName}", bucketName, keyName); } } From 89b380ee0cfbd2832dea16c8d20635ec09d8ae36 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 25 Mar 2026 10:41:23 +0100 Subject: [PATCH 086/172] File ignored --- Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs new file mode 100644 index 0000000..b1a93b5 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Logs +{ + public record UpdateTrackAudioLog(Guid TrackId, + string PreviousFolderAudioKeyName); +} From fb34f190766e55ef0c142903e92730a8545724bc Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 25 Mar 2026 18:09:37 +0100 Subject: [PATCH 087/172] Missing files --- .../Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs | 0 .../Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs | 0 Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs | 0 .../Messaging/Activities/Logs/TransferFilesToBucketLog.cs | 0 .../Messaging/Activities/Logs/UpdatePlayListPictureLog.cs | 0 Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs | 0 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Infrastructure/Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs create mode 100644 Infrastructure/Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs create mode 100644 Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs create mode 100644 Infrastructure/Messaging/Activities/Logs/TransferFilesToBucketLog.cs create mode 100644 Infrastructure/Messaging/Activities/Logs/UpdatePlayListPictureLog.cs create mode 100644 Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs diff --git a/Infrastructure/Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs b/Infrastructure/Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs b/Infrastructure/Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs b/Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/Logs/TransferFilesToBucketLog.cs b/Infrastructure/Messaging/Activities/Logs/TransferFilesToBucketLog.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/Messaging/Activities/Logs/UpdatePlayListPictureLog.cs new file mode 100644 index 0000000..e69de29 diff --git a/Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs b/Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs new file mode 100644 index 0000000..e69de29 From 83aeeeb82a341c71736921b0f81b3621539a14d1 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Wed, 25 Mar 2026 18:12:08 +0100 Subject: [PATCH 088/172] Fix namespaces --- Infrastructure/InfrastructureDependencyInjection.cs | 2 +- .../Audio/GenerateAudioWorkflowPathsActivity.cs | 4 ++-- .../Activities/Audio/TranscodeDashAudioActivity.cs | 6 +++--- .../Activities/Audio/UpdateTrackAudioActivity.cs | 2 +- .../Files/DownloadFileFromBucketActivity.cs | 6 +++--- .../Files/RemoveFileFromBucketActivity.cs | 4 ++-- .../Files/TransferFilesToBucketActivity.cs | 6 +++--- .../GeneratePictureWorkflowPathsActivity.cs | 4 ++-- .../Activities/Pictures/ResizePictureActivity.cs | 6 +++--- .../Pictures/UpdatePlayListPictureActivity.cs | 6 +++--- .../Pictures/UpdateTrackPictureActivity.cs | 6 +++--- .../Arguments/DownloadFileFromBucketArguments.cs | 2 +- .../GenerateAudioWorkflowPathsArguments.cs | 2 +- .../GeneratePictureWorkflowPathsArguments.cs | 2 +- .../Arguments/RemoveFileFromBucketArguments.cs | 2 +- .../Arguments/ResizePictureLocalArguments.cs | 2 +- .../Arguments/TranscodeDashAudioArguments.cs | 2 +- .../Arguments/TransferFilesToBucketArguments.cs | 2 +- .../Arguments/UpdatePlayListPictureArguments.cs | 2 +- .../Arguments/UpdateTrackPictureArguments.cs | 2 +- .../MassTransit/Consumers/MessagingHelper.cs | 2 +- .../MassTransit/Consumers/RemoveFileConsumer.cs | 4 ++-- .../Consumers/RoutingSlipCleanUpConsumer.cs | 2 +- .../Consumers/UpdatePlayListPictureConsumer.cs | 4 ++-- .../Consumers/UpdateTrackAudioConsumer.cs | 2 +- .../Consumers/UpdateTrackPictureConsumer.cs | 4 ++-- .../MessagingDependencyInjection.cs | 13 ++++++------- .../MassTransit/EventBus/MassTransitEventBus.cs | 2 +- .../Filters/ProcessTrackingConsumeFilter.cs | 2 +- .../MassTransit/Logs/DownloadFileFromBucketLog.cs | 2 +- .../Logs/GenerateAudioWorkflowPathsLog.cs | 2 +- .../Logs/GeneratePictureWorkflowPathsLog.cs | 2 +- Infrastructure/MassTransit/Logs/ResizePictureLog.cs | 2 +- .../MassTransit/Logs/TranscodeDashAudioLog.cs | 2 +- .../MassTransit/Logs/TransferFilesToBucketLog.cs | 2 +- .../MassTransit/Logs/UpdatePlayListPictureLog.cs | 2 +- .../MassTransit/Logs/UpdateTrackPictureLog.cs | 2 +- .../MassTransit/RoutingSlip/ActivityNames.cs | 2 +- .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 7 +++---- .../Builders/PictureWorkflowRoutingSlipBuilder.cs | 7 +++---- .../RemoveFileFromBucketRoutingSlipBuilder.cs | 7 +++---- .../Variables/RoutingSlipVariableNames.cs | 2 +- .../Activities/DownloadFileFromBucketActivity.cs | 0 .../GenerateAudioWorkflowPathsActivity.cs | 0 .../GeneratePictureWorkflowPathsActivity.cs | 0 .../Activities/Logs/DownloadFileFromBucketLog.cs | 0 .../Logs/GenerateAudioWorkflowPathsLog.cs | 0 .../Logs/GeneratePictureWorkflowPathsLog.cs | 0 .../Messaging/Activities/Logs/ResizePictureLog.cs | 0 .../Activities/Logs/TransferFilesToBucketLog.cs | 0 .../Activities/Logs/UpdatePlayListPictureLog.cs | 0 .../Activities/Logs/UpdateTrackPictureLog.cs | 0 .../Messaging/Activities/ResizePictureActivity.cs | 0 .../Activities/TranscodeDashAudioActivity.cs | 0 .../Messaging/Activities/TransferFilesToBucket.cs | 0 .../Activities/UpdatePlayListPictureActivity.cs | 0 .../Activities/UpdateTrackPictureActivity.cs | 0 .../Messaging/Consumers/RemoveFileConsumer.cs | 0 .../Consumers/TranscodeAudioFromTrackConsumer.cs | 0 .../Consumers/UpdatePlayListPictureConsumer.cs | 0 .../Consumers/UpdateTrackPictureConsumer.cs | 0 .../Messaging/MessagingDependencyInjection.cs | 0 .../Messaging/RoutingSlip/ActivityNames.cs | 0 .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 0 .../Builders/PictureWorkflowRoutingSlipBuilder.cs | 0 .../Builders/RemoveFileRoutingSlipBuilder.cs | 0 .../Messaging/RoutingSlipVariableNames.cs | 0 67 files changed, 71 insertions(+), 75 deletions(-) delete mode 100644 Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/GenerateAudioWorkflowPathsActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/GeneratePictureWorkflowPathsActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/TransferFilesToBucketLog.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/UpdatePlayListPictureLog.cs delete mode 100644 Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs delete mode 100644 Infrastructure/Messaging/Activities/ResizePictureActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/TransferFilesToBucket.cs delete mode 100644 Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs delete mode 100644 Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs delete mode 100644 Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs delete mode 100644 Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs delete mode 100644 Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs delete mode 100644 Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs delete mode 100644 Infrastructure/Messaging/MessagingDependencyInjection.cs delete mode 100644 Infrastructure/Messaging/RoutingSlip/ActivityNames.cs delete mode 100644 Infrastructure/Messaging/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs delete mode 100644 Infrastructure/Messaging/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs delete mode 100644 Infrastructure/Messaging/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs delete mode 100644 Infrastructure/Messaging/RoutingSlipVariableNames.cs diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs index 72263f8..d460c83 100644 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ b/Infrastructure/InfrastructureDependencyInjection.cs @@ -4,7 +4,7 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Audio; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Identity; -using Musify.Infrastructure.Messaging; +using Musify.Infrastructure.MassTransit; using Musify.Infrastructure.Observability; using Musify.Infrastructure.Persistence; using Musify.Infrastructure.Pictures; diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index d5f807e..ab57cc9 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -2,9 +2,9 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class GenerateAudioWorkflowPathsActivity( ILogger logger, diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 7665e75..efa4e51 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class TranscodeDashAudioActivity( IAudioTranscoder audioTranscoder, diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 2530b45..221271d 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -4,7 +4,7 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; -using Musify.Infrastructure.Messaging.Activities; +using Musify.Infrastructure.MassTransit.Activities; namespace Musify.Infrastructure.MassTransit.Activities.Audio { diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index ecc3e34..8ff1571 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class DownloadFileFromBucketActivity( IStorageHandler storageHandler, diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index ee68e77..1a37762 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -2,9 +2,9 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class RemoveFileFromBucketActivity( IStorageHandler storageHandler, diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 7af6d04..23078b9 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class TransferFilesToBucketActivity( IStorageHandler storageHandler, diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index a8bd767..af9eae7 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -2,9 +2,9 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class GeneratePictureWorkflowPathsActivity( ILogger logger, diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 9321df0..adbcb5b 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class ResizePictureActivity( IPictureHandler pictureHandler, diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 40a68a8..e8fd4e7 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class UpdatePlayListPictureActivity( IDatabase database, diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 585b0b6..713a9bf 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.Messaging.Activities +namespace Musify.Infrastructure.MassTransit.Activities { public class UpdateTrackPictureActivity( IDatabase database, diff --git a/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs index ff0769d..2d86e95 100644 --- a/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record DownloadFileFromBucketArguments( string BucketName, diff --git a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs index 1c74687..8d866be 100644 --- a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record GenerateAudioWorkflowPathsArguments( string TemporaryRootDirectory, diff --git a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs index 3fcd2b8..ad6fc1c 100644 --- a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record GeneratePictureWorkflowPathsArguments( string TemporaryRootDirectory, diff --git a/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs index 23e7058..0f13017 100644 --- a/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record RemoveFileFromBucketArguments( string BucketName, diff --git a/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs index bfd86e8..a50c957 100644 --- a/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs +++ b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record ResizePictureLocalArguments( string SourceFilePathVariableName, diff --git a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs index 00190b8..e48d678 100644 --- a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record TranscodeDashAudioArguments( string SourceFilePathVariableName, diff --git a/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs index 5782e6c..7087961 100644 --- a/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record TransferFilesToBucketArguments( string DestinationBucketName, diff --git a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs index 8267f58..f9fc4d2 100644 --- a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record UpdatePlayListPictureArguments( Guid PlayListId, diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs index 797173d..08cc747 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record UpdateTrackPictureArguments( Guid TrackId, diff --git a/Infrastructure/MassTransit/Consumers/MessagingHelper.cs b/Infrastructure/MassTransit/Consumers/MessagingHelper.cs index e5a3ec0..914781a 100644 --- a/Infrastructure/MassTransit/Consumers/MessagingHelper.cs +++ b/Infrastructure/MassTransit/Consumers/MessagingHelper.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Consumers +namespace Musify.Infrastructure.MassTransit.Consumers { internal static class MessagingHelper { diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs index 2a2e83a..bde736d 100644 --- a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs @@ -1,8 +1,8 @@ using MassTransit; using Musify.Application.Events; -using Musify.Infrastructure.Messaging.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; -namespace Musify.Infrastructure.Messaging.Consumers +namespace Musify.Infrastructure.MassTransit.Consumers { public class RemoveFileConsumer( IBus bus, diff --git a/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs b/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs index f98390c..bf78274 100644 --- a/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs @@ -1,7 +1,7 @@ using MassTransit; using MassTransit.Courier.Contracts; using Microsoft.Extensions.Logging; -using Musify.Infrastructure.Messaging; +using Musify.Infrastructure.MassTransit; namespace Musify.Infrastructure.MassTransit.Consumers { diff --git a/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs index 75a849a..fb93f7b 100644 --- a/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureConsumer.cs @@ -1,8 +1,8 @@ using MassTransit; using Musify.Application.Events; -using Musify.Infrastructure.Messaging.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; -namespace Musify.Infrastructure.Messaging.Consumers +namespace Musify.Infrastructure.MassTransit.Consumers { public class UpdatePlayListPictureConsumer( IBus bus, diff --git a/Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs index 2d44e9c..8cb1b5a 100644 --- a/Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/UpdateTrackAudioConsumer.cs @@ -1,6 +1,6 @@ using MassTransit; using Musify.Application.Events; -using Musify.Infrastructure.Messaging.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; namespace Musify.Infrastructure.MassTransit.Consumers { diff --git a/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs index 9e138f3..396c6e0 100644 --- a/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/UpdateTrackPictureConsumer.cs @@ -1,8 +1,8 @@ using MassTransit; using Musify.Application.Events; -using Musify.Infrastructure.Messaging.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; -namespace Musify.Infrastructure.Messaging.Consumers +namespace Musify.Infrastructure.MassTransit.Consumers { public class UpdateTrackPictureConsumer( IBus bus, diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs index b2b4cc4..7a10f39 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -5,14 +5,13 @@ using Musify.Infrastructure.MassTransit.Activities.Audio; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.MassTransit.Logs; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Activities.Logs; -using Musify.Infrastructure.Messaging.Consumers; -using Musify.Infrastructure.Messaging.Filters; -using Musify.Infrastructure.Messaging.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.Activities; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Infrastructure.MassTransit.Filters; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; -namespace Musify.Infrastructure.Messaging +namespace Musify.Infrastructure.MassTransit { public static class MessagingDependencyInjection { diff --git a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs index 0e15411..8c2bfef 100644 --- a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs +++ b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs @@ -1,7 +1,7 @@ using MassTransit; using Musify.Application.Contracts.Infrastructure; -namespace Musify.Infrastructure.Messaging +namespace Musify.Infrastructure.MassTransit { public class MassTransitEventBus(IPublishEndpoint publishEndpoint) : IEventBus diff --git a/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs b/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs index 3df0b38..e0ae97c 100644 --- a/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs +++ b/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs @@ -2,7 +2,7 @@ using MassTransit; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; -namespace Musify.Infrastructure.Messaging.Filters +namespace Musify.Infrastructure.MassTransit.Filters { public class ProcessTrackingConsumeFilter(IProcessTrackingStore processTrackingStore) : IFilter> diff --git a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs index 8b383f6..87e87fd 100644 --- a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record DownloadFileFromBucketLog( string BucketName, diff --git a/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs b/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs index c012bd9..fc2d8ac 100644 --- a/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs +++ b/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record GenerateAudioWorkflowPathsLog(string WorkingDirectory); } diff --git a/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs b/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs index 0bd6f43..7c23cba 100644 --- a/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs +++ b/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record GeneratePictureWorkflowPathsLog(string WorkingDirectory); } diff --git a/Infrastructure/MassTransit/Logs/ResizePictureLog.cs b/Infrastructure/MassTransit/Logs/ResizePictureLog.cs index 053a4dd..bfd59a0 100644 --- a/Infrastructure/MassTransit/Logs/ResizePictureLog.cs +++ b/Infrastructure/MassTransit/Logs/ResizePictureLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record ResizePictureLog(string DestinationFilePath); } diff --git a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs index 2345392..038ec0b 100644 --- a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs +++ b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record TranscodeDashAudioLog(string WorkingDirectory); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs index c4f59af..85d4553 100644 --- a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record TransferFilesToBucketLog(string DestinationBucketName, string[] UploadedKeys); } diff --git a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs index 9b7ceb6..da7482d 100644 --- a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record UpdatePlayListPictureLog( Guid PlayListId, diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs index bff1857..dda2cac 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record UpdateTrackPictureLog( Guid TrackId, diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index b726249..e0d07c7 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging.RoutingSlip +namespace Musify.Infrastructure.MassTransit.RoutingSlip { internal static class ActivityNames { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index e93f33b..d5f230d 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -5,11 +5,10 @@ using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities.Audio; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Consumers; +using Musify.Infrastructure.MassTransit.Activities; +using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders +namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration) { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 5ae5f8c..425331e 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -3,11 +3,10 @@ using MassTransit.Courier.Contracts; using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Consumers; +using Musify.Infrastructure.MassTransit.Activities; +using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders +namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { public class PictureWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration) { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs index a45bdb2..8336635 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs @@ -2,11 +2,10 @@ using MassTransit; using MassTransit.Courier.Contracts; using Musify.Application.Events; using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.Messaging.Activities; -using Musify.Infrastructure.Messaging.Activities.Arguments; -using Musify.Infrastructure.Messaging.Consumers; +using Musify.Infrastructure.MassTransit.Activities; +using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.Messaging.RoutingSlip.Builders +namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { public class RemoveFileFromBucketRoutingSlipBuilder { diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs index 760b767..8ffad05 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.Messaging +namespace Musify.Infrastructure.MassTransit { internal static class RoutingSlipVariableNames { diff --git a/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs b/Infrastructure/Messaging/Activities/DownloadFileFromBucketActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/Messaging/Activities/GenerateAudioWorkflowPathsActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/Messaging/Activities/GeneratePictureWorkflowPathsActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/Messaging/Activities/Logs/DownloadFileFromBucketLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs b/Infrastructure/Messaging/Activities/Logs/GenerateAudioWorkflowPathsLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs b/Infrastructure/Messaging/Activities/Logs/GeneratePictureWorkflowPathsLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs b/Infrastructure/Messaging/Activities/Logs/ResizePictureLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/TransferFilesToBucketLog.cs b/Infrastructure/Messaging/Activities/Logs/TransferFilesToBucketLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/Messaging/Activities/Logs/UpdatePlayListPictureLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs b/Infrastructure/Messaging/Activities/Logs/UpdateTrackPictureLog.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs b/Infrastructure/Messaging/Activities/TranscodeDashAudioActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs b/Infrastructure/Messaging/Activities/TransferFilesToBucket.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdateTrackPictureActivity.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs b/Infrastructure/Messaging/Consumers/TranscodeAudioFromTrackConsumer.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdateTrackPictureConsumer.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/MessagingDependencyInjection.cs b/Infrastructure/Messaging/MessagingDependencyInjection.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/RoutingSlip/ActivityNames.cs b/Infrastructure/Messaging/RoutingSlip/ActivityNames.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/Messaging/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/Messaging/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs b/Infrastructure/Messaging/RoutingSlip/Builders/RemoveFileRoutingSlipBuilder.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Infrastructure/Messaging/RoutingSlipVariableNames.cs b/Infrastructure/Messaging/RoutingSlipVariableNames.cs deleted file mode 100644 index e69de29..0000000 From 497f6a561fe112b161502a23467a81161e9bb9cb Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Wed, 25 Mar 2026 19:40:07 +0100 Subject: [PATCH 089/172] Update paths --- .../Configuration/PlayListConfiguration.cs | 22 ++++++++++--- .../Configuration/TrackConfiguration.cs | 30 +++++++++++++---- .../Handlers/CreatePlayListCommandHandler.cs | 22 ++++--------- .../Handlers/DeletePlayListCommandHandler.cs | 12 ++----- .../Handlers/UpdatePlayListCommandHandler.cs | 29 +++++++++-------- .../Handler/CreateTrackCommandHandler.cs | 32 ++++++------------- .../GeneratePictureWorkflowPathsActivity.cs | 1 - 7 files changed, 78 insertions(+), 70 deletions(-) diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 7992eaa..d887fd3 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -19,16 +19,16 @@ namespace Musify.Application.Configuration public string ParentFolders { get; set; } = "PlayLists"; [Required] - public string OriginalPictures { get; set; } = "OriginalPictures"; + public string OriginalPicturesFolder { get; set; } = "OriginalPictures"; [Required] - public string SmallPictures { get; set; } = "SmallPictures"; + public string SmallPicturesFolder { get; set; } = "SmallPictures"; [Required] - public string MediumPictures { get; set; } = "MediumPictures"; + public string MediumPicturesFolder { get; set; } = "MediumPictures"; [Required] - public string LargePictures { get; set; } = "LargePictures"; + public string LargePicturesFolder { get; set; } = "LargePictures"; [Required] public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp"; @@ -41,6 +41,20 @@ namespace Musify.Application.Configuration [Required] public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; + + public string SmallPicturesPath => Path.Combine(ParentFolders, SmallPicturesFolder); + + public string MediumPicturesPath => Path.Combine(ParentFolders, MediumPicturesFolder); + + public string LargePicturesPath => Path.Combine(ParentFolders, LargePicturesFolder); + + public string PresetSmallPicturePath => Path.Combine(ParentFolders, PresetSmallPicture); + + public string PresetMediumPicturePath => Path.Combine(ParentFolders, PresetMediumPicture); + + public string PresetLargePicturePath => Path.Combine(ParentFolders, PresetLargePicture); + + public string OriginalPicturesPath => Path.Combine(ParentFolders, OriginalPicturesFolder); } public class PlayListPicturesSizes diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index f72dd79..58cf2ce 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -19,16 +19,16 @@ namespace Musify.Application.Configuration public string ParentFolder { get; set; } = "Tracks"; [Required] - public string OriginalPictures { get; set; } = "OriginalPictures"; + public string OriginalPicturesFolder { get; set; } = "OriginalPictures"; [Required] - public string SmallPictures { get; set; } = "SmallPictures"; + public string SmallPicturesFolder { get; set; } = "SmallPictures"; [Required] - public string MediumPictures { get; set; } = "MediumPictures"; + public string MediumPicturesFolder { get; set; } = "MediumPictures"; [Required] - public string LargePictures { get; set; } = "LargePictures"; + public string LargePicturesFolder { get; set; } = "LargePictures"; [Required] public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp"; @@ -43,10 +43,28 @@ namespace Musify.Application.Configuration public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; [Required] - public string OriginalAudios { get; set; } = "OriginalAudios"; + public string OriginalAudiosFolder { get; set; } = "OriginalAudios"; [Required] - public string ProcessedAudio { get; set; } = "ProcessedAudios"; + public string ProcessedAudioFolder { get; set; } = "ProcessedAudios"; + + public string SmallPicturesPath => Path.Combine(ParentFolder, SmallPicturesFolder); + + public string MediumPicturesPath => Path.Combine(ParentFolder, MediumPicturesFolder); + + public string LargePicturesPath => Path.Combine(ParentFolder, LargePicturesFolder); + + public string PresetSmallPicturePath => Path.Combine(ParentFolder, PresetSmallPicture); + + public string PresetMediumPicturePath => Path.Combine(ParentFolder, PresetMediumPicture); + + public string PresetLargePicturePath => Path.Combine(ParentFolder, PresetLargePicture); + + public string OriginalPicturesPath => Path.Combine(ParentFolder, OriginalPicturesFolder); + + public string OriginalAudiosPath => Path.Combine(ParentFolder, OriginalAudiosFolder); + + public string ProcessedAudiosPath => Path.Combine(ParentFolder, ProcessedAudioFolder); } public class TrackPicturesSizes diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 2c12c83..c0ffe52 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -56,9 +56,7 @@ namespace Musify.Application.PlayLists.Handlers { var imageId = Guid.NewGuid(); var pictureName = imageId + picture.FileType; - var pictureStorageKey = Path.Join( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.OriginalPictures, + var pictureStorageKey = Path.Join(playListConfiguration.Routes.OriginalPicturesPath, pictureName); var uploadResult = await UploadFile(picture, pictureStorageKey, cancellationToken); @@ -99,18 +97,12 @@ namespace Musify.Application.PlayLists.Handlers async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) { - var smallPictureKeyName = Path.Combine( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.SmallPictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); - var mediumPictureKeyName = Path.Combine( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.MediumPictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); - var largePictureKeyName = Path.Combine( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.LargePictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); + var smallPictureKeyName = Path.Combine(playListConfiguration.Routes.SmallPicturesPath, + Guid.NewGuid() + pictureHandler.FileExtension); + var mediumPictureKeyName = Path.Combine(playListConfiguration.Routes.MediumPicturesPath, + Guid.NewGuid() + pictureHandler.FileExtension); + var largePictureKeyName = Path.Combine(playListConfiguration.Routes.LargePicturesPath, + Guid.NewGuid() + pictureHandler.FileExtension); await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index f248f6f..8f7838c 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -31,19 +31,13 @@ namespace Musify.Application.PlayLists.Handlers var keysToRemove = new List(); if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) - keysToRemove.Add(Path.Combine( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.SmallPictures, + keysToRemove.Add(Path.Combine(playListConfiguration.Routes.SmallPicturesPath, playList.SmallPictureKeyName)); if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) - keysToRemove.Add(Path.Combine( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.MediumPictures, + keysToRemove.Add(Path.Combine(playListConfiguration.Routes.MediumPicturesPath, playList.MediumPictureKeyName)); if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) - keysToRemove.Add(Path.Combine( - playListConfiguration.Routes.ParentFolders, - playListConfiguration.Routes.LargePictures, + keysToRemove.Add(Path.Combine(playListConfiguration.Routes.LargePicturesPath, playList.LargePictureKeyName)); foreach (var key in keysToRemove) diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 803f8ed..5e6a96c 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -53,20 +53,23 @@ namespace Musify.Application.PlayLists.Handlers if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) await PublishRemoveFileEvent(Path.Combine( - playListConfiguration.Routes.SmallPictures, - playList.SmallPictureKeyName), cancellationToken); + playListConfiguration.Routes.SmallPicturesPath, + playList.SmallPictureKeyName), + cancellationToken); if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) await PublishRemoveFileEvent(Path.Combine( - playListConfiguration.Routes.MediumPictures, - playList.MediumPictureKeyName), cancellationToken); + playListConfiguration.Routes.MediumPicturesPath, + playList.MediumPictureKeyName), + cancellationToken); if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) await PublishRemoveFileEvent(Path.Combine( - playListConfiguration.Routes.LargePictures, - playList.LargePictureKeyName), cancellationToken); + playListConfiguration.Routes.LargePicturesPath, + playList.LargePictureKeyName), + cancellationToken); var newImageId = Guid.NewGuid(); var pictureName = newImageId + request.NewPicture.FileType; - var newImageStorageKey = Path.Join(playListConfiguration.Routes.OriginalPictures, pictureName); + var newImageStorageKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); var uploadResult = await UploadFile(request.NewPicture, newImageStorageKey, cancellationToken); if (!uploadResult.IsSuccess) @@ -127,14 +130,14 @@ namespace Musify.Application.PlayLists.Handlers async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) { var smallPictureKeyName = Path.Combine( - playListConfiguration.Routes.SmallPictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); + playListConfiguration.Routes.SmallPicturesPath, + Guid.NewGuid() + pictureHandler.FileExtension); var mediumPictureKeyName = Path.Combine( - playListConfiguration.Routes.MediumPictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); + playListConfiguration.Routes.MediumPicturesPath, + Guid.NewGuid() + pictureHandler.FileExtension); var largePictureKeyName = Path.Combine( - playListConfiguration.Routes.LargePictures, - $"{playList.Id}.{pictureHandler.FileExtension}"); + playListConfiguration.Routes.LargePicturesPath, + Guid.NewGuid() + pictureHandler.FileExtension); await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index ccd74b4..f683fe4 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -33,14 +33,12 @@ namespace Musify.Application.Tracks.Handler NormalizedTitle = request.Title.Trim().ToUpperInvariant(), OriginalPictureKeyName = originalPictureFileName, OriginalAudioKeyName = originalAudioFileName, - SmallPictureKeyName = trackConfiguration.Routes.PresetSmallPicture, + SmallPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, MediumPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, }; - var originalPictureKeyName = Path.Combine( - trackConfiguration.Routes.ParentFolder, - trackConfiguration.Routes.OriginalPictures, + var originalPictureKeyName = Path.Combine(trackConfiguration.Routes.PresetMediumPicture, originalPictureFileName); var pictureUploadResult = await UploadFile(request.Picture, originalPictureKeyName, cancellationToken); if (!pictureUploadResult.IsSuccess) @@ -50,9 +48,7 @@ namespace Musify.Application.Tracks.Handler } - var audioKeyName = Path.Combine( - trackConfiguration.Routes.ParentFolder, - trackConfiguration.Routes.OriginalAudios, + var audioKeyName = Path.Combine(trackConfiguration.Routes.OriginalAudiosFolder, Guid.NewGuid() + request.Audio.FileType); var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); if (!audioUploadResult.IsSuccess) @@ -101,18 +97,12 @@ namespace Musify.Application.Tracks.Handler async Task PublishUpdateEvent(string originalPictureKeyName, Track track, CancellationToken cancellationToken) { - var smallPictureKeyName = Path.Combine( - trackConfiguration.Routes.ParentFolder, - trackConfiguration.Routes.SmallPictures, - $"{track.Id}.{pictureHandler.FileExtension}"); - var mediumPictureKeyName = Path.Combine( - trackConfiguration.Routes.ParentFolder, - trackConfiguration.Routes.MediumPictures, - $"{track.Id}.{pictureHandler.FileExtension}"); - var largePictureKeyName = Path.Combine( - trackConfiguration.Routes.ParentFolder, - trackConfiguration.Routes.LargePictures, - $"{track.Id}.{pictureHandler.FileExtension}"); + var smallPictureKeyName = Path.Combine(trackConfiguration.Routes.MediumPicturesPath, + track.Id + pictureHandler.FileExtension); + var mediumPictureKeyName = Path.Combine(trackConfiguration.Routes.MediumPicturesPath, + track.Id + pictureHandler.FileExtension); + var largePictureKeyName = Path.Combine(trackConfiguration.Routes.LargePicturesPath, + track.Id + pictureHandler.FileExtension); await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, @@ -131,9 +121,7 @@ namespace Musify.Application.Tracks.Handler async Task PublishTranscodeEvent(Guid trackId, string audioKeyName, CancellationToken cancellationToken) { - var destinationFolderAudio = Path.Combine( - trackConfiguration.Routes.ParentFolder, - trackConfiguration.Routes.ProcessedAudio, + var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudioFolder, Guid.NewGuid().ToString()); await eventBus.PublishAsync(new UpdateTrackAudioEvent( diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index af9eae7..57e27d3 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -36,7 +36,6 @@ namespace Musify.Infrastructure.MassTransit.Activities var sourceFileExtension = Path.GetExtension(originalFileName); var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, destinationFolderName); - var sourceFileName = Guid.NewGuid().ToString() + sourceFileExtension; var smallPictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; var mediumPictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; From c791113fbdce68510a01a31eba0b79ab0a1ccd03 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 26 Mar 2026 18:23:59 +0100 Subject: [PATCH 090/172] Upload file activity --- .../Files/DownloadFileFromBucketActivity.cs | 5 +- .../Files/UploadFileToBucketActivity.cs | 95 +++++++++++++++++++ .../Arguments/UploadFileToBucketArguments.cs | 8 ++ .../MessagingDependencyInjection.cs | 12 +++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs create mode 100644 Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 8ff1571..901c7d6 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -65,7 +65,10 @@ namespace Musify.Infrastructure.MassTransit.Activities await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); + return executeContext.Completed(new DownloadFileFromBucketLog( + executeContext.Arguments.BucketName, + executeContext.Arguments.KeyName, + destinationPath)); } catch (Exception exception) { diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs new file mode 100644 index 0000000..62356ae --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -0,0 +1,95 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; + +namespace Musify.Infrastructure.MassTransit.Activities.Files +{ + public class UploadFileToBucketActivity(IStorageHandler storageHandler, + ILogger logger, + IProcessTrackingStore processTrackingStore) + : IActivity + { + public const string ExecuteEndpointName = "upload-file-to-bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + var processId = await processTrackingStore.GetOrCreateProcessAsync( + "RoutingSlip", + executeContext.CorrelationId ?? executeContext.TrackingNumber, + executeContext.ConversationId, + executeContext.MessageId, + executeContext.CancellationToken); + + var stepId = await processTrackingStore.StartStepAsync( + processId, + nameof(UploadFileToBucketActivity), + ProcessStepComponentType.Activity, + 0, + executeContext.CancellationToken); + + try + { + using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); + + var uploadResult = await storageHandler.UploadFileAsync( + fileStream, + executeContext.Arguments.ContentType, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + executeContext.CancellationToken); + + if (!uploadResult.IsSuccess) + { + var errors = string.Join("; ", uploadResult.Errors); + logger.LogWarning("Failed to upload file to bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + errors); + throw new Exception(errors); + } + + await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); + return executeContext.Completed(new UploadFileToBucketLog( + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName)); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while uploading file to bucket for process {ProcessId} at step {StepId}", processId, stepId); + await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + throw; + } + } + + public async Task Compensate(CompensateContext compensateContext) + { + try + { + var uploadResult = await storageHandler.RemoveFileAsync( + compensateContext.Log.DestinationBucketName, + compensateContext.Log.DestinationKeyName, + compensateContext.CancellationToken); + + if (!uploadResult.IsSuccess) + { + var errors = string.Join("; ", uploadResult.Errors); + var errorMessage = $"Failed to remove file from bucket during compensation. Bucket: {compensateContext.Log.DestinationBucketName}, Key: {compensateContext.Log.DestinationKeyName}, Errors: {errors}"; + + logger.LogError(errorMessage); + return compensateContext.Failed(new Exception(errorMessage)); + } + return compensateContext.Compensated(); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while compensating upload file to bucket activity for bucket {BucketName} and key {KeyName}", + compensateContext.Log.DestinationBucketName, + compensateContext.Log.DestinationKeyName); + return compensateContext.Failed(exception); + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs new file mode 100644 index 0000000..7f58fcd --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record UploadFileToBucketArguments( + string SourceFilePath, + string ContentType, + string DestinationBucketName, + string DestinationKeyName); +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs index 7a10f39..af7b44c 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -10,6 +10,7 @@ using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; using Musify.Infrastructure.MassTransit.Filters; using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.Activities.Files; namespace Musify.Infrastructure.MassTransit { @@ -53,6 +54,7 @@ namespace Musify.Infrastructure.MassTransit options.AddActivity(); options.AddActivity(); options.AddActivity(); + options.AddActivity(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -131,6 +133,16 @@ namespace Musify.Infrastructure.MassTransit busFactoryConfigurator, busRegistrationContext, TransferFilesToBucketActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackAudioActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UploadFileToBucketActivity.ExecuteEndpointName); } ); } ); From 8431ed04c1f3631d1adf886ffb9c18f63be776a0 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 26 Mar 2026 18:24:11 +0100 Subject: [PATCH 091/172] Missing log --- Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs diff --git a/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs b/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs new file mode 100644 index 0000000..8dd9dd6 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.MassTransit.Logs +{ + public record UploadFileToBucketLog(string DestinationBucketName, string DestinationKeyName); +} \ No newline at end of file From a3a09e45c349cdb8d63b83f7564aa67c51320c9d Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 26 Mar 2026 19:59:44 +0100 Subject: [PATCH 092/172] Use variable --- .../Activities/Files/UploadFileToBucketActivity.cs | 6 +++++- .../MassTransit/Arguments/UploadFileToBucketArguments.cs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 62356ae..4f20d49 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -30,9 +30,13 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files 0, executeContext.CancellationToken); + var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariableName); + if (string.IsNullOrEmpty(sourceFilePath)) + throw new InvalidOperationException($"Upload activity requires variable '{executeContext.Arguments.FilePathVariableName}' with source file path."); + try { - using var fileStream = File.OpenRead(executeContext.Arguments.SourceFilePath); + using var fileStream = File.OpenRead(sourceFilePath); var uploadResult = await storageHandler.UploadFileAsync( fileStream, diff --git a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs index 7f58fcd..ab6ccda 100644 --- a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs @@ -1,7 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Arguments { public record UploadFileToBucketArguments( - string SourceFilePath, + string FilePathVariableName, string ContentType, string DestinationBucketName, string DestinationKeyName); From c4fae8b5e61436fb2bc228dff2b4192ed13f3915 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Fri, 27 Mar 2026 18:07:17 +0100 Subject: [PATCH 093/172] TransferFiles --- Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs | 1 + .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 2 +- .../Builders/PictureWorkflowRoutingSlipBuilder.cs | 8 -------- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index e0d07c7..3b6d6cf 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -9,6 +9,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip internal const string ResizeSmall = "ResizeSmall"; internal const string ResizeMedium = "ResizeMedium"; internal const string ResizeLarge = "ResizeLarge"; + internal const string TransferFiles = "TransferFiles"; internal const string UploadFiles = "UploadFiles"; internal const string RemoveFile = "RemoveFile"; internal const string DeleteOriginal = "DeleteOriginal"; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index d5f230d..6a29463 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -45,7 +45,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders RoutingSlipVariableNames.Workflow.TemporalDirectory)); routingSlipBuilder.AddActivity( - ActivityNames.UploadFiles, + ActivityNames.TransferFiles, MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), new TransferFilesToBucketArguments( message.DestinationBucketName, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 425331e..1c4043c 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -129,14 +129,6 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders largePictureWidth, largePictureHeight); - routingSlipBuilder.AddActivity( - ActivityNames.UploadFiles, - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), - new TransferFilesToBucketArguments( - sourceBucketName, - RoutingSlipVariableNames.Workflow.TemporalDirectory, - DestinationKeyNameVariableName: RoutingSlipVariableNames.Picture.DestinationFolderName)); - return routingSlipBuilder; } From 8e678435f4102d9cab6abf8086b4d438ceff6ecd Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Fri, 27 Mar 2026 21:16:38 +0100 Subject: [PATCH 094/172] Changes on routes --- .../Infrastructure/IStorageHandler.cs | 2 + Application/Events/UpdateTrackPictureEvent.cs | 6 +- .../Handler/CreateTrackCommandHandler.cs | 21 +++---- Infrastructure/Infrastructure.csproj | 1 + .../Files/UploadFileToBucketActivity.cs | 15 +++-- .../Pictures/ResizePictureActivity.cs | 5 +- .../GeneratePictureWorkflowPathsArguments.cs | 7 +-- .../Arguments/UploadFileToBucketArguments.cs | 3 +- .../MessagingDependencyInjection.cs | 12 ++-- .../MassTransit/RoutingSlip/ActivityNames.cs | 4 +- .../PictureWorkflowRoutingSlipBuilder.cs | 56 +++++++++++++------ .../Variables/RoutingSlipVariableNames.cs | 4 ++ Infrastructure/Storage/StorageHandler.cs | 16 ++++++ 13 files changed, 92 insertions(+), 60 deletions(-) diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs index c247c88..3056fab 100644 --- a/Application/Contracts/Infrastructure/IStorageHandler.cs +++ b/Application/Contracts/Infrastructure/IStorageHandler.cs @@ -8,6 +8,8 @@ namespace Musify.Application.Contracts.Infrastructure Task GetUrlAsync(string bucketName, string keyName, TimeSpan ExpirationTime, CancellationToken cancellationToken); + Task> UploadFileAsync(string filePath, string bucketName, string keyName, CancellationToken cancellationToken); + Task> UploadFileAsync(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken); Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); diff --git a/Application/Events/UpdateTrackPictureEvent.cs b/Application/Events/UpdateTrackPictureEvent.cs index 1ba00be..b9ae6e7 100644 --- a/Application/Events/UpdateTrackPictureEvent.cs +++ b/Application/Events/UpdateTrackPictureEvent.cs @@ -4,13 +4,13 @@ Guid TrackId, string SourceBucketName, string SourceKeyName, - string SmallPictureKeyName, + string SmallPictureRoute, int SmallPictureWidth, int SmallPictureHeight, - string MediumPictureKeyName, + string MediumPictureRoute, int MediumPictureWidth, int MediumPictureHeight, - string LargePictureKeyName, + string LargePictureRoute, int LargePictureWidth, int LargePictureHeight); } \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index f683fe4..b7c8220 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -11,7 +11,7 @@ using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler { - public class CreateTrackCommandHandler(IDatabase database, IPictureHandler pictureHandler, IEventBus eventBus, IStorageHandler storageHandler, + public class CreateTrackCommandHandler(IDatabase database, IEventBus eventBus, IStorageHandler storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) : IRequestHandler >> { @@ -38,7 +38,7 @@ namespace Musify.Application.Tracks.Handler LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, }; - var originalPictureKeyName = Path.Combine(trackConfiguration.Routes.PresetMediumPicture, + var originalPictureKeyName = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, originalPictureFileName); var pictureUploadResult = await UploadFile(request.Picture, originalPictureKeyName, cancellationToken); if (!pictureUploadResult.IsSuccess) @@ -48,7 +48,7 @@ namespace Musify.Application.Tracks.Handler } - var audioKeyName = Path.Combine(trackConfiguration.Routes.OriginalAudiosFolder, + var audioKeyName = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, Guid.NewGuid() + request.Audio.FileType); var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); if (!audioUploadResult.IsSuccess) @@ -97,31 +97,24 @@ namespace Musify.Application.Tracks.Handler async Task PublishUpdateEvent(string originalPictureKeyName, Track track, CancellationToken cancellationToken) { - var smallPictureKeyName = Path.Combine(trackConfiguration.Routes.MediumPicturesPath, - track.Id + pictureHandler.FileExtension); - var mediumPictureKeyName = Path.Combine(trackConfiguration.Routes.MediumPicturesPath, - track.Id + pictureHandler.FileExtension); - var largePictureKeyName = Path.Combine(trackConfiguration.Routes.LargePicturesPath, - track.Id + pictureHandler.FileExtension); - await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, storageConfiguration.BucketName, originalPictureKeyName, - smallPictureKeyName, + trackConfiguration.Routes.SmallPicturesPath, trackConfiguration.PicturesSizes.SmallPictureWidth, trackConfiguration.PicturesSizes.SmallPictureHeight, - mediumPictureKeyName, + trackConfiguration.Routes.MediumPicturesPath, trackConfiguration.PicturesSizes.MediumPictureWidth, trackConfiguration.PicturesSizes.MediumPictureHeight, - largePictureKeyName, + trackConfiguration.Routes.LargePicturesPath, trackConfiguration.PicturesSizes.LargePictureWidth, trackConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } async Task PublishTranscodeEvent(Guid trackId, string audioKeyName, CancellationToken cancellationToken) { - var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudioFolder, + var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, Guid.NewGuid().ToString()); await eventBus.PublishAsync(new UpdateTrackAudioEvent( diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 256cc80..85e8989 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -21,6 +21,7 @@ + diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 4f20d49..da16d42 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -1,5 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; +using MimeMapping; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Arguments; @@ -33,16 +34,14 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariableName); if (string.IsNullOrEmpty(sourceFilePath)) throw new InvalidOperationException($"Upload activity requires variable '{executeContext.Arguments.FilePathVariableName}' with source file path."); + var fileName = Path.GetFileName(sourceFilePath); try { - using var fileStream = File.OpenRead(sourceFilePath); - var uploadResult = await storageHandler.UploadFileAsync( - fileStream, - executeContext.Arguments.ContentType, + sourceFilePath, executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, + Path.Combine(executeContext.Arguments.DestinationRoute, fileName), executeContext.CancellationToken); if (!uploadResult.IsSuccess) @@ -50,15 +49,15 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files var errors = string.Join("; ", uploadResult.Errors); logger.LogWarning("Failed to upload file to bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName, + executeContext.Arguments.DestinationRoute, errors); throw new Exception(errors); } await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(new UploadFileToBucketLog( + return executeContext.CompletedWithVariables(new UploadFileToBucketLog( executeContext.Arguments.DestinationBucketName, - executeContext.Arguments.DestinationKeyName)); + executeContext.Arguments.DestinationRoute)); } catch (Exception exception) { diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index adbcb5b..461da5a 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -4,6 +4,7 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; namespace Musify.Infrastructure.MassTransit.Activities { @@ -66,9 +67,7 @@ namespace Musify.Infrastructure.MassTransit.Activities 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); @@ -80,7 +79,7 @@ namespace Musify.Infrastructure.MassTransit.Activities executeContext.Arguments.Height); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); + return executeContext.Completed(new ResizePictureLog(destinationFilePath)); } catch (Exception exception) { diff --git a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs index ad6fc1c..2460135 100644 --- a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs @@ -2,8 +2,5 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record GeneratePictureWorkflowPathsArguments( string TemporaryRootDirectory, - string SourceKeyName, - string SmallPictureKeyName, - string MediumPictureKeyName, - string LargePictureKeyName); -} + string SourceKeyName); +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs index ab6ccda..9d01163 100644 --- a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs @@ -2,7 +2,6 @@ { public record UploadFileToBucketArguments( string FilePathVariableName, - string ContentType, string DestinationBucketName, - string DestinationKeyName); + string DestinationRoute); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs index af7b44c..1406c5b 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs @@ -182,14 +182,14 @@ namespace Musify.Infrastructure.MassTransit where TLog : class { busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => - { - endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - } ); + endpointConfigurator.ExecuteActivityHost( + MessagingHelper.BuildCompensateActivityUri(endpointName), + busRegistrationContext) + ); busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_compensate", endpointConfigurator => - { - endpointConfigurator.CompensateActivityHost(busRegistrationContext); - } ); + endpointConfigurator.CompensateActivityHost(busRegistrationContext) + ); } } } \ No newline at end of file diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 3b6d6cf..85033b7 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -9,8 +9,10 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip 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 TransferFiles = "TransferFiles"; - internal const string UploadFiles = "UploadFiles"; internal const string RemoveFile = "RemoveFile"; internal const string DeleteOriginal = "DeleteOriginal"; internal const string UpdateTrackPicture = "UpdateTrackPicture"; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 1c4043c..5ddc032 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -1,10 +1,13 @@ using MassTransit; using MassTransit.Courier.Contracts; +using MimeMapping; using Musify.Application.Events; using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.MassTransit.Activities; using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Consumers; namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { @@ -15,13 +18,13 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders var routingSlipBuilder = BuildPictureWorkflow( message.SourceBucketName, message.SourceKeyName, - message.SmallPictureKeyName, + message.SmallPictureRoute, message.SmallPictureWidth, message.SmallPictureHeight, - message.MediumPictureKeyName, + message.MediumPictureRoute, message.MediumPictureWidth, message.MediumPictureHeight, - message.LargePictureKeyName, + message.LargePictureRoute, message.LargePictureWidth, message.LargePictureHeight, correlationId); @@ -36,9 +39,9 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders new UpdateTrackPictureArguments( message.TrackId, message.SourceKeyName, - message.SmallPictureKeyName, - message.MediumPictureKeyName, - message.LargePictureKeyName)); + message.SmallPictureRoute, + message.MediumPictureRoute, + message.LargePictureRoute)); return routingSlipBuilder; } @@ -73,15 +76,15 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders } RoutingSlipBuilder BuildPictureWorkflow( - string sourceBucketName, + string bucketName, string sourceKeyName, - string smallPictureKeyName, + string destinationSmallPictureRoute, int smallPictureWidth, int smallPictureHeight, - string mediumPictureKeyName, + string destinationMediumPictureRoute, int mediumPictureWidth, int mediumPictureHeight, - string largePictureKeyName, + string destinationLargePictureRoute, int largePictureWidth, int largePictureHeight, Guid? correlationId) @@ -95,37 +98,43 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders MessagingHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), new GeneratePictureWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, - sourceKeyName, - smallPictureKeyName, - mediumPictureKeyName, - largePictureKeyName)); + sourceKeyName)); routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( - sourceBucketName, + bucketName, sourceKeyName, RoutingSlipVariableNames.Picture.OriginalFilePath)); AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeSmall, + ActivityNames.UploadSmall, + bucketName, RoutingSlipVariableNames.Picture.SmallResizedFilePath, + destinationSmallPictureRoute, smallPictureWidth, smallPictureHeight); AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeMedium, + ActivityNames.ResizeMedium, + bucketName, RoutingSlipVariableNames.Picture.MediumResizedFilePath, + destinationMediumPictureRoute, mediumPictureWidth, mediumPictureHeight); AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeLarge, + ActivityNames.UploadLarge, + bucketName, RoutingSlipVariableNames.Picture.LargeResizedFilePath, + destinationLargePictureRoute, largePictureWidth, largePictureHeight); @@ -134,19 +143,30 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders static void AddResizeActivity( RoutingSlipBuilder routingSlipBuilder, - string activityName, + string resizeActivityName, + string uploadActivityName, + string bucketName, string destinationFilePathVariableName, + string destinationBucketRoute, int width, int height) { routingSlipBuilder.AddActivity( - activityName, + resizeActivityName, MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), new ResizePictureLocalArguments( RoutingSlipVariableNames.Picture.OriginalFilePath, destinationFilePathVariableName, width, height)); + + routingSlipBuilder.AddActivity( + uploadActivityName, + MessagingHelper.BuildExecuteActivityUri(UploadFileToBucketActivity.ExecuteEndpointName), + new UploadFileToBucketArguments( + destinationFilePathVariableName, + bucketName, + destinationBucketRoute)); } } } diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs index 8ffad05..f549121 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -22,6 +22,10 @@ namespace Musify.Infrastructure.MassTransit internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath"; internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath"; internal const string DestinationFolderName = "Picture.DestinationFolderName"; + + internal const string SmallPictureKeyName = "Picture.SmallPictureKeyName"; + internal const string MediumPictureKeyName = "Picture.MediumPictureKeyName"; + internal const string LargePictureKeyName = "Picture.LargePictureKeyName"; } } } diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index a61c822..44fce65 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -3,6 +3,7 @@ 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; @@ -111,6 +112,21 @@ namespace Musify.Infrastructure.Storage return Result.Success(); } + public async Task> UploadFileAsync(string filePath, string bucketName, string keyName, CancellationToken cancellationToken) + { + try + { + using var fileStream = File.OpenRead(filePath); + var contentType = MimeUtility.GetMimeMapping(filePath); + return await UploadFileAsync(fileStream, contentType, bucketName, keyName, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}"); + } + } + public async Task> UploadFileAsync(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken) { var request = new PutObjectRequest From 744ce1ae9661e12f0c16dcd03bb8420633eddcdd Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 27 Mar 2026 23:06:17 +0100 Subject: [PATCH 095/172] Fixed routes and bugs --- .../Infrastructure/IPictureHandler.cs | 6 +---- .../Events/UpdatePlayListPictureEvent.cs | 6 ++--- .../Handlers/CreatePlayListCommandHandler.cs | 17 ++++-------- .../Handlers/UpdatePlayListCommandHandler.cs | 18 +++---------- .../Audio/TranscodeDashAudioActivity.cs | 5 ++-- .../Audio/UpdateTrackAudioActivity.cs | 1 - .../Files/DownloadFileFromBucketActivity.cs | 4 +-- .../Files/TransferFilesToBucketActivity.cs | 10 ++----- .../Files/UploadFileToBucketActivity.cs | 3 +-- .../GeneratePictureWorkflowPathsActivity.cs | 21 +++++++-------- .../Pictures/ResizePictureActivity.cs | 7 +++-- .../Pictures/UpdatePlayListPictureActivity.cs | 13 ++++++--- .../Pictures/UpdateTrackPictureActivity.cs | 13 ++++++--- .../UpdatePlayListPictureArguments.cs | 6 ++--- .../Arguments/UpdateTrackPictureArguments.cs | 6 ++--- .../PictureWorkflowRoutingSlipBuilder.cs | 27 +++++++++---------- .../Variables/RoutingSlipVariableNames.cs | 4 --- Infrastructure/Pictures/PictureHandler.cs | 6 +---- 18 files changed, 72 insertions(+), 101 deletions(-) diff --git a/Application/Contracts/Infrastructure/IPictureHandler.cs b/Application/Contracts/Infrastructure/IPictureHandler.cs index e582b42..2662fc1 100644 --- a/Application/Contracts/Infrastructure/IPictureHandler.cs +++ b/Application/Contracts/Infrastructure/IPictureHandler.cs @@ -4,10 +4,6 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IPictureHandler { - public string FileExtension { get; } - - public string ContentType { get; } - - Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); + Task> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs index 5441f85..cdf89a9 100644 --- a/Application/Events/UpdatePlayListPictureEvent.cs +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -4,13 +4,13 @@ Guid PlayListId, string SourceBucketName, string SourceKeyName, - string SmallPictureKeyName, + string SmallPictureRoute, int SmallPictureWidth, int SmallPictureHeight, - string MediumPictureKeyName, + string MediumPictureRoute, int MediumPictureWidth, int MediumPictureHeight, - string LargePictureKeyName, + string LargePictureRoute, int LargePictureWidth, int LargePictureHeight); } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index c0ffe52..9973b6e 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -12,7 +12,7 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { - public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, IPictureHandler pictureHandler, + public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { @@ -68,7 +68,7 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); } - playList.OriginalPictureKeyName = pictureStorageKey; + playList.OriginalPictureKeyName = pictureName; logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); try @@ -97,24 +97,17 @@ namespace Musify.Application.PlayLists.Handlers async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) { - var smallPictureKeyName = Path.Combine(playListConfiguration.Routes.SmallPicturesPath, - Guid.NewGuid() + pictureHandler.FileExtension); - var mediumPictureKeyName = Path.Combine(playListConfiguration.Routes.MediumPicturesPath, - Guid.NewGuid() + pictureHandler.FileExtension); - var largePictureKeyName = Path.Combine(playListConfiguration.Routes.LargePicturesPath, - Guid.NewGuid() + pictureHandler.FileExtension); - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.BucketName, originalPictureKeyName, - smallPictureKeyName, + playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, playListConfiguration.PicturesSizes.SmallPictureHeight, - mediumPictureKeyName, + playListConfiguration.Routes.MediumPicturesPath, playListConfiguration.PicturesSizes.MediumPictureWidth, playListConfiguration.PicturesSizes.MediumPictureHeight, - largePictureKeyName, + playListConfiguration.Routes.LargePicturesPath, playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 5e6a96c..4ab50f3 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -14,7 +14,7 @@ namespace Musify.Application.PlayLists.Handlers { public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, - PlayListConfiguration playListConfiguration, IPictureHandler pictureHandler) + PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) @@ -129,27 +129,17 @@ namespace Musify.Application.PlayLists.Handlers async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) { - var smallPictureKeyName = Path.Combine( - playListConfiguration.Routes.SmallPicturesPath, - Guid.NewGuid() + pictureHandler.FileExtension); - var mediumPictureKeyName = Path.Combine( - playListConfiguration.Routes.MediumPicturesPath, - Guid.NewGuid() + pictureHandler.FileExtension); - var largePictureKeyName = Path.Combine( - playListConfiguration.Routes.LargePicturesPath, - Guid.NewGuid() + pictureHandler.FileExtension); - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.BucketName, originalPictureKeyName, - smallPictureKeyName, + playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, playListConfiguration.PicturesSizes.SmallPictureHeight, - mediumPictureKeyName, + playListConfiguration.Routes.MediumPicturesPath, playListConfiguration.PicturesSizes.MediumPictureWidth, playListConfiguration.PicturesSizes.MediumPictureHeight, - largePictureKeyName, + playListConfiguration.Routes.LargePicturesPath, playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); } diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index efa4e51..6ed7f2d 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -32,10 +32,9 @@ namespace Musify.Infrastructure.MassTransit.Activities executeContext.CancellationToken); var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); + ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariableName); - - if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(workingDirectory)) - throw new InvalidOperationException("Transcode activity requires source file path and working directory variables."); + ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); try { diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 221271d..d77c60b 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -4,7 +4,6 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; -using Musify.Infrastructure.MassTransit.Activities; namespace Musify.Infrastructure.MassTransit.Activities.Audio { diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 901c7d6..faf68e1 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -32,9 +32,7 @@ namespace Musify.Infrastructure.MassTransit.Activities executeContext.CancellationToken); var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); - if (string.IsNullOrWhiteSpace(destinationPath)) - throw new InvalidOperationException( - $"Download activity requires variable '{executeContext.Arguments.DestinationFilePathVariableName}' with destination file path."); + ArgumentNullException.ThrowIfNull(destinationPath, nameof(destinationPath)); try { diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 23078b9..27c92ca 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -32,16 +32,10 @@ namespace Musify.Infrastructure.MassTransit.Activities executeContext.CancellationToken); var folderPath = executeContext.GetVariable(executeContext.Arguments.SourceDirectoryVariableName); - if (string.IsNullOrWhiteSpace(folderPath)) - throw new InvalidOperationException( - $"Transfer activity requires variable '{executeContext.Arguments.SourceDirectoryVariableName}' with source directory path."); + ArgumentNullException.ThrowIfNull(folderPath, nameof(folderPath)); var destinationKeyName = executeContext.Arguments.DestinationKeyName; - if (string.IsNullOrWhiteSpace(destinationKeyName) && !string.IsNullOrWhiteSpace(executeContext.Arguments.DestinationKeyNameVariableName)) - destinationKeyName = executeContext.GetVariable(executeContext.Arguments.DestinationKeyNameVariableName); - - if (string.IsNullOrWhiteSpace(destinationKeyName)) - throw new InvalidOperationException("Transfer activity requires a destination key name from arguments or variables."); + ArgumentNullException.ThrowIfNull(destinationKeyName, nameof(destinationKeyName)); try { diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index da16d42..447290c 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -32,8 +32,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files executeContext.CancellationToken); var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariableName); - if (string.IsNullOrEmpty(sourceFilePath)) - throw new InvalidOperationException($"Upload activity requires variable '{executeContext.Arguments.FilePathVariableName}' with source file path."); + ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var fileName = Path.GetFileName(sourceFilePath); try diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 57e27d3..7c6a352 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -13,6 +13,8 @@ namespace Musify.Infrastructure.MassTransit.Activities { public const string ExecuteEndpointName = "generate-picture-workflow-paths"; + internal const string ResizedPictureFileExtension = ".webp"; + public async Task Execute(ExecuteContext executeContext) { var processId = await processTrackingStore.GetOrCreateProcessAsync( @@ -32,19 +34,16 @@ namespace Musify.Infrastructure.MassTransit.Activities try { var destinationFolderName = Guid.NewGuid().ToString(); - var originalFileName = Path.GetFileName(executeContext.Arguments.SourceKeyName); - var sourceFileExtension = Path.GetExtension(originalFileName); var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, destinationFolderName); - var sourceFileName = Guid.NewGuid().ToString() + sourceFileExtension; - var smallPictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; - var mediumPictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; - var largePictureFileName = Guid.NewGuid().ToString() + sourceFileExtension; - - var sourceFilePath = Path.Combine(workingDirectory, sourceFileName); - var smallPictureFilePath = Path.Combine(workingDirectory, smallPictureFileName); - var mediumPictureFilePath = Path.Combine(workingDirectory, mediumPictureFileName); - var largePictureFilePath = Path.Combine(workingDirectory, largePictureFileName); + var sourceFilePath = Path.Combine( + workingDirectory, Guid.NewGuid().ToString() + Path.GetExtension(executeContext.Arguments.SourceKeyName)); + 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); diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 461da5a..5c66884 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -33,10 +33,9 @@ namespace Musify.Infrastructure.MassTransit.Activities executeContext.CancellationToken); var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); + ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var destinationFilePath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); - - if (string.IsNullOrWhiteSpace(sourceFilePath) || string.IsNullOrWhiteSpace(destinationFilePath)) - throw new InvalidOperationException("Resize activity requires source and destination file path variables."); + ArgumentNullException.ThrowIfNull(destinationFilePath, nameof(destinationFilePath)); try { @@ -48,7 +47,7 @@ namespace Musify.Infrastructure.MassTransit.Activities await using var fileStream = File.OpenRead(sourceFilePath); - var resizedPicture = await pictureHandler.ResizePictureAsync( + var resizedPicture = await pictureHandler.ResizePictureAsWebpAsync( fileStream, executeContext.Arguments.Width, executeContext.Arguments.Height, diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index e8fd4e7..1672fac 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -31,6 +31,13 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); + var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariableName); + ArgumentNullException.ThrowIfNull(smallResizedVariable, nameof(smallResizedVariable)); + var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariableName); + ArgumentNullException.ThrowIfNull(mediumResizedVariable, nameof(mediumResizedVariable)); + var largeResizedVariable = executeContext.GetVariable(executeContext.Arguments.LargePictureVariableName); + ArgumentNullException.ThrowIfNull(largeResizedVariable, nameof(largeResizedVariable)); + try { var playList = await database.PlayLists.FindAsync( @@ -52,9 +59,9 @@ namespace Musify.Infrastructure.MassTransit.Activities playList.LargePictureKeyName); playList.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKeyName); - playList.SmallPictureKeyName = Path.GetFileName(executeContext.Arguments.SmallPictureKeyName); - playList.MediumPictureKeyName = Path.GetFileName(executeContext.Arguments.MediumPictureKeyName); - playList.LargePictureKeyName = Path.GetFileName(executeContext.Arguments.LargePictureKeyName); + playList.SmallPictureKeyName = Path.GetFileName(smallResizedVariable); + playList.MediumPictureKeyName = Path.GetFileName(mediumResizedVariable); + playList.LargePictureKeyName = Path.GetFileName(largeResizedVariable); database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 713a9bf..da099ca 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -31,6 +31,13 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); + var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariableName); + ArgumentNullException.ThrowIfNull(smallResizedVariable, nameof(smallResizedVariable)); + var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariableName); + ArgumentNullException.ThrowIfNull(mediumResizedVariable, nameof(mediumResizedVariable)); + var largeResizedVariable = executeContext.GetVariable(executeContext.Arguments.LargePictureVariableName); + ArgumentNullException.ThrowIfNull(largeResizedVariable, nameof(largeResizedVariable)); + try { var track = await database.Tracks.FindAsync( @@ -52,9 +59,9 @@ namespace Musify.Infrastructure.MassTransit.Activities track.LargePictureKeyName); track.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKeyName); - track.SmallPictureKeyName = Path.GetFileName(executeContext.Arguments.SmallPictureKeyName); - track.MediumPictureKeyName = Path.GetFileName(executeContext.Arguments.MediumPictureKeyName); - track.LargePictureKeyName = Path.GetFileName(executeContext.Arguments.LargePictureKeyName); + track.SmallPictureKeyName = Path.GetFileName(smallResizedVariable); + track.MediumPictureKeyName = Path.GetFileName(mediumResizedVariable); + track.LargePictureKeyName = Path.GetFileName(largeResizedVariable); database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs index f9fc4d2..853cebb 100644 --- a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs @@ -3,7 +3,7 @@ public record UpdatePlayListPictureArguments( Guid PlayListId, string OriginalPictureKeyName, - string SmallPictureKeyName, - string MediumPictureKeyName, - string LargePictureKeyName); + string SmallPictureVariableName, + string MediumPictureVariableName, + string LargePictureVariableName); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs index 08cc747..a0bcf27 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs @@ -3,7 +3,7 @@ public record UpdateTrackPictureArguments( Guid TrackId, string OriginalPictureKeyName, - string SmallPictureKeyName, - string MediumPictureKeyName, - string LargePictureKeyName); + string SmallPictureVariableName, + string MediumPictureVariableName, + string LargePictureVariableName); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 5ddc032..9b660e8 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -1,6 +1,5 @@ using MassTransit; using MassTransit.Courier.Contracts; -using MimeMapping; using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities; @@ -29,19 +28,15 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders message.LargePictureHeight, correlationId); - routingSlipBuilder.AddSubscription( - MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), - RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); - routingSlipBuilder.AddActivity( ActivityNames.UpdateTrackPicture, MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), new UpdateTrackPictureArguments( message.TrackId, message.SourceKeyName, - message.SmallPictureRoute, - message.MediumPictureRoute, - message.LargePictureRoute)); + RoutingSlipVariableNames.Picture.SmallResizedFilePath, + RoutingSlipVariableNames.Picture.MediumResizedFilePath, + RoutingSlipVariableNames.Picture.LargeResizedFilePath)); return routingSlipBuilder; } @@ -51,13 +46,13 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders var routingSlipBuilder = BuildPictureWorkflow( message.SourceBucketName, message.SourceKeyName, - message.SmallPictureKeyName, + message.SmallPictureRoute, message.SmallPictureWidth, message.SmallPictureHeight, - message.MediumPictureKeyName, + message.MediumPictureRoute, message.MediumPictureWidth, message.MediumPictureHeight, - message.LargePictureKeyName, + message.LargePictureRoute, message.LargePictureWidth, message.LargePictureHeight, correlationId); @@ -68,9 +63,9 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders new UpdatePlayListPictureArguments( message.PlayListId, message.SourceKeyName, - message.SmallPictureKeyName, - message.MediumPictureKeyName, - message.LargePictureKeyName)); + RoutingSlipVariableNames.Picture.SmallResizedFilePath, + RoutingSlipVariableNames.Picture.MediumResizedFilePath, + RoutingSlipVariableNames.Picture.LargeResizedFilePath)); return routingSlipBuilder; } @@ -91,6 +86,10 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + routingSlipBuilder.AddSubscription( + MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddActivity( diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs index f549121..8ffad05 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -22,10 +22,6 @@ namespace Musify.Infrastructure.MassTransit internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath"; internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath"; internal const string DestinationFolderName = "Picture.DestinationFolderName"; - - internal const string SmallPictureKeyName = "Picture.SmallPictureKeyName"; - internal const string MediumPictureKeyName = "Picture.MediumPictureKeyName"; - internal const string LargePictureKeyName = "Picture.LargePictureKeyName"; } } } diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index 1181fac..f5c3a12 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -9,11 +9,7 @@ namespace Musify.Infrastructure.Pictures public class PictureHandler(ILogger logger) : IPictureHandler { - public string FileExtension => "webp"; - - public string ContentType => "image/webp"; - - public async Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) + public async Task> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) { try { From dae911d3eb7160319ab450dde8f9eb238b13b6de Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 27 Mar 2026 23:07:05 +0100 Subject: [PATCH 096/172] Renames --- Infrastructure/Infrastructure.csproj | 1 - .../{MessagingHelper.cs => EndpointHelper.cs} | 2 +- ...ection.cs => MassTransitDependencyInjection.cs} | 4 ++-- .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 12 ++++++------ .../Builders/PictureWorkflowRoutingSlipBuilder.cs | 14 +++++++------- .../RemoveFileFromBucketRoutingSlipBuilder.cs | 4 ++-- 6 files changed, 18 insertions(+), 19 deletions(-) rename Infrastructure/MassTransit/Consumers/{MessagingHelper.cs => EndpointHelper.cs} (90%) rename Infrastructure/MassTransit/DependencyInjection/{MessagingDependencyInjection.cs => MassTransitDependencyInjection.cs} (98%) diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 85e8989..b2d0c01 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -43,7 +43,6 @@ - diff --git a/Infrastructure/MassTransit/Consumers/MessagingHelper.cs b/Infrastructure/MassTransit/Consumers/EndpointHelper.cs similarity index 90% rename from Infrastructure/MassTransit/Consumers/MessagingHelper.cs rename to Infrastructure/MassTransit/Consumers/EndpointHelper.cs index 914781a..47c8efd 100644 --- a/Infrastructure/MassTransit/Consumers/MessagingHelper.cs +++ b/Infrastructure/MassTransit/Consumers/EndpointHelper.cs @@ -1,6 +1,6 @@ namespace Musify.Infrastructure.MassTransit.Consumers { - internal static class MessagingHelper + internal static class EndpointHelper { internal static Uri BuildExecuteActivityUri(string endpointName) => new($"queue:{endpointName}_execute"); diff --git a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs similarity index 98% rename from Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs rename to Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 1406c5b..a4301db 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MessagingDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -14,7 +14,7 @@ using Musify.Infrastructure.MassTransit.Activities.Files; namespace Musify.Infrastructure.MassTransit { - public static class MessagingDependencyInjection + public static class MassTransitDependencyInjection { public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors) { @@ -183,7 +183,7 @@ namespace Musify.Infrastructure.MassTransit { busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => endpointConfigurator.ExecuteActivityHost( - MessagingHelper.BuildCompensateActivityUri(endpointName), + EndpointHelper.BuildCompensateActivityUri(endpointName), busRegistrationContext) ); diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 6a29463..6008d78 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -19,19 +19,19 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddSubscription( - MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); routingSlipBuilder.AddActivity( ActivityNames.GenerateAudioWorkflowPaths, - MessagingHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), new GenerateAudioWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKeyName)); routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( message.SourceBucketName, message.SourceKeyName, @@ -39,14 +39,14 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.TranscodeAudio, - MessagingHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), new TranscodeDashAudioArguments( RoutingSlipVariableNames.Audio.SourceFilePath, RoutingSlipVariableNames.Workflow.TemporalDirectory)); routingSlipBuilder.AddActivity( ActivityNames.TransferFiles, - MessagingHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), new TransferFilesToBucketArguments( message.DestinationBucketName, RoutingSlipVariableNames.Audio.TranscodedDirectory, @@ -54,7 +54,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UpdateTrackAudio, - MessagingHelper.BuildExecuteActivityUri(UpdateTrackAudioActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(UpdateTrackAudioActivity.ExecuteEndpointName), new UpdateTrackAudioArguments( message.TrackId, message.DestinationFolderKeyName)); diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 9b660e8..ce0cd27 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -30,7 +30,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UpdateTrackPicture, - MessagingHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), new UpdateTrackPictureArguments( message.TrackId, message.SourceKeyName, @@ -59,7 +59,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.UpdatePlayListPicture, - MessagingHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), new UpdatePlayListPictureArguments( message.PlayListId, message.SourceKeyName, @@ -87,21 +87,21 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); routingSlipBuilder.AddSubscription( - MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddActivity( ActivityNames.GeneratePictureWorkflowPaths, - MessagingHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), new GeneratePictureWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, sourceKeyName)); routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, - MessagingHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( bucketName, sourceKeyName, @@ -152,7 +152,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { routingSlipBuilder.AddActivity( resizeActivityName, - MessagingHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(ResizePictureActivity.ExecuteEndpointName), new ResizePictureLocalArguments( RoutingSlipVariableNames.Picture.OriginalFilePath, destinationFilePathVariableName, @@ -161,7 +161,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddActivity( uploadActivityName, - MessagingHelper.BuildExecuteActivityUri(UploadFileToBucketActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(UploadFileToBucketActivity.ExecuteEndpointName), new UploadFileToBucketArguments( destinationFilePathVariableName, bucketName, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs index 8336635..f7b60ed 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs @@ -16,12 +16,12 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddSubscription( - MessagingHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); routingSlipBuilder.AddActivity( ActivityNames.RemoveFile, - MessagingHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( message.BucketName, message.KeyName)); From 8e864421a80593e1f68064a0bbee138f77848b68 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 02:56:08 +0100 Subject: [PATCH 097/172] Fix --- .../MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 6ed7f2d..2aa571a 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -34,7 +34,7 @@ namespace Musify.Infrastructure.MassTransit.Activities var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariableName); - ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); + ArgumentNullException.ThrowIfNull(workingDirectory, nameof(sourceFilePath)); try { From 1b5c482ecf4a90763d16c63aebfd0bf14f78b9d5 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 02:56:18 +0100 Subject: [PATCH 098/172] Ops --- .../MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 2aa571a..075e795 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -34,7 +34,7 @@ namespace Musify.Infrastructure.MassTransit.Activities var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariableName); - ArgumentNullException.ThrowIfNull(workingDirectory, nameof(sourceFilePath)); + ArgumentNullException.ThrowIfNull(workingDirectory, nameof(workingDirectory)); try { From cd22a140f37ab220f5d10e963339bf3da54dc06c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 02:57:08 +0100 Subject: [PATCH 099/172] Changes --- .../Activities/Audio/TranscodeDashAudioActivity.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 075e795..667be55 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -71,7 +71,11 @@ namespace Musify.Infrastructure.MassTransit.Activities { try { - Directory.Delete(compensateContext.Log.WorkingDirectory, recursive: true); + if (Directory.Exists(compensateContext.Log.WorkingDirectory)) + Directory.Delete(compensateContext.Log.WorkingDirectory, recursive: true); + else + logger.LogWarning("Working directory {WorkingDirectory} does not exist during compensation", + compensateContext.Log.WorkingDirectory); return Task.FromResult(compensateContext.Compensated()); } catch (Exception exception) From 3132b139f725b3d35dad1a18ec4a1f1dfc86e9cd Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 03:27:31 +0100 Subject: [PATCH 100/172] Rename variables --- Application/Events/ImageSize.cs | 7 ++ Application/Events/RemoveFileEvent.cs | 4 +- .../Events/UpdatePlayListPictureEvent.cs | 16 +--- Application/Events/UpdateTrackAudioEvent.cs | 8 +- Application/Events/UpdateTrackPictureEvent.cs | 16 +--- .../Handlers/CreatePlayListCommandHandler.cs | 26 +++--- .../Handlers/UpdatePlayListCommandHandler.cs | 36 ++++---- .../Handler/CreateTrackCommandHandler.cs | 43 +++++---- .../GenerateAudioWorkflowPathsActivity.cs | 4 +- .../Audio/TranscodeDashAudioActivity.cs | 4 +- .../Audio/UpdateTrackAudioActivity.cs | 4 +- .../Files/DownloadFileFromBucketActivity.cs | 14 +-- .../Files/RemoveFileFromBucketActivity.cs | 12 +-- .../Files/TransferFilesToBucketActivity.cs | 23 +++-- .../Files/UploadFileToBucketActivity.cs | 18 ++-- .../GeneratePictureWorkflowPathsActivity.cs | 4 +- .../Pictures/ResizePictureActivity.cs | 4 +- .../Pictures/UpdatePlayListPictureActivity.cs | 16 ++-- .../Pictures/UpdateTrackPictureActivity.cs | 16 ++-- .../DownloadFileFromBucketArguments.cs | 6 +- .../GenerateAudioWorkflowPathsArguments.cs | 2 +- .../GeneratePictureWorkflowPathsArguments.cs | 2 +- .../RemoveFileFromBucketArguments.cs | 4 +- .../Arguments/ResizePictureLocalArguments.cs | 4 +- .../Arguments/TranscodeDashAudioArguments.cs | 4 +- .../TransferFilesToBucketArguments.cs | 7 +- .../UpdatePlayListPictureArguments.cs | 8 +- .../Arguments/UpdateTrackAudioArguments.cs | 2 +- .../Arguments/UpdateTrackPictureArguments.cs | 8 +- .../Arguments/UploadFileToBucketArguments.cs | 4 +- .../Consumers/RoutingSlipCleanUpConsumer.cs | 1 - .../MassTransitDependencyInjection.cs | 20 +--- .../Filters/ProcessTrackingConsumeFilter.cs | 2 +- .../Logs/DownloadFileFromBucketLog.cs | 4 +- .../Logs/TransferFilesToBucketLog.cs | 2 +- .../Logs/UpdatePlayListPictureLog.cs | 8 +- .../MassTransit/Logs/UpdateTrackAudioLog.cs | 5 +- .../MassTransit/Logs/UpdateTrackPictureLog.cs | 8 +- .../MassTransit/Logs/UploadFileToBucketLog.cs | 2 +- .../AudioWorkflowRoutingSlipBuilder.cs | 13 ++- .../PictureWorkflowRoutingSlipBuilder.cs | 91 +++++++------------ .../RemoveFileFromBucketRoutingSlipBuilder.cs | 5 +- 42 files changed, 226 insertions(+), 261 deletions(-) create mode 100644 Application/Events/ImageSize.cs diff --git a/Application/Events/ImageSize.cs b/Application/Events/ImageSize.cs new file mode 100644 index 0000000..1a1d107 --- /dev/null +++ b/Application/Events/ImageSize.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Events +{ + public record ImageSize( + string SavePath, + int Width, + int Height); +} \ No newline at end of file diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs index 76fc108..4378be5 100644 --- a/Application/Events/RemoveFileEvent.cs +++ b/Application/Events/RemoveFileEvent.cs @@ -1,6 +1,6 @@ namespace Musify.Application.Events { public record RemoveFileEvent( - string BucketName, - string KeyName); + string Bucket, + string Key); } \ No newline at end of file diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs index cdf89a9..3063a5e 100644 --- a/Application/Events/UpdatePlayListPictureEvent.cs +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -2,15 +2,9 @@ { public record UpdatePlayListPictureEvent( Guid PlayListId, - string SourceBucketName, - string SourceKeyName, - string SmallPictureRoute, - int SmallPictureWidth, - int SmallPictureHeight, - string MediumPictureRoute, - int MediumPictureWidth, - int MediumPictureHeight, - string LargePictureRoute, - int LargePictureWidth, - int LargePictureHeight); + string Bucket, + string SourceKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); } \ No newline at end of file diff --git a/Application/Events/UpdateTrackAudioEvent.cs b/Application/Events/UpdateTrackAudioEvent.cs index 6891d9a..30c7ede 100644 --- a/Application/Events/UpdateTrackAudioEvent.cs +++ b/Application/Events/UpdateTrackAudioEvent.cs @@ -2,8 +2,8 @@ { public record UpdateTrackAudioEvent( Guid TrackId, - string SourceBucketName, - string SourceKeyName, - string DestinationBucketName, - string DestinationFolderKeyName); + string SourceBucket, + string SourceKey, + string DestinationBucket, + string DestinationFolderKey); } \ No newline at end of file diff --git a/Application/Events/UpdateTrackPictureEvent.cs b/Application/Events/UpdateTrackPictureEvent.cs index b9ae6e7..2d8c80b 100644 --- a/Application/Events/UpdateTrackPictureEvent.cs +++ b/Application/Events/UpdateTrackPictureEvent.cs @@ -2,15 +2,9 @@ { public record UpdateTrackPictureEvent( Guid TrackId, - string SourceBucketName, - string SourceKeyName, - string SmallPictureRoute, - int SmallPictureWidth, - int SmallPictureHeight, - string MediumPictureRoute, - int MediumPictureWidth, - int MediumPictureHeight, - string LargePictureRoute, - int LargePictureWidth, - int LargePictureHeight); + string Bucket, + string SourceKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 9973b6e..15282d5 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -95,21 +95,25 @@ namespace Musify.Application.PlayLists.Handlers return uploadFileResult; } - async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) + async Task PublishUpdateEvent(string originalPictureKey, PlayList playList, CancellationToken cancellationToken) { await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.BucketName, - originalPictureKeyName, - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + originalPictureKey, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 4ab50f3..1e476f1 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -92,11 +92,11 @@ namespace Musify.Application.PlayLists.Handlers database.PlayLists.Update(playList); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKeyName}.", playList.Id, newImageStorageKey); + logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKey}.", playList.Id, newImageStorageKey); return Result.Success(PlayListResponse.FromEntity(playList)); } - async Task PublishRemoveFileEvent(string keyName, CancellationToken cancellationToken) + async Task PublishRemoveFileEvent(string key, CancellationToken cancellationToken) { try { @@ -104,14 +104,14 @@ namespace Musify.Application.PlayLists.Handlers storageConfiguration.BucketName, Path.Combine( playListConfiguration.Routes.ParentFolders, - keyName)), cancellationToken); + key)), cancellationToken); return Result.NoContent(); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for key {KeyName}.", keyName); - return Result.Error($"Failed to publish remove file event for key {keyName}."); + logger.LogError(exception, "Failed to publish remove file event for key {Key}.", key); + return Result.Error($"Failed to publish remove file event for key {key}."); } } @@ -127,21 +127,25 @@ namespace Musify.Application.PlayLists.Handlers return uploadFileResult; } - async Task PublishUpdateEvent(string originalPictureKeyName, PlayList playList, CancellationToken cancellationToken) + async Task PublishUpdateEvent(string originalPictureKey, PlayList playList, CancellationToken cancellationToken) { await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.BucketName, - originalPictureKeyName, - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + originalPictureKey, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index b7c8220..139e967 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -38,9 +38,9 @@ namespace Musify.Application.Tracks.Handler LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, }; - var originalPictureKeyName = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, + var originalPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, originalPictureFileName); - var pictureUploadResult = await UploadFile(request.Picture, originalPictureKeyName, cancellationToken); + var pictureUploadResult = await UploadFile(request.Picture, originalPictureKey, cancellationToken); if (!pictureUploadResult.IsSuccess) { logger.LogError("Failed to upload picture for track {TrackTitle}", request.Title); @@ -48,9 +48,9 @@ namespace Musify.Application.Tracks.Handler } - var audioKeyName = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, + var audioKey = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, Guid.NewGuid() + request.Audio.FileType); - var audioUploadResult = await UploadFile(request.Audio, audioKeyName, cancellationToken); + var audioUploadResult = await UploadFile(request.Audio, audioKey, cancellationToken); if (!audioUploadResult.IsSuccess) { logger.LogError("Failed to upload audio for track {TrackTitle}", request.Title); @@ -59,7 +59,7 @@ namespace Musify.Application.Tracks.Handler try { - await PublishUpdateEvent(originalPictureKeyName, track, cancellationToken); + await PublishUpdateEvent(originalPictureKey, track, cancellationToken); } catch (Exception exception) { @@ -83,36 +83,39 @@ namespace Musify.Application.Tracks.Handler return Result.Success(); } - async Task> UploadFile(IFileData fileData, string keyName, CancellationToken cancellationToken) + async Task> UploadFile(IFileData fileData, string key, CancellationToken cancellationToken) { var uploadFile = await storageHandler.UploadFileAsync( fileData.FileStream, fileData.ContentType, storageConfiguration.BucketName, - keyName, + key, cancellationToken); return uploadFile; } - async Task PublishUpdateEvent(string originalPictureKeyName, Track track, CancellationToken cancellationToken) + async Task PublishUpdateEvent(string originalPictureKey, Track track, CancellationToken cancellationToken) { await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, storageConfiguration.BucketName, - originalPictureKeyName, - trackConfiguration.Routes.SmallPicturesPath, - trackConfiguration.PicturesSizes.SmallPictureWidth, - trackConfiguration.PicturesSizes.SmallPictureHeight, - trackConfiguration.Routes.MediumPicturesPath, - trackConfiguration.PicturesSizes.MediumPictureWidth, - trackConfiguration.PicturesSizes.MediumPictureHeight, - trackConfiguration.Routes.LargePicturesPath, - trackConfiguration.PicturesSizes.LargePictureWidth, - trackConfiguration.PicturesSizes.LargePictureHeight), cancellationToken); + originalPictureKey, + new ImageSize( + trackConfiguration.Routes.PresetSmallPicture, + trackConfiguration.PicturesSizes.SmallPictureWidth, + trackConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + trackConfiguration.Routes.PresetMediumPicture, + trackConfiguration.PicturesSizes.MediumPictureWidth, + trackConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + trackConfiguration.Routes.PresetLargePicture, + trackConfiguration.PicturesSizes.LargePictureWidth, + trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); } - async Task PublishTranscodeEvent(Guid trackId, string audioKeyName, CancellationToken cancellationToken) + async Task PublishTranscodeEvent(Guid trackId, string audioKey, CancellationToken cancellationToken) { var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, Guid.NewGuid().ToString()); @@ -120,7 +123,7 @@ namespace Musify.Application.Tracks.Handler await eventBus.PublishAsync(new UpdateTrackAudioEvent( trackId, storageConfiguration.BucketName, - audioKeyName, + audioKey, storageConfiguration.BucketName, destinationFolderAudio), cancellationToken); } diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index ab57cc9..8699be2 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -32,7 +32,7 @@ namespace Musify.Infrastructure.MassTransit.Activities try { var folderName = Guid.NewGuid().ToString(); - var sourceFileName = Path.GetFileName(executeContext.Arguments.SourceKeyName); + var sourceFileName = Path.GetFileName(executeContext.Arguments.SourceKey); var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, folderName); var sourceFilePath = Path.Combine(workingDirectory, sourceFileName); @@ -54,7 +54,7 @@ namespace Musify.Infrastructure.MassTransit.Activities catch (Exception exception) { logger.LogError(exception, "Error generating audio workflow paths for key {SourceKeyName}", - executeContext.Arguments.SourceKeyName); + executeContext.Arguments.SourceKey); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 667be55..c7f60cb 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -31,9 +31,9 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); - var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); + var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); - var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariableName); + var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariable); ArgumentNullException.ThrowIfNull(workingDirectory, nameof(workingDirectory)); try diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index d77c60b..37ea040 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -48,7 +48,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio track.Id, track.AudioFolderKeyName); - track.AudioFolderKeyName = Path.GetFileName(executeContext.Arguments.AudioFolderKeyName); + track.AudioFolderKeyName = Path.GetFileName(executeContext.Arguments.AudioFolderKey); database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); @@ -81,7 +81,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio return compensateContext.Compensated(); } - track.AudioFolderKeyName = compensateContext.Log.PreviousFolderAudioKeyName; + track.AudioFolderKeyName = compensateContext.Log.PreviousFolderAudioKey; database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index faf68e1..61e8ec5 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -31,22 +31,22 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); - var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); + var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariable); ArgumentNullException.ThrowIfNull(destinationPath, nameof(destinationPath)); try { var getFile = await storageHandler.GetFileAsync( - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, + executeContext.Arguments.Bucket, + executeContext.Arguments.Key, executeContext.CancellationToken); if (!getFile.IsSuccess) { var errorMessage = string.Join("; ", getFile.Errors); logger.LogWarning("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, + executeContext.Arguments.Bucket, + executeContext.Arguments.Key, errorMessage); throw new Exception(errorMessage); } @@ -64,8 +64,8 @@ namespace Musify.Infrastructure.MassTransit.Activities await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(new DownloadFileFromBucketLog( - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, + executeContext.Arguments.Bucket, + executeContext.Arguments.Key, destinationPath)); } catch (Exception exception) diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 1a37762..73ef143 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -33,16 +33,16 @@ namespace Musify.Infrastructure.MassTransit.Activities try { var removeFile = await storageHandler.RemoveFileAsync( - executeContext.Arguments.BucketName, - executeContext.Arguments.KeyName, + executeContext.Arguments.Bucket, + executeContext.Arguments.Key, executeContext.CancellationToken); if (!removeFile.IsSuccess) { var errorMessage = string.Join("; ", removeFile.Errors); logger.LogWarning("Failed to remove file {KeyName} from bucket {BucketName}. Errors: {Errors}", - executeContext.Arguments.KeyName, - executeContext.Arguments.BucketName, + executeContext.Arguments.Key, + executeContext.Arguments.Bucket, errorMessage); } @@ -52,8 +52,8 @@ namespace Musify.Infrastructure.MassTransit.Activities catch (Exception exception) { logger.LogError(exception, "Error removing file {KeyName} from bucket {BucketName}", - executeContext.Arguments.KeyName, - executeContext.Arguments.BucketName); + executeContext.Arguments.Key, + executeContext.Arguments.Bucket); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 27c92ca..645c948 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -31,22 +31,21 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); - var folderPath = executeContext.GetVariable(executeContext.Arguments.SourceDirectoryVariableName); + var folderPath = executeContext.GetVariable(executeContext.Arguments.SourceDirectoryVariable); ArgumentNullException.ThrowIfNull(folderPath, nameof(folderPath)); - - var destinationKeyName = executeContext.Arguments.DestinationKeyName; - ArgumentNullException.ThrowIfNull(destinationKeyName, nameof(destinationKeyName)); + var destinationKey = executeContext.Arguments.DestinationKey; + ArgumentNullException.ThrowIfNull(destinationKey, nameof(destinationKey)); try { var uploadedKeys = Directory.GetFiles(folderPath) - .Select(file => Path.Combine(destinationKeyName, Path.GetFileName(file))) + .Select(file => Path.Combine(destinationKey, Path.GetFileName(file))) .ToArray(); var transferFilesResult = await storageHandler.TransferFilesAsync( folderPath, - executeContext.Arguments.DestinationBucketName, - destinationKeyName, + executeContext.Arguments.DestinationBucket, + destinationKey, executeContext.CancellationToken); if (!transferFilesResult.IsSuccess) @@ -54,8 +53,8 @@ namespace Musify.Infrastructure.MassTransit.Activities var errorMessage = string.Join("; ", transferFilesResult.Errors); logger.LogWarning("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", folderPath, - executeContext.Arguments.DestinationBucketName, - destinationKeyName, + executeContext.Arguments.DestinationBucket, + destinationKey, errorMessage); throw new Exception(errorMessage); } @@ -67,7 +66,7 @@ namespace Musify.Infrastructure.MassTransit.Activities { logger.LogError(exception, "Error transferring files from {FolderPath} to bucket {DestinationBucketName}", folderPath, - executeContext.Arguments.DestinationBucketName); + executeContext.Arguments.DestinationBucket); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } @@ -80,7 +79,7 @@ namespace Musify.Infrastructure.MassTransit.Activities foreach (var uploadedKey in compensateContext.Log.UploadedKeys) { await storageHandler.RemoveFileAsync( - compensateContext.Log.DestinationBucketName, + compensateContext.Log.DestinationBucket, uploadedKey, compensateContext.CancellationToken); } @@ -89,7 +88,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating transferred files to bucket {DestinationBucketName}", compensateContext.Log.DestinationBucketName); + logger.LogError(exception, "Error compensating transferred files to bucket {DestinationBucketName}", compensateContext.Log.DestinationBucket); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 447290c..d7d225b 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -31,7 +31,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files 0, executeContext.CancellationToken); - var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariableName); + var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var fileName = Path.GetFileName(sourceFilePath); @@ -39,7 +39,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files { var uploadResult = await storageHandler.UploadFileAsync( sourceFilePath, - executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationBucket, Path.Combine(executeContext.Arguments.DestinationRoute, fileName), executeContext.CancellationToken); @@ -47,7 +47,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files { var errors = string.Join("; ", uploadResult.Errors); logger.LogWarning("Failed to upload file to bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", - executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationBucket, executeContext.Arguments.DestinationRoute, errors); throw new Exception(errors); @@ -55,7 +55,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new UploadFileToBucketLog( - executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationBucket, executeContext.Arguments.DestinationRoute)); } catch (Exception exception) @@ -71,14 +71,14 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files try { var uploadResult = await storageHandler.RemoveFileAsync( - compensateContext.Log.DestinationBucketName, - compensateContext.Log.DestinationKeyName, + compensateContext.Log.DestinationBucket, + compensateContext.Log.DestinationKey, compensateContext.CancellationToken); if (!uploadResult.IsSuccess) { var errors = string.Join("; ", uploadResult.Errors); - var errorMessage = $"Failed to remove file from bucket during compensation. Bucket: {compensateContext.Log.DestinationBucketName}, Key: {compensateContext.Log.DestinationKeyName}, Errors: {errors}"; + var errorMessage = $"Failed to remove file from bucket during compensation. Bucket: {compensateContext.Log.DestinationBucket}, Key: {compensateContext.Log.DestinationKey}, Errors: {errors}"; logger.LogError(errorMessage); return compensateContext.Failed(new Exception(errorMessage)); @@ -88,8 +88,8 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files catch (Exception exception) { logger.LogError(exception, "An error occurred while compensating upload file to bucket activity for bucket {BucketName} and key {KeyName}", - compensateContext.Log.DestinationBucketName, - compensateContext.Log.DestinationKeyName); + compensateContext.Log.DestinationBucket, + compensateContext.Log.DestinationKey); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 7c6a352..867a83a 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -37,7 +37,7 @@ namespace Musify.Infrastructure.MassTransit.Activities var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, destinationFolderName); var sourceFilePath = Path.Combine( - workingDirectory, Guid.NewGuid().ToString() + Path.GetExtension(executeContext.Arguments.SourceKeyName)); + workingDirectory, Guid.NewGuid().ToString() + Path.GetExtension(executeContext.Arguments.SourceKey)); var smallPictureFilePath = Path.Combine( workingDirectory, Guid.NewGuid().ToString() + ResizedPictureFileExtension); var mediumPictureFilePath = Path.Combine( @@ -65,7 +65,7 @@ namespace Musify.Infrastructure.MassTransit.Activities catch (Exception exception) { logger.LogError(exception, "Error generating picture workflow paths for key {SourceKeyName}", - executeContext.Arguments.SourceKeyName); + executeContext.Arguments.SourceKey); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 5c66884..eea6a1f 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -32,9 +32,9 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); - var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariableName); + var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); - var destinationFilePath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariableName); + var destinationFilePath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariable); ArgumentNullException.ThrowIfNull(destinationFilePath, nameof(destinationFilePath)); try diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 1672fac..992bd5a 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -31,11 +31,11 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); - var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariableName); + var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariable); ArgumentNullException.ThrowIfNull(smallResizedVariable, nameof(smallResizedVariable)); - var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariableName); + var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariable); ArgumentNullException.ThrowIfNull(mediumResizedVariable, nameof(mediumResizedVariable)); - var largeResizedVariable = executeContext.GetVariable(executeContext.Arguments.LargePictureVariableName); + var largeResizedVariable = executeContext.GetVariable(executeContext.Arguments.LargePictureVariable); ArgumentNullException.ThrowIfNull(largeResizedVariable, nameof(largeResizedVariable)); try @@ -58,7 +58,7 @@ namespace Musify.Infrastructure.MassTransit.Activities playList.MediumPictureKeyName, playList.LargePictureKeyName); - playList.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKeyName); + playList.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); playList.SmallPictureKeyName = Path.GetFileName(smallResizedVariable); playList.MediumPictureKeyName = Path.GetFileName(mediumResizedVariable); playList.LargePictureKeyName = Path.GetFileName(largeResizedVariable); @@ -94,10 +94,10 @@ namespace Musify.Infrastructure.MassTransit.Activities return compensateContext.Compensated(); } - playList.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKeyName; - playList.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKeyName; - playList.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKeyName; - playList.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKeyName; + playList.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKey; + playList.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKey; + playList.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKey; + playList.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKey; database.PlayLists.Update(playList); await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index da099ca..fd68ed7 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -31,11 +31,11 @@ namespace Musify.Infrastructure.MassTransit.Activities 0, executeContext.CancellationToken); - var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariableName); + var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariable); ArgumentNullException.ThrowIfNull(smallResizedVariable, nameof(smallResizedVariable)); - var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariableName); + var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariable); ArgumentNullException.ThrowIfNull(mediumResizedVariable, nameof(mediumResizedVariable)); - var largeResizedVariable = executeContext.GetVariable(executeContext.Arguments.LargePictureVariableName); + var largeResizedVariable = executeContext.GetVariable(executeContext.Arguments.LargePictureVariable); ArgumentNullException.ThrowIfNull(largeResizedVariable, nameof(largeResizedVariable)); try @@ -58,7 +58,7 @@ namespace Musify.Infrastructure.MassTransit.Activities track.MediumPictureKeyName, track.LargePictureKeyName); - track.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKeyName); + track.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); track.SmallPictureKeyName = Path.GetFileName(smallResizedVariable); track.MediumPictureKeyName = Path.GetFileName(mediumResizedVariable); track.LargePictureKeyName = Path.GetFileName(largeResizedVariable); @@ -94,10 +94,10 @@ namespace Musify.Infrastructure.MassTransit.Activities return compensateContext.Compensated(); } - track.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKeyName; - track.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKeyName; - track.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKeyName; - track.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKeyName; + track.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKey; + track.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKey; + track.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKey; + track.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKey; database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs index 2d86e95..996306d 100644 --- a/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs @@ -1,7 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record DownloadFileFromBucketArguments( - string BucketName, - string KeyName, - string DestinationFilePathVariableName); + string Bucket, + string Key, + string DestinationFilePathVariable); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs index 8d866be..44e4f07 100644 --- a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs @@ -2,5 +2,5 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record GenerateAudioWorkflowPathsArguments( string TemporaryRootDirectory, - string SourceKeyName); + string SourceKey); } diff --git a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs index 2460135..e112a6d 100644 --- a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs @@ -2,5 +2,5 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record GeneratePictureWorkflowPathsArguments( string TemporaryRootDirectory, - string SourceKeyName); + string SourceKey); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs index 0f13017..ca2bb1c 100644 --- a/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs @@ -1,6 +1,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record RemoveFileFromBucketArguments( - string BucketName, - string KeyName); + string Bucket, + string Key); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs index a50c957..633d34e 100644 --- a/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs +++ b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs @@ -1,8 +1,8 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record ResizePictureLocalArguments( - string SourceFilePathVariableName, - string DestinationFilePathVariableName, + string SourceFilePathVariable, + string DestinationFilePathVariable, int Width, int Height); } diff --git a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs index e48d678..c445504 100644 --- a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs @@ -1,6 +1,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record TranscodeDashAudioArguments( - string SourceFilePathVariableName, - string WorkingDirectoryVariableName); + string SourceFilePathVariable, + string WorkingDirectoryVariable); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs index 7087961..d8b9313 100644 --- a/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs @@ -1,8 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record TransferFilesToBucketArguments( - string DestinationBucketName, - string SourceDirectoryVariableName, - string? DestinationKeyName = null, - string? DestinationKeyNameVariableName = null); + string DestinationBucket, + string SourceDirectoryVariable, + string DestinationKey); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs index 853cebb..d47d31f 100644 --- a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs @@ -2,8 +2,8 @@ { public record UpdatePlayListPictureArguments( Guid PlayListId, - string OriginalPictureKeyName, - string SmallPictureVariableName, - string MediumPictureVariableName, - string LargePictureVariableName); + string OriginalPictureKey, + string SmallPictureVariable, + string MediumPictureVariable, + string LargePictureVariable); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs index b3a741f..10edb2b 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs @@ -2,5 +2,5 @@ { public record UpdateTrackAudioArguments( Guid TrackId, - string AudioFolderKeyName); + string AudioFolderKey); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs index a0bcf27..005c260 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs @@ -2,8 +2,8 @@ { public record UpdateTrackPictureArguments( Guid TrackId, - string OriginalPictureKeyName, - string SmallPictureVariableName, - string MediumPictureVariableName, - string LargePictureVariableName); + string OriginalPictureKey, + string SmallPictureVariable, + string MediumPictureVariable, + string LargePictureVariable); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs index 9d01163..8fd35f8 100644 --- a/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UploadFileToBucketArguments.cs @@ -1,7 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Arguments { public record UploadFileToBucketArguments( - string FilePathVariableName, - string DestinationBucketName, + string FilePathVariable, + string DestinationBucket, string DestinationRoute); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs b/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs index bf78274..ece1cda 100644 --- a/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/RoutingSlipCleanUpConsumer.cs @@ -1,7 +1,6 @@ using MassTransit; using MassTransit.Courier.Contracts; using Microsoft.Extensions.Logging; -using Musify.Infrastructure.MassTransit; namespace Musify.Infrastructure.MassTransit.Consumers { diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index a4301db..2da7293 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -65,29 +65,19 @@ namespace Musify.Infrastructure.MassTransit busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); busFactoryConfigurator.ReceiveEndpoint(UpdateTrackAudioConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); busFactoryConfigurator.ReceiveEndpoint(RoutingSlipCleanUpConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); ConfigureExecuteActivityEndpoint( busFactoryConfigurator, diff --git a/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs b/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs index e0ae97c..5ac4a07 100644 --- a/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs +++ b/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs @@ -42,7 +42,7 @@ namespace Musify.Infrastructure.MassTransit.Filters public void Probe(ProbeContext context) { - context.CreateFilterScope(nameof(ProcessTrackingConsumeFilter)); + context.CreateFilterScope(nameof(ProcessTrackingConsumeFilter<>)); } } } diff --git a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs index 87e87fd..b46f9a5 100644 --- a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs @@ -1,7 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record DownloadFileFromBucketLog( - string BucketName, - string KeyName, + string Bucket, + string Key, string DestinationFilePath); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs index 85d4553..6eeee6c 100644 --- a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs @@ -1,4 +1,4 @@ namespace Musify.Infrastructure.MassTransit.Activities.Logs { - public record TransferFilesToBucketLog(string DestinationBucketName, string[] UploadedKeys); + public record TransferFilesToBucketLog(string DestinationBucket, string[] UploadedKeys); } diff --git a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs index da7482d..069442f 100644 --- a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs @@ -2,8 +2,8 @@ namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record UpdatePlayListPictureLog( Guid PlayListId, - string PreviousOriginalPictureKeyName, - string PreviousSmallPictureKeyName, - string PreviousMediumPictureKeyName, - string PreviousLargePictureKeyName); + string PreviousOriginalPictureKey, + string PreviousSmallPictureKey, + string PreviousMediumPictureKey, + string PreviousLargePictureKey); } diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs index b1a93b5..395670b 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs @@ -1,5 +1,6 @@ namespace Musify.Infrastructure.MassTransit.Logs { - public record UpdateTrackAudioLog(Guid TrackId, - string PreviousFolderAudioKeyName); + public record UpdateTrackAudioLog( + Guid TrackId, + string PreviousFolderAudioKey); } diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs index dda2cac..a760eb7 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs @@ -2,8 +2,8 @@ namespace Musify.Infrastructure.MassTransit.Activities.Logs { public record UpdateTrackPictureLog( Guid TrackId, - string PreviousOriginalPictureKeyName, - string PreviousSmallPictureKeyName, - string PreviousMediumPictureKeyName, - string PreviousLargePictureKeyName); + string PreviousOriginalPictureKey, + string PreviousSmallPictureKey, + string PreviousMediumPictureKey, + string PreviousLargePictureKey); } diff --git a/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs b/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs index 8dd9dd6..eac29f1 100644 --- a/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/UploadFileToBucketLog.cs @@ -1,4 +1,4 @@ namespace Musify.Infrastructure.MassTransit.Logs { - public record UploadFileToBucketLog(string DestinationBucketName, string DestinationKeyName); + public record UploadFileToBucketLog(string DestinationBucket, string DestinationKey); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 6008d78..4d96026 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -15,7 +15,6 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders public RoutingSlipBuilder Build(UpdateTrackAudioEvent message, Guid? correlationId) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddSubscription( @@ -27,14 +26,14 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders EndpointHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), new GenerateAudioWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, - message.SourceKeyName)); + message.SourceKey)); routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, EndpointHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( - message.SourceBucketName, - message.SourceKeyName, + message.SourceBucket, + message.SourceKey, RoutingSlipVariableNames.Audio.SourceFilePath)); routingSlipBuilder.AddActivity( @@ -48,16 +47,16 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders ActivityNames.TransferFiles, EndpointHelper.BuildExecuteActivityUri(TransferFilesToBucketActivity.ExecuteEndpointName), new TransferFilesToBucketArguments( - message.DestinationBucketName, + message.DestinationBucket, RoutingSlipVariableNames.Audio.TranscodedDirectory, - message.DestinationFolderKeyName)); + message.DestinationFolderKey)); routingSlipBuilder.AddActivity( ActivityNames.UpdateTrackAudio, EndpointHelper.BuildExecuteActivityUri(UpdateTrackAudioActivity.ExecuteEndpointName), new UpdateTrackAudioArguments( message.TrackId, - message.DestinationFolderKeyName)); + message.DestinationFolderKey)); return routingSlipBuilder; } diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index ce0cd27..ef80864 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -15,17 +15,11 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) { var routingSlipBuilder = BuildPictureWorkflow( - message.SourceBucketName, - message.SourceKeyName, - message.SmallPictureRoute, - message.SmallPictureWidth, - message.SmallPictureHeight, - message.MediumPictureRoute, - message.MediumPictureWidth, - message.MediumPictureHeight, - message.LargePictureRoute, - message.LargePictureWidth, - message.LargePictureHeight, + message.Bucket, + message.SourceKey, + message.Small, + message.Medium, + message.Large, correlationId); routingSlipBuilder.AddActivity( @@ -33,7 +27,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders EndpointHelper.BuildExecuteActivityUri(UpdateTrackPictureActivity.ExecuteEndpointName), new UpdateTrackPictureArguments( message.TrackId, - message.SourceKeyName, + message.SourceKey, RoutingSlipVariableNames.Picture.SmallResizedFilePath, RoutingSlipVariableNames.Picture.MediumResizedFilePath, RoutingSlipVariableNames.Picture.LargeResizedFilePath)); @@ -44,17 +38,11 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders public RoutingSlipBuilder Build(UpdatePlayListPictureEvent message, Guid? correlationId) { var routingSlipBuilder = BuildPictureWorkflow( - message.SourceBucketName, - message.SourceKeyName, - message.SmallPictureRoute, - message.SmallPictureWidth, - message.SmallPictureHeight, - message.MediumPictureRoute, - message.MediumPictureWidth, - message.MediumPictureHeight, - message.LargePictureRoute, - message.LargePictureWidth, - message.LargePictureHeight, + message.Bucket, + message.SourceKey, + message.Small, + message.Medium, + message.Large, correlationId); routingSlipBuilder.AddActivity( @@ -62,7 +50,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders EndpointHelper.BuildExecuteActivityUri(UpdatePlayListPictureActivity.ExecuteEndpointName), new UpdatePlayListPictureArguments( message.PlayListId, - message.SourceKeyName, + message.SourceKey, RoutingSlipVariableNames.Picture.SmallResizedFilePath, RoutingSlipVariableNames.Picture.MediumResizedFilePath, RoutingSlipVariableNames.Picture.LargeResizedFilePath)); @@ -71,71 +59,58 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders } RoutingSlipBuilder BuildPictureWorkflow( - string bucketName, - string sourceKeyName, - string destinationSmallPictureRoute, - int smallPictureWidth, - int smallPictureHeight, - string destinationMediumPictureRoute, - int mediumPictureWidth, - int mediumPictureHeight, - string destinationLargePictureRoute, - int largePictureWidth, - int largePictureHeight, + string bucket, + string sourceKey, + ImageSize small, + ImageSize medium, + ImageSize large, Guid? correlationId) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddSubscription( EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); - routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); - routingSlipBuilder.AddActivity( ActivityNames.GeneratePictureWorkflowPaths, EndpointHelper.BuildExecuteActivityUri(GeneratePictureWorkflowPathsActivity.ExecuteEndpointName), new GeneratePictureWorkflowPathsArguments( workerConfiguration.Routes.TemporaryFilesDirectory, - sourceKeyName)); + sourceKey)); routingSlipBuilder.AddActivity( ActivityNames.DownloadFile, EndpointHelper.BuildExecuteActivityUri(DownloadFileFromBucketActivity.ExecuteEndpointName), new DownloadFileFromBucketArguments( - bucketName, - sourceKeyName, + bucket, + sourceKey, RoutingSlipVariableNames.Picture.OriginalFilePath)); AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeSmall, ActivityNames.UploadSmall, - bucketName, + bucket, RoutingSlipVariableNames.Picture.SmallResizedFilePath, - destinationSmallPictureRoute, - smallPictureWidth, - smallPictureHeight); + small); AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeMedium, ActivityNames.ResizeMedium, - bucketName, + bucket, RoutingSlipVariableNames.Picture.MediumResizedFilePath, - destinationMediumPictureRoute, - mediumPictureWidth, - mediumPictureHeight); + medium); AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeLarge, ActivityNames.UploadLarge, - bucketName, + bucket, RoutingSlipVariableNames.Picture.LargeResizedFilePath, - destinationLargePictureRoute, - largePictureWidth, - largePictureHeight); + large); return routingSlipBuilder; } @@ -146,9 +121,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders string uploadActivityName, string bucketName, string destinationFilePathVariableName, - string destinationBucketRoute, - int width, - int height) + ImageSize imageSize) { routingSlipBuilder.AddActivity( resizeActivityName, @@ -156,8 +129,8 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders new ResizePictureLocalArguments( RoutingSlipVariableNames.Picture.OriginalFilePath, destinationFilePathVariableName, - width, - height)); + imageSize.Width, + imageSize.Height)); routingSlipBuilder.AddActivity( uploadActivityName, @@ -165,7 +138,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders new UploadFileToBucketArguments( destinationFilePathVariableName, bucketName, - destinationBucketRoute)); + imageSize.SavePath)); } } -} +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs index f7b60ed..c6cb98d 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs @@ -12,7 +12,6 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders public RoutingSlipBuilder Build(RemoveFileEvent message, Guid? correlationId) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); - routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); routingSlipBuilder.AddSubscription( @@ -23,8 +22,8 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders ActivityNames.RemoveFile, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( - message.BucketName, - message.KeyName)); + message.Bucket, + message.Key)); return routingSlipBuilder; } From 7709fe737f3239de5506e6135fbe0b7575dd83ce Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 15:32:36 +0100 Subject: [PATCH 101/172] Renames and changes --- .../Infrastructure/IStorageHandler.cs | 14 +-- .../Handlers/CreatePlayListCommandHandler.cs | 12 +-- .../Handlers/DeletePlayListCommandHandler.cs | 20 ++-- .../Handlers/UpdatePlayListCommandHandler.cs | 24 ++--- .../PlayLists/Responses/PlayListResponse.cs | 6 +- .../Handler/CreateTrackCommandHandler.cs | 16 ++-- .../Handlers/CreateUserCommandHandler.cs | 2 - Domain/Entities/PlayList.cs | 10 +- Domain/Entities/Track.cs | 12 +-- Domain/Entities/Upload.cs | 4 +- Infrastructure/Audio/AudioTranscoder.cs | 34 +++---- .../Identity/KeycloakUserService.cs | 4 +- .../GenerateAudioWorkflowPathsActivity.cs | 4 +- .../Audio/TranscodeDashAudioActivity.cs | 8 +- .../Audio/UpdateTrackAudioActivity.cs | 14 +-- .../Files/DownloadFileFromBucketActivity.cs | 4 +- .../Files/RemoveFileFromBucketActivity.cs | 4 +- .../Files/TransferFilesToBucketActivity.cs | 6 +- .../Files/UploadFileToBucketActivity.cs | 11 +-- .../GeneratePictureWorkflowPathsActivity.cs | 4 +- .../Pictures/ResizePictureActivity.cs | 14 ++- .../Pictures/UpdatePlayListPictureActivity.cs | 32 +++---- .../Pictures/UpdateTrackPictureActivity.cs | 32 +++---- ...20260328141805_FirstMigration.Designer.cs} | 30 +++--- ...on.cs => 20260328141805_FirstMigration.cs} | 26 ++--- .../Migrations/DatabaseModelSnapshot.cs | 28 +++--- Infrastructure/Pictures/PictureHandler.cs | 6 +- Infrastructure/Storage/StorageHandler.cs | 96 +++++++++---------- 28 files changed, 236 insertions(+), 241 deletions(-) rename Infrastructure/Persistence/Migrations/{20260325003221_FirstMigration.Designer.cs => 20260328141805_FirstMigration.Designer.cs} (93%) rename Infrastructure/Persistence/Migrations/{20260325003221_FirstMigration.cs => 20260328141805_FirstMigration.cs} (88%) diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs index 3056fab..cb5d2df 100644 --- a/Application/Contracts/Infrastructure/IStorageHandler.cs +++ b/Application/Contracts/Infrastructure/IStorageHandler.cs @@ -4,19 +4,19 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IStorageHandler { - Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken); + Task> GetFileAsync(string bucket, string key, CancellationToken cancellationToken); - Task GetUrlAsync(string bucketName, string keyName, TimeSpan ExpirationTime, CancellationToken cancellationToken); + Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); - Task> UploadFileAsync(string filePath, string bucketName, string keyName, CancellationToken cancellationToken); + Task> UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken); - Task> UploadFileAsync(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken); + Task> UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken); - Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); + Task TransferFilesAsync(string sourceDirectory, string bucket, string route, CancellationToken cancellationToken); - Task RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken); + Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken); - Task CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName, + Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 15282d5..1bdd67d 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -32,10 +32,10 @@ namespace Musify.Application.PlayLists.Handlers Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, - OriginalPictureKeyName = playListConfiguration.Routes.PresetOriginalPicture, - SmallPictureKeyName = playListConfiguration.Routes.PresetSmallPicture, - MediumPictureKeyName = playListConfiguration.Routes.PresetMediumPicture, - LargePictureKeyName = playListConfiguration.Routes.PresetLargePicture, + OriginalPictureName = playListConfiguration.Routes.PresetOriginalPicture, + SmallPictureName = playListConfiguration.Routes.PresetSmallPicture, + MediumPictureName = playListConfiguration.Routes.PresetMediumPicture, + LargePictureName = playListConfiguration.Routes.PresetLargePicture, }; var setPictureResult = request.Picture is null @@ -63,12 +63,12 @@ namespace Musify.Application.PlayLists.Handlers if (!uploadResult.IsSuccess) { - logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error: {ErrorMessage}", + 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."); } - playList.OriginalPictureKeyName = pictureName; + playList.OriginalPictureName = pictureName; logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); try diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 8f7838c..1151b5e 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -30,15 +30,15 @@ namespace Musify.Application.PlayLists.Handlers var keysToRemove = new List(); - if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) keysToRemove.Add(Path.Combine(playListConfiguration.Routes.SmallPicturesPath, - playList.SmallPictureKeyName)); - if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + playList.SmallPictureName)); + if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) keysToRemove.Add(Path.Combine(playListConfiguration.Routes.MediumPicturesPath, - playList.MediumPictureKeyName)); - if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + playList.MediumPictureName)); + if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) keysToRemove.Add(Path.Combine(playListConfiguration.Routes.LargePicturesPath, - playList.LargePictureKeyName)); + playList.LargePictureName)); foreach (var key in keysToRemove) { @@ -48,7 +48,7 @@ namespace Musify.Application.PlayLists.Handlers { if (publishResult.IsNotFound()) { - logger.LogWarning("File with key={KeyName} not found in storage while trying to publish remove file event for PlayList with id={PlayListId}.", key, playList.Id); + logger.LogWarning("File with key={Key} not found in storage while trying to publish remove file event for PlayList with id={PlayListId}.", key, playList.Id); continue; } @@ -62,19 +62,19 @@ namespace Musify.Application.PlayLists.Handlers return Result.NoContent(); } - async Task TryPublishRemoveFileEventAsync(Guid playListId, string keyName, CancellationToken cancellationToken) + async Task TryPublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) { try { await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, - keyName), cancellationToken); + key), cancellationToken); return Result.Success(); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for PlayList id={PlayListId} and key={KeyName}.", playListId, keyName); + logger.LogError(exception, "Failed to publish remove file event for PlayList id={PlayListId} and key={Key}.", playListId, key); return Result.Error($"Failed to publish remove file event for PlayList with id {playListId}."); } } diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 1e476f1..0647ed9 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -23,13 +23,13 @@ namespace Musify.Application.PlayLists.Handlers if (playList is null) { - logger.LogInformation("PlayList with id {PlayListId} not found", request.PlayListId); + logger.LogInformation("PlayList with id={PlayListId} not found", request.PlayListId); return Result.NotFound(); } if (playList.UserId != request.UserId) { - logger.LogWarning("User with id {UserId} is not the owner of the PlayList with id {PlayListId}", request.UserId, request.PlayListId); + logger.LogWarning("User with id={UserId} is not the owner of the PlayList with id={PlayListId}", request.UserId, request.PlayListId); return Result.Unauthorized(); } @@ -51,20 +51,20 @@ namespace Musify.Application.PlayLists.Handlers return Result.Success(PlayListResponse.FromEntity(playList)); } - if (playList.SmallPictureKeyName != playListConfiguration.Routes.PresetSmallPicture) + if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) await PublishRemoveFileEvent(Path.Combine( playListConfiguration.Routes.SmallPicturesPath, - playList.SmallPictureKeyName), + playList.SmallPictureName), cancellationToken); - if (playList.MediumPictureKeyName != playListConfiguration.Routes.PresetMediumPicture) + if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) await PublishRemoveFileEvent(Path.Combine( playListConfiguration.Routes.MediumPicturesPath, - playList.MediumPictureKeyName), + playList.MediumPictureName), cancellationToken); - if (playList.LargePictureKeyName != playListConfiguration.Routes.PresetLargePicture) + if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) await PublishRemoveFileEvent(Path.Combine( playListConfiguration.Routes.LargePicturesPath, - playList.LargePictureKeyName), + playList.LargePictureName), cancellationToken); var newImageId = Guid.NewGuid(); @@ -74,7 +74,7 @@ namespace Musify.Application.PlayLists.Handlers var uploadResult = await UploadFile(request.NewPicture, newImageStorageKey, cancellationToken); if (!uploadResult.IsSuccess) { - logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error: {ErrorMessage}", + 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."); } @@ -85,14 +85,14 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish update picture event for PlayList with id {PlayListId}.", playList.Id); + logger.LogError(exception, "Failed to publish update picture event for PlayList with id={PlayListId}.", playList.Id); return Result.Error($"Failed to publish update picture event for PlayList with id {playList.Id}."); } database.PlayLists.Update(playList); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("Updated PlayList with id={PlayListId} and picture key {PictureKey}.", playList.Id, newImageStorageKey); + logger.LogInformation("Updated PlayList with id={PlayListId} and picture key={PictureKey}.", playList.Id, newImageStorageKey); return Result.Success(PlayListResponse.FromEntity(playList)); } @@ -110,7 +110,7 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for key {Key}.", key); + logger.LogError(exception, "Failed to publish remove file event for key={Key}.", key); return Result.Error($"Failed to publish remove file event for key {key}."); } } diff --git a/Application/PlayLists/Responses/PlayListResponse.cs b/Application/PlayLists/Responses/PlayListResponse.cs index d2888fc..6ebebaf 100644 --- a/Application/PlayLists/Responses/PlayListResponse.cs +++ b/Application/PlayLists/Responses/PlayListResponse.cs @@ -18,9 +18,9 @@ namespace Musify.Application.PlayLists.Responses playList.Id, playList.Name, playList.Description, - playList.SmallPictureKeyName, - playList.MediumPictureKeyName, - playList.LargePictureKeyName, + playList.SmallPictureName, + playList.MediumPictureName, + playList.LargePictureName, playList.CreatedDate, playList.UpdatedDate); } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 139e967..423ff4d 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -21,7 +21,7 @@ namespace Musify.Application.Tracks.Handler if (userExists is null) { - logger.LogWarning("User with id {UserId} not found", request.UserId); + logger.LogWarning("User with id={UserId} not found", request.UserId); return Result.NotFound($"User with id {request.UserId} not found"); } @@ -31,11 +31,11 @@ namespace Musify.Application.Tracks.Handler { Title = request.Title, NormalizedTitle = request.Title.Trim().ToUpperInvariant(), - OriginalPictureKeyName = originalPictureFileName, - OriginalAudioKeyName = originalAudioFileName, - SmallPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, - MediumPictureKeyName = trackConfiguration.Routes.PresetMediumPicture, - LargePictureKeyName = trackConfiguration.Routes.PresetLargePicture, + OriginalPictureName = originalPictureFileName, + OriginalAudioName = originalAudioFileName, + SmallPictureName = trackConfiguration.Routes.PresetMediumPicture, + MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, + LargePictureName = trackConfiguration.Routes.PresetLargePicture, }; var originalPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, @@ -43,7 +43,7 @@ namespace Musify.Application.Tracks.Handler var pictureUploadResult = await UploadFile(request.Picture, originalPictureKey, cancellationToken); if (!pictureUploadResult.IsSuccess) { - logger.LogError("Failed to upload picture for track {TrackTitle}", request.Title); + logger.LogError("Failed to upload picture for track={TrackTitle}", request.Title); return Result.Error($"Failed to upload picture for track {request.Title}"); } @@ -53,7 +53,7 @@ namespace Musify.Application.Tracks.Handler var audioUploadResult = await UploadFile(request.Audio, audioKey, cancellationToken); if (!audioUploadResult.IsSuccess) { - logger.LogError("Failed to upload audio for track {TrackTitle}", request.Title); + logger.LogError("Failed to upload audio for track={TrackTitle}", request.Title); return Result.Error($"Failed to upload audio for track {request.Title}"); } diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index 64ab778..a7debf7 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -14,8 +14,6 @@ namespace Musify.Application.Users.Handlers { public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) { - logger.LogDebug("Handling {CommandName} with Id: {UserId}", nameof(CreateUserCommand), request.Id); - var existingUser = await database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); if (existingUser is not null) diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 876854e..3d73d19 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -22,24 +22,24 @@ namespace Musify.Domain.Entities public required string NormalizedName { get; set; } [Required] - [MaxLength(254)] + [MaxLength(256)] public required string Description { get; set; } = "No description was provided."; [Required] [MaxLength(64)] - public required string OriginalPictureKeyName { get; set; } + public required string OriginalPictureName { get; set; } [Required] [MaxLength(64)] - public required string SmallPictureKeyName { get; set; } + public required string SmallPictureName { get; set; } [Required] [MaxLength(64)] - public required string MediumPictureKeyName { get; set; } + public required string MediumPictureName { get; set; } [Required] [MaxLength(64)] - public required string LargePictureKeyName { get; set; } + public required string LargePictureName { get; set; } [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index ba4f802..b1f9592 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -18,26 +18,26 @@ namespace Musify.Domain.Entities [Required] [MaxLength(64)] - public required string OriginalPictureKeyName { get; set; } + public required string OriginalPictureName { get; set; } [Required] [MaxLength(64)] - public required string SmallPictureKeyName { get; set; } + public required string SmallPictureName { get; set; } [Required] [MaxLength(64)] - public required string MediumPictureKeyName { get; set; } + public required string MediumPictureName { get; set; } [Required] [MaxLength(64)] - public required string LargePictureKeyName { get; set; } + public required string LargePictureName { get; set; } [Required] [MaxLength(64)] - public required string OriginalAudioKeyName { get; set; } + public required string OriginalAudioName { get; set; } [MaxLength(64)] - public string AudioFolderKeyName { get; set; } = string.Empty; + public string AudioFolderName { get; set; } = string.Empty; [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; diff --git a/Domain/Entities/Upload.cs b/Domain/Entities/Upload.cs index a74db25..9e8c42b 100644 --- a/Domain/Entities/Upload.cs +++ b/Domain/Entities/Upload.cs @@ -10,10 +10,10 @@ namespace Musify.Domain.Entities public Guid Id { get; set; } = Guid.NewGuid(); [Required] - public required string KeyName { get; set; } + public required string Bucket { get; set; } [Required] - public required string BucketName { get; set; } + public required string Key { get; set; } [Required] public required string ContentType { get; set; } diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Audio/AudioTranscoder.cs index 140c88b..cfe5212 100644 --- a/Infrastructure/Audio/AudioTranscoder.cs +++ b/Infrastructure/Audio/AudioTranscoder.cs @@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Audio Directory.CreateDirectory(destinationPath); logger.LogInformation( - "Starting audio transcoding to DASH. DestinationPath: {DestinationPath}, Timeout: {Timeout}, Executable: {Executable}", + "Starting audio transcoding to DASH. DestinationPath={DestinationPath}, Timeout={Timeout}, Executable={Executable}", destinationPath, audioTranscoderConfiguration.TranscodingTimeout, audioTranscoderConfiguration.Ffmpeg.ExecutableName); @@ -33,24 +33,24 @@ namespace Musify.Infrastructure.Audio if (executionResult.ExitCode != 0) { logger.LogError( - "Audio transcoding to DASH format failed with exit code {ExitCode}. Standard Output: {StandardOutput}, Standard Error: {StandardError}", + "Audio transcoding to DASH format failed with exit code={ExitCode}. Standard Output={StandardOutput}, Standard Error={StandardError}", executionResult.ExitCode, executionResult.StandardOutput, executionResult.StandardError); - return Result.Error($"Audio transcoding failed with exit code {executionResult.ExitCode}. See logs for details."); + return Result.Error($"Audio transcoding failed with exit code: {executionResult.ExitCode}. See logs for details."); } - logger.LogInformation("Audio transcoding to DASH format completed successfully. DestinationPath: {DestinationPath}", destinationPath); + logger.LogInformation("Audio transcoding to DASH format completed successfully. DestinationPath={DestinationPath}", destinationPath); return Result.Success(); } catch (TimeoutException timeoutException) { - logger.LogError(timeoutException, "Audio transcoding timed out after {Timeout}. DestinationPath: {DestinationPath}", audioTranscoderConfiguration.TranscodingTimeout, destinationPath); + logger.LogError(timeoutException, "Audio transcoding timed out after={Timeout}. DestinationPath={DestinationPath}", audioTranscoderConfiguration.TranscodingTimeout, destinationPath); return Result.Error("Audio transcoding timed out."); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while transcoding audio to DASH format. DestinationPath: {DestinationPath}", destinationPath); + logger.LogError(exception, "An error occurred while transcoding audio to DASH format. DestinationPath={DestinationPath}", destinationPath); return Result.Error(exception.Message); } } @@ -59,13 +59,13 @@ namespace Musify.Infrastructure.Audio { if (!File.Exists(filePath)) { - logger.LogWarning("Audio validation requested for a non-existing file. FilePath: {FilePath}", filePath); + logger.LogWarning("Audio validation requested for a non-existing file. FilePath={FilePath}", filePath); return Result.Error($"Audio file not found: {filePath}"); } try { - logger.LogInformation("Starting audio file validation. FilePath: {FilePath}, Timeout: {Timeout}", filePath, audioTranscoderConfiguration.TranscodingTimeout); + logger.LogInformation("Starting audio file validation. FilePath={FilePath}, Timeout={Timeout}", filePath, audioTranscoderConfiguration.TranscodingTimeout); var executionResult = await ExecuteFfmpegAsync( BuildValidateAudioArguments(filePath), @@ -77,25 +77,25 @@ namespace Musify.Infrastructure.Audio if (!isValid) { logger.LogWarning( - "Audio validation failed for file {FilePath}. Standard Error: {StandardError}", + "Audio validation failed for file={FilePath}. Standard Error={StandardError}", filePath, executionResult.StandardError); } else { - logger.LogInformation("Audio validation completed successfully. FilePath: {FilePath}", filePath); + logger.LogInformation("Audio validation completed successfully. FilePath={FilePath}", filePath); } return Result.Success(isValid); } catch (TimeoutException timeoutException) { - logger.LogError(timeoutException, "Audio validation timed out after {Timeout} for file {FilePath}.", audioTranscoderConfiguration.TranscodingTimeout, filePath); + logger.LogError(timeoutException, "Audio validation timed out after={Timeout} for file={FilePath}.", audioTranscoderConfiguration.TranscodingTimeout, filePath); return Result.Error("Audio validation timed out."); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while validating audio file {FilePath}.", filePath); + logger.LogError(exception, "An error occurred while validating audio file={FilePath}.", filePath); return Result.Error(exception.Message); } } @@ -110,7 +110,7 @@ namespace Musify.Infrastructure.Audio using var process = BuildProcess(arguments, workingDirectory, redirectStandardInput: true); process.Start(); - logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId: {ProcessId}, WorkingDirectory: {WorkingDirectory}", operationName, process.Id, workingDirectory); + logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId={ProcessId}, WorkingDirectory={WorkingDirectory}", operationName, process.Id, workingDirectory); if (inputStream.CanSeek) inputStream.Position = 0; @@ -130,7 +130,7 @@ namespace Musify.Infrastructure.Audio using var process = BuildProcess(arguments, workingDirectory, redirectStandardInput: false); process.Start(); - logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId: {ProcessId}, WorkingDirectory: {WorkingDirectory}", operationName, process.Id, workingDirectory); + logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId={ProcessId}, WorkingDirectory={WorkingDirectory}", operationName, process.Id, workingDirectory); return await WaitForProcessResultAsync(process, operationName, cancellationToken); } @@ -169,7 +169,7 @@ namespace Musify.Infrastructure.Audio catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { logger.LogError( - "[{Operation}] ffmpeg timed out after {Timeout}. ProcessId: {ProcessId}", + "[{Operation}] ffmpeg timed out after={Timeout}. ProcessId={ProcessId}", operationName, audioTranscoderConfiguration.TranscodingTimeout, process.Id); @@ -182,7 +182,7 @@ namespace Musify.Infrastructure.Audio var standardOutput = await outputTask; logger.LogDebug( - "[{Operation}] ffmpeg finished. ProcessId: {ProcessId}, ExitCode: {ExitCode}, StdOutLength: {StdOutLength}, StdErrLength: {StdErrLength}", + "[{Operation}] ffmpeg finished. ProcessId={ProcessId}, ExitCode={ExitCode}, StdOutLength={StdOutLength}, StdErrLength={StdErrLength}", operationName, process.Id, process.ExitCode, @@ -201,7 +201,7 @@ namespace Musify.Infrastructure.Audio } catch (Exception exception) { - logger.LogError(exception, "Failed to kill ffmpeg process. ProcessId: {ProcessId}", process.Id); + logger.LogError(exception, "Failed to kill ffmpeg process. ProcessId={ProcessId}", process.Id); } } diff --git a/Infrastructure/Identity/KeycloakUserService.cs b/Infrastructure/Identity/KeycloakUserService.cs index 16ea1fa..e725d1e 100644 --- a/Infrastructure/Identity/KeycloakUserService.cs +++ b/Infrastructure/Identity/KeycloakUserService.cs @@ -23,12 +23,12 @@ namespace Musify.Infrastructure.Identity } catch (OperationCanceledException) { - logger.LogError("Operation to retrieve Keycloak user with id {KeycloakId} was cancelled.", keycloakId); + logger.LogError("Operation to retrieve Keycloak user with id={KeycloakId} was cancelled.", keycloakId); return Result.Error("Operation was cancelled."); } catch (Exception exception) { - logger.LogWarning(exception, "Keycloak user with id {KeycloakId} could not be retrieved.", keycloakId); + logger.LogWarning(exception, "Keycloak user with id={KeycloakId} could not be retrieved.", keycloakId); return Result.NotFound(); } return Result.Success(KeycloakUserMapper.Map(keycloakUser)); diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 8699be2..a006bfd 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -38,7 +38,7 @@ namespace Musify.Infrastructure.MassTransit.Activities Directory.CreateDirectory(workingDirectory); - logger.LogDebug("Generated audio workflow paths. WorkingDirectory: {WorkingDirectory}, SourceFilePath: {SourceFilePath}", + logger.LogDebug("Generated audio workflow paths. WorkingDirectory={WorkingDirectory}, SourceFilePath={SourceFilePath}", workingDirectory, sourceFilePath); @@ -53,7 +53,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error generating audio workflow paths for key {SourceKeyName}", + logger.LogError(exception, "Error generating audio workflow paths for key={SourceKey}", executeContext.Arguments.SourceKey); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index c7f60cb..9629e4b 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -48,7 +48,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!result.IsSuccess) { var errorMessage = string.Join("; ", result.Errors); - logger.LogError("Transcoding failed for file {SourceFilePath}. Errors: {ErrorMessage}", + logger.LogError("Transcoding failed for file={SourceFilePath}. Errors={ErrorMessage}", sourceFilePath, errorMessage); throw new Exception(errorMessage); } @@ -60,7 +60,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error during transcoding activity for file {SourceFilePath}", + logger.LogError(exception, "Error during transcoding activity for file={SourceFilePath}", sourceFilePath); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -74,13 +74,13 @@ namespace Musify.Infrastructure.MassTransit.Activities if (Directory.Exists(compensateContext.Log.WorkingDirectory)) Directory.Delete(compensateContext.Log.WorkingDirectory, recursive: true); else - logger.LogWarning("Working directory {WorkingDirectory} does not exist during compensation", + logger.LogWarning("Working directory={WorkingDirectory} does not exist during compensation", compensateContext.Log.WorkingDirectory); return Task.FromResult(compensateContext.Compensated()); } catch (Exception exception) { - logger.LogError(exception, "Error compensating transcoded audio directory {WorkingDirectory}", compensateContext.Log.WorkingDirectory); + logger.LogError(exception, "Error compensating transcoded audio directory={WorkingDirectory}", compensateContext.Log.WorkingDirectory); return Task.FromResult(compensateContext.Failed(exception)); } } diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 37ea040..9381b48 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -39,21 +39,21 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio if (track is null) { - logger.LogWarning("Track with id {TrackId} not found", + logger.LogWarning("Track with id={TrackId} not found", executeContext.Arguments.TrackId); throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); } var log = new UpdateTrackAudioLog( track.Id, - track.AudioFolderKeyName); + track.AudioFolderName); - track.AudioFolderKeyName = Path.GetFileName(executeContext.Arguments.AudioFolderKey); + track.AudioFolderName = Path.GetFileName(executeContext.Arguments.AudioFolderKey); database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated Track {TrackId} audio", + logger.LogInformation("Updated Track={TrackId} audio", executeContext.Arguments.TrackId); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); @@ -61,7 +61,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio } catch (Exception exception) { - logger.LogError(exception, "Error updating Track {TrackId} audio", + logger.LogError(exception, "Error updating Track={TrackId} audio", executeContext.Arguments.TrackId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -81,7 +81,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio return compensateContext.Compensated(); } - track.AudioFolderKeyName = compensateContext.Log.PreviousFolderAudioKey; + track.AudioFolderName = compensateContext.Log.PreviousFolderAudioKey; database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); @@ -90,7 +90,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio } catch (Exception exception) { - logger.LogError(exception, "Error compensating Track {TrackId} audio", compensateContext.Log.TrackId); + logger.LogError(exception, "Error compensating Track={TrackId} audio", compensateContext.Log.TrackId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 61e8ec5..62172f4 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -44,7 +44,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!getFile.IsSuccess) { var errorMessage = string.Join("; ", getFile.Errors); - logger.LogWarning("Failed to download file from bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", + logger.LogWarning("Failed to download file from bucket. Bucket={Bucket}, Key={Key}, Errors={Errors}", executeContext.Arguments.Bucket, executeContext.Arguments.Key, errorMessage); @@ -85,7 +85,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating downloaded file {DestinationFilePath}", compensateContext.Log.DestinationFilePath); + logger.LogError(exception, "Error compensating downloaded file={DestinationFilePath}", compensateContext.Log.DestinationFilePath); return Task.FromResult(compensateContext.Failed(exception)); } } diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 73ef143..6d889b5 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -40,7 +40,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!removeFile.IsSuccess) { var errorMessage = string.Join("; ", removeFile.Errors); - logger.LogWarning("Failed to remove file {KeyName} from bucket {BucketName}. Errors: {Errors}", + logger.LogWarning("Failed to remove file={Key} from bucket={Bucket}. Errors={Errors}", executeContext.Arguments.Key, executeContext.Arguments.Bucket, errorMessage); @@ -51,7 +51,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error removing file {KeyName} from bucket {BucketName}", + logger.LogError(exception, "Error removing file={Key} from bucket={Bucket}", executeContext.Arguments.Key, executeContext.Arguments.Bucket); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 645c948..c3500c7 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -51,7 +51,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!transferFilesResult.IsSuccess) { var errorMessage = string.Join("; ", transferFilesResult.Errors); - logger.LogWarning("Failed to transfer files from {FolderPath} to bucket {DestinationBucketName}, key {DestinationKeyName}. Errors: {Errors}", + logger.LogWarning("Failed to transfer files from={FolderPath} to bucket={DestinationBucket}, key={DestinationKey}. Errors={Errors}", folderPath, executeContext.Arguments.DestinationBucket, destinationKey, @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error transferring files from {FolderPath} to bucket {DestinationBucketName}", + logger.LogError(exception, "Error transferring files from={FolderPath} to bucket={DestinationBucket}", folderPath, executeContext.Arguments.DestinationBucket); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); @@ -88,7 +88,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating transferred files to bucket {DestinationBucketName}", compensateContext.Log.DestinationBucket); + logger.LogError(exception, "Error compensating transferred files to bucket={DestinationBucket}", compensateContext.Log.DestinationBucket); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index d7d225b..60585c4 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files if (!uploadResult.IsSuccess) { var errors = string.Join("; ", uploadResult.Errors); - logger.LogWarning("Failed to upload file to bucket. Bucket: {BucketName}, Key: {KeyName}, Errors: {Errors}", + logger.LogWarning("Failed to upload file to bucket. Bucket={Bucket}, Key={Key}, Errors={Errors}", executeContext.Arguments.DestinationBucket, executeContext.Arguments.DestinationRoute, errors); @@ -60,7 +60,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files } catch (Exception exception) { - logger.LogError(exception, "An error occurred while uploading file to bucket for process {ProcessId} at step {StepId}", processId, stepId); + logger.LogError(exception, "An error occurred while uploading file to bucket for process={ProcessId} at step={StepId}", processId, stepId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } @@ -78,16 +78,15 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files if (!uploadResult.IsSuccess) { var errors = string.Join("; ", uploadResult.Errors); - var errorMessage = $"Failed to remove file from bucket during compensation. Bucket: {compensateContext.Log.DestinationBucket}, Key: {compensateContext.Log.DestinationKey}, Errors: {errors}"; + logger.LogError($"Failed to remove file from bucket during compensation. Bucket={compensateContext.Log.DestinationBucket}, Key={compensateContext.Log.DestinationKey}, Errors={errors}"); - logger.LogError(errorMessage); - return compensateContext.Failed(new Exception(errorMessage)); + 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) { - logger.LogError(exception, "An error occurred while compensating upload file to bucket activity for bucket {BucketName} and key {KeyName}", + logger.LogError(exception, "An error occurred while compensating upload file to bucket activity for bucket={Bucket} and key={Key}", compensateContext.Log.DestinationBucket, compensateContext.Log.DestinationKey); return compensateContext.Failed(exception); diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 867a83a..f34a7bd 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -47,7 +47,7 @@ namespace Musify.Infrastructure.MassTransit.Activities Directory.CreateDirectory(workingDirectory); - logger.LogDebug("Generated picture workflow paths. WorkingDirectory: {WorkingDirectory}, SourceFilePath: {SourceFilePath}", + logger.LogDebug("Generated picture workflow paths. WorkingDirectory={WorkingDirectory}, SourceFilePath={SourceFilePath}", workingDirectory, sourceFilePath); @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error generating picture workflow paths for key {SourceKeyName}", + logger.LogError(exception, "Error generating picture workflow paths for key={SourceKey}", executeContext.Arguments.SourceKey); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index eea6a1f..4768713 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -41,7 +41,7 @@ namespace Musify.Infrastructure.MassTransit.Activities { if (!File.Exists(sourceFilePath)) { - logger.LogWarning("Source file not found at {SourceFilePath}", sourceFilePath); + logger.LogWarning("Source file not found at={SourceFilePath}", sourceFilePath); throw new FileNotFoundException($"Source file not found: {sourceFilePath}"); } @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!resizedPicture.IsSuccess) { var errorMessage = string.Join("; ", resizedPicture.Errors); - logger.LogError("Failed to resize picture {SourceFilePath} to {Width}x{Height}. Errors: {Errors}", + logger.LogError("Failed to resize picture={SourceFilePath} to={Width}x{Height}. Errors={Errors}", sourceFilePath, executeContext.Arguments.Width, executeContext.Arguments.Height, @@ -71,18 +71,16 @@ namespace Musify.Infrastructure.MassTransit.Activities await using var destinationStream = File.Create(destinationFilePath); await resizedPicture.Value.CopyToAsync(destinationStream, executeContext.CancellationToken); - logger.LogDebug("Picture resized from {SourceFilePath} to {DestinationFilePath} ({Width}x{Height})", + logger.LogDebug("Picture resized from={SourceFilePath} to={DestinationFilePath}", sourceFilePath, - destinationFilePath, - executeContext.Arguments.Width, - executeContext.Arguments.Height); + destinationFilePath); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(new ResizePictureLog(destinationFilePath)); } catch (Exception exception) { - logger.LogError(exception, "Error resizing picture {SourceFilePath}", sourceFilePath); + logger.LogError(exception, "Error resizing picture={SourceFilePath}", sourceFilePath); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } @@ -97,7 +95,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating resized file {DestinationFilePath}", compensateContext.Log.DestinationFilePath); + logger.LogError(exception, "Error compensating resized file={DestinationFilePath}", compensateContext.Log.DestinationFilePath); return Task.FromResult(compensateContext.Failed(exception)); } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 992bd5a..2901432 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -46,27 +46,27 @@ namespace Musify.Infrastructure.MassTransit.Activities if (playList is null) { - logger.LogWarning("PlayList with id {PlayListId} not found", + logger.LogWarning("PlayList with id={PlayListId} not found", executeContext.Arguments.PlayListId); throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); } var log = new UpdatePlayListPictureLog( playList.Id, - playList.OriginalPictureKeyName, - playList.SmallPictureKeyName, - playList.MediumPictureKeyName, - playList.LargePictureKeyName); + playList.OriginalPictureName, + playList.SmallPictureName, + playList.MediumPictureName, + playList.LargePictureName); - playList.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); - playList.SmallPictureKeyName = Path.GetFileName(smallResizedVariable); - playList.MediumPictureKeyName = Path.GetFileName(mediumResizedVariable); - playList.LargePictureKeyName = Path.GetFileName(largeResizedVariable); + playList.OriginalPictureName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); + playList.SmallPictureName = Path.GetFileName(smallResizedVariable); + playList.MediumPictureName = Path.GetFileName(mediumResizedVariable); + playList.LargePictureName = Path.GetFileName(largeResizedVariable); database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated PlayList {PlayListId} pictures", + logger.LogInformation("Updated PlayList={PlayListId} pictures", executeContext.Arguments.PlayListId); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); @@ -74,7 +74,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error updating PlayList {PlayListId} pictures", + logger.LogError(exception, "Error updating PlayList={PlayListId} pictures", executeContext.Arguments.PlayListId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -94,10 +94,10 @@ namespace Musify.Infrastructure.MassTransit.Activities return compensateContext.Compensated(); } - playList.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKey; - playList.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKey; - playList.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKey; - playList.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKey; + playList.OriginalPictureName = compensateContext.Log.PreviousOriginalPictureKey; + playList.SmallPictureName = compensateContext.Log.PreviousSmallPictureKey; + playList.MediumPictureName = compensateContext.Log.PreviousMediumPictureKey; + playList.LargePictureName = compensateContext.Log.PreviousLargePictureKey; database.PlayLists.Update(playList); await database.SaveChangesAsync(compensateContext.CancellationToken); @@ -106,7 +106,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating PlayList {PlayListId} pictures", compensateContext.Log.PlayListId); + logger.LogError(exception, "Error compensating PlayList={PlayListId} pictures", compensateContext.Log.PlayListId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index fd68ed7..0ec8e1a 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -46,27 +46,27 @@ namespace Musify.Infrastructure.MassTransit.Activities if (track is null) { - logger.LogWarning("Track with id {TrackId} not found", + logger.LogWarning("Track with id={TrackId} not found", executeContext.Arguments.TrackId); throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); } var log = new UpdateTrackPictureLog( track.Id, - track.OriginalPictureKeyName, - track.SmallPictureKeyName, - track.MediumPictureKeyName, - track.LargePictureKeyName); + track.OriginalPictureName, + track.SmallPictureName, + track.MediumPictureName, + track.LargePictureName); - track.OriginalPictureKeyName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); - track.SmallPictureKeyName = Path.GetFileName(smallResizedVariable); - track.MediumPictureKeyName = Path.GetFileName(mediumResizedVariable); - track.LargePictureKeyName = Path.GetFileName(largeResizedVariable); + track.OriginalPictureName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); + track.SmallPictureName = Path.GetFileName(smallResizedVariable); + track.MediumPictureName = Path.GetFileName(mediumResizedVariable); + track.LargePictureName = Path.GetFileName(largeResizedVariable); database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated Track {TrackId} pictures", + logger.LogInformation("Updated Track={TrackId} pictures", executeContext.Arguments.TrackId); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); @@ -74,7 +74,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error updating Track {TrackId} pictures", + logger.LogError(exception, "Error updating Track={TrackId} pictures", executeContext.Arguments.TrackId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -94,10 +94,10 @@ namespace Musify.Infrastructure.MassTransit.Activities return compensateContext.Compensated(); } - track.OriginalPictureKeyName = compensateContext.Log.PreviousOriginalPictureKey; - track.SmallPictureKeyName = compensateContext.Log.PreviousSmallPictureKey; - track.MediumPictureKeyName = compensateContext.Log.PreviousMediumPictureKey; - track.LargePictureKeyName = compensateContext.Log.PreviousLargePictureKey; + track.OriginalPictureName = compensateContext.Log.PreviousOriginalPictureKey; + track.SmallPictureName = compensateContext.Log.PreviousSmallPictureKey; + track.MediumPictureName = compensateContext.Log.PreviousMediumPictureKey; + track.LargePictureName = compensateContext.Log.PreviousLargePictureKey; database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); @@ -106,7 +106,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating Track {TrackId} pictures", compensateContext.Log.TrackId); + logger.LogError(exception, "Error compensating Track={TrackId} pictures", compensateContext.Log.TrackId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.Designer.cs b/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.Designer.cs similarity index 93% rename from Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.Designer.cs rename to Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.Designer.cs index e9a6cc7..f84d009 100644 --- a/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Musify.Infrastructure.Persistence.Migrations { [DbContext(typeof(Database))] - [Migration("20260325003221_FirstMigration")] + [Migration("20260328141805_FirstMigration")] partial class FirstMigration { /// @@ -36,15 +36,15 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("Description") .IsRequired() - .HasMaxLength(254) - .HasColumnType("character varying(254)"); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); - b.Property("LargePictureKeyName") + b.Property("LargePictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("MediumPictureKeyName") + b.Property("MediumPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -59,12 +59,12 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("OriginalPictureKeyName") + b.Property("OriginalPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("SmallPictureKeyName") + b.Property("SmallPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -195,7 +195,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AudioFolderKeyName") + b.Property("AudioFolderName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -203,12 +203,12 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); - b.Property("LargePictureKeyName") + b.Property("LargePictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("MediumPictureKeyName") + b.Property("MediumPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -218,17 +218,17 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("OriginalAudioKeyName") + b.Property("OriginalAudioName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("OriginalPictureKeyName") + b.Property("OriginalPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("SmallPictureKeyName") + b.Property("SmallPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -252,7 +252,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("BucketName") + b.Property("Bucket") .IsRequired() .HasColumnType("text"); @@ -263,7 +263,7 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("KeyName") + b.Property("Key") .IsRequired() .HasColumnType("text"); diff --git a/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs similarity index 88% rename from Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.cs rename to Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs index 5994892..d184dd8 100644 --- a/Infrastructure/Persistence/Migrations/20260325003221_FirstMigration.cs +++ b/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs @@ -37,12 +37,12 @@ namespace Musify.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), Title = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), NormalizedTitle = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - OriginalPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - OriginalAudioKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - AudioFolderKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + OriginalPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + SmallPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + MediumPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + LargePictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + OriginalAudioName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + AudioFolderName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -56,8 +56,8 @@ namespace Musify.Infrastructure.Persistence.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - KeyName = table.Column(type: "text", nullable: false), - BucketName = table.Column(type: "text", nullable: false), + Bucket = table.Column(type: "text", nullable: false), + Key = table.Column(type: "text", nullable: false), ContentType = table.Column(type: "text", nullable: false), CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), State = table.Column(type: "integer", nullable: false) @@ -113,11 +113,11 @@ namespace Musify.Infrastructure.Persistence.Migrations UserId = table.Column(type: "uuid", nullable: false), Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), NormalizedName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Description = table.Column(type: "character varying(254)", maxLength: 254, nullable: false), - OriginalPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - SmallPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - MediumPictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - LargePictureKeyName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + OriginalPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + SmallPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + MediumPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + LargePictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) }, diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index b4994a4..85d9421 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -33,15 +33,15 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("Description") .IsRequired() - .HasMaxLength(254) - .HasColumnType("character varying(254)"); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); - b.Property("LargePictureKeyName") + b.Property("LargePictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("MediumPictureKeyName") + b.Property("MediumPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -56,12 +56,12 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("OriginalPictureKeyName") + b.Property("OriginalPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("SmallPictureKeyName") + b.Property("SmallPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -192,7 +192,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AudioFolderKeyName") + b.Property("AudioFolderName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -200,12 +200,12 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); - b.Property("LargePictureKeyName") + b.Property("LargePictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("MediumPictureKeyName") + b.Property("MediumPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -215,17 +215,17 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("OriginalAudioKeyName") + b.Property("OriginalAudioName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("OriginalPictureKeyName") + b.Property("OriginalPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("SmallPictureKeyName") + b.Property("SmallPictureName") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -249,7 +249,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("BucketName") + b.Property("Bucket") .IsRequired() .HasColumnType("text"); @@ -260,7 +260,7 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("KeyName") + b.Property("Key") .IsRequired() .HasColumnType("text"); diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index f5c3a12..4bbf97c 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Pictures { try { - logger.LogDebug("Resizing picture to {Width}x{Height}", width, height); + logger.LogDebug("Resizing picture to={Width}x{Height}", width, height); if (pictureStream.CanSeek && pictureStream.Position > 0) pictureStream.Position = 0; @@ -31,12 +31,12 @@ namespace Musify.Infrastructure.Pictures memoryStream.Position = 0; - logger.LogDebug("Picture resized successfully. OutputLength: {Length}", memoryStream.Length); + logger.LogDebug("Picture resized successfully. OutputLength={Length}", memoryStream.Length); return Result.Success(memoryStream); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while resizing the picture to {Width}x{Height}.", width, height); + logger.LogError(exception, "An error occurred while resizing the picture to={Width}x{Height}.", width, height); return Result.Error("An error occurred while resizing the picture."); } } diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index 44fce65..3293281 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -13,61 +13,61 @@ namespace Musify.Infrastructure.Storage public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, InfrastructureStorageConfiguration storageClientConfiguration, ILogger logger) : IStorageHandler { - public async Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) + public async Task> GetFileAsync(string bucket, string key, CancellationToken cancellationToken) { var request = new GetObjectRequest { - BucketName = bucketName, - Key = keyName, + BucketName = bucket, + Key = key, }; - logger.LogDebug("Getting file from S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); + logger.LogDebug("Getting file from S3. Bucket={Bucket}, Key={Key}", bucket, key); try { var response = await amazonS3.GetObjectAsync(request, cancellationToken); - logger.LogDebug("Successfully got file from S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogDebug("Successfully got file from S3 with bucket={Bucket} and key={Key}", bucket, key); return Result.Success(response.ResponseStream); } catch (Exception exception) { - logger.LogWarning(exception, "Failed to get file from S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); - return Result.NotFound($"File not found in S3 with bucket name {bucketName} and key name {keyName}"); + logger.LogWarning(exception, "Failed to get file from S3. Bucket={Bucket}, Key={Key}", bucket, key); + return Result.NotFound($"File not found in S3 with bucket: {bucket} and key: {key}"); } } - public async Task GetUrlAsync(string bucketName, string keyName, TimeSpan expirationTime, CancellationToken cancellationToken) + public async Task GetUrlAsync(string bucket, string key, TimeSpan expirationTime, CancellationToken cancellationToken) { var request = new GetPreSignedUrlRequest { - BucketName = bucketName, - Key = keyName, + BucketName = bucket, + Key = key, Expires = DateTime.UtcNow + expirationTime, Protocol = storageClientConfiguration.UseHttp ? Protocol.HTTP : Protocol.HTTPS }; - logger.LogDebug("Getting pre-signed URL from S3. Bucket: {BucketName}, Key: {KeyName}, Expires: {Expires}", bucketName, keyName, request.Expires); + logger.LogDebug("Getting pre-signed URL from S3. Bucket={BucketName}, Key={Key}, Expires={Expires}", bucket, key, request.Expires); var url = await amazonS3.GetPreSignedURLAsync(request); return url; } - public async Task RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) + public async Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken) { var request = new DeleteObjectRequest { - BucketName = bucketName, - Key = keyName, + BucketName = bucket, + Key = key, }; - logger.LogDebug("Removing file from S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); + logger.LogDebug("Removing file from S3. Bucket={Bucket}, Key={Key}", bucket, key); var response = await amazonS3.DeleteObjectAsync(request, cancellationToken); return Result.NoContent(); } - public async Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken) + public async Task TransferFilesAsync(string sourceDirectory, string bucket, string route, CancellationToken cancellationToken) { var trasnsferUtility = new TransferUtility(amazonS3); @@ -81,67 +81,67 @@ namespace Musify.Infrastructure.Storage string fileName = Path.GetFileName(file); string filePath = Path.Combine(sourceDirectory, file); - var keyName = Path.Combine(route, fileName); + var key = Path.Combine(route, fileName); var upload = new Upload { Id = Guid.NewGuid(), - BucketName = bucketName, - KeyName = keyName, - ContentType = "Application/Octet-Stream" + Bucket = bucket, + Key = key, + ContentType = MimeUtility.GetMimeMapping(filePath) }; await database.Uploads.AddAsync(upload, cancellationToken); try { - await trasnsferUtility.UploadAsync(filePath, bucketName, keyName, cancellationToken); + await trasnsferUtility.UploadAsync(filePath, bucket, key, cancellationToken); upload.State = UploadState.Successful; successCount++; - logger.LogDebug("Transferred file to S3. Bucket: {BucketName}, KeyName: {KeyName}", bucketName, keyName); + logger.LogDebug("Transferred file to S3. Bucket={Bucket}, Key={Key}", bucket, key); } catch (Exception exception) { failedCount++; upload.State = UploadState.Failed; - logger.LogError(exception, "Failed to transfer file to S3. Bucket: {BucketName}, keyName: {keyName}", bucketName, keyName); + logger.LogError(exception, "Failed to transfer file to S3. Bucket={Bucket}, key={key}", bucket, key); } } await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("TransferFiles completed. Bucket: {BucketName}, Route: {Route}, Success: {SuccessCount}, Failed: {FailedCount}", bucketName, route, successCount, failedCount); + logger.LogInformation("TransferFiles completed. Bucket={Bucket}, Route={Route}, Success={SuccessCount}, Failed={FailedCount}", bucket, route, successCount, failedCount); return Result.Success(); } - public async Task> UploadFileAsync(string filePath, string bucketName, string keyName, 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, bucketName, keyName, cancellationToken); + return await UploadFileAsync(fileStream, contentType, bucket, key, cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); - return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}"); + logger.LogError(exception, "Failed to upload file to S3 with bucket={Bucket} and key={Key}", bucket, key); + return Result.Error($"Failed to upload file to S3 with bucket: {bucket} and key: {key}"); } } - public async Task> UploadFileAsync(Stream sourceStream, string contentType, string bucketName, string keyName, CancellationToken cancellationToken) + public async Task> UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken) { var request = new PutObjectRequest { - BucketName = bucketName, + BucketName = bucket, ContentType = contentType, InputStream = sourceStream, - Key = keyName + Key = key }; var upload = new Upload { Id = Guid.NewGuid(), - BucketName = bucketName, - KeyName = keyName, + Bucket = bucket, + Key = key, ContentType = contentType }; await database.Uploads.AddAsync(upload, cancellationToken); @@ -149,45 +149,45 @@ namespace Musify.Infrastructure.Storage try { var response = await amazonS3.PutObjectAsync(request, cancellationToken); - logger.LogDebug("Uploaded file to S3. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); + logger.LogDebug("Uploaded file to S3. Bucket?={Bucket}, Key={Key}", bucket, key); } catch (Exception exception) { - logger.LogError(exception, "Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + logger.LogError(exception, "Failed to upload file to S3 with bucket={Bucket} and key={Key}", bucket, key); upload.State = UploadState.Failed; - return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}"); + return Result.Error($"Failed to upload file to S3 with bucket: {bucket} and key: {key}"); } upload.State = UploadState.Successful; await database.SaveChangesAsync(cancellationToken); - logger.LogDebug("Saved upload record to database. Bucket: {BucketName}, Key: {KeyName}", bucketName, keyName); + logger.LogDebug("Saved upload record to database. Bucket={Bucket}, Key={Key}", bucket, key); - return Result.Success(keyName); + return Result.Success(key); } - public async Task CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName, + public async Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, CancellationToken cancellationToken) { var request = new CopyObjectRequest { - SourceBucket = sourceBucketName, - SourceKey = sourceKeyName, - DestinationBucket = destinationBucketName, - DestinationKey = destinationKeyName + SourceBucket = sourceBucket, + SourceKey = sourceKey, + DestinationBucket = destinationBucket, + DestinationKey = destinationKey }; try { var response = await amazonS3.CopyObjectAsync(request, cancellationToken); - logger.LogInformation("Successfully copied file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", - sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); + logger.LogInformation("Successfully copied file from S3 with bucket={SourceBucket} and key={SourceKey} to bucket={DestinationBucket} and key={DestinationKey}", + sourceBucket, sourceKey, destinationBucket, destinationKey); return Result.Success(); } catch (Exception exception) { - logger.LogError(exception, "Failed to copy file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", - sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); - return Result.Error($"Failed to copy file from S3 with bucket name {sourceBucketName} and key name {sourceKeyName} to bucket name {destinationBucketName} and key name {destinationKeyName}"); + logger.LogError(exception, "Failed to copy file from S3 with bucket={SourceBucket} and key={SourceKey} to bucket={DestinationBucket} and key={DestinationKey}", + sourceBucket, sourceKey, destinationBucket, destinationKey); + return Result.Error($"Failed to copy file from S3 with bucket: {sourceBucket} and key: {sourceKey} to bucket: {destinationBucket} and key: {destinationKey}"); } } } From 4e3302c144834221084b5858a13d3b1ceedbde05 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 20:47:54 +0100 Subject: [PATCH 102/172] Rewrite handlers --- Application/Extensions/ResultExtensions.cs | 24 +++ .../Commands/CreatePlayListCommand.cs | 19 +- .../Handlers/CreatePlayListCommandHandler.cs | 157 +++++++------- .../Handlers/DeletePlayListCommandHandler.cs | 38 +--- .../Handlers/UpdatePlayListCommandHandler.cs | 191 ++++++++++-------- .../Tracks/Commands/CreateTrackCommand.cs | 18 +- .../Tracks/Commands/DeleteTrackCommand.cs | 10 + .../Handler/CreateTrackCommandHandler.cs | 174 ++++++++-------- .../Handler/DeleteTrackCommandHandler.cs | 69 +++++++ .../Users/Commands/CreateUserCommand.cs | 12 +- .../Handlers/CreateUserCommandHandler.cs | 13 +- .../Handlers/DeleteUserCommandHandler.cs | 1 - 12 files changed, 441 insertions(+), 285 deletions(-) create mode 100644 Application/Extensions/ResultExtensions.cs create mode 100644 Application/Tracks/Commands/DeleteTrackCommand.cs create mode 100644 Application/Tracks/Handler/DeleteTrackCommandHandler.cs diff --git a/Application/Extensions/ResultExtensions.cs b/Application/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..d4f0ad5 --- /dev/null +++ b/Application/Extensions/ResultExtensions.cs @@ -0,0 +1,24 @@ +using Ardalis.Result; + +namespace Musify.Application.Extensions +{ + public static class ResultExtensions + { + public static Result ToErrorResult(this Result result) + { + var newResult = result.Status switch + { + ResultStatus.Error => Result.Error(new ErrorList(result.Errors)), + ResultStatus.Forbidden => Result.Forbidden(result.Errors.ToArray()), + ResultStatus.Unauthorized => Result.Unauthorized(result.Errors.ToArray()), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors.ToArray()), + ResultStatus.NotFound => Result.NotFound(result.Errors.ToArray()), + ResultStatus.Conflict => Result.Conflict(result.Errors.ToArray()), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported.") + }; + return newResult; + } + } +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index 0ceddcc..cda4122 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -2,6 +2,7 @@ using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Responses; +using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands { @@ -10,5 +11,21 @@ namespace Musify.Application.PlayLists.Commands string Name, string Description, IFileData? Picture) - : IRequest> >; + : IRequest> > + { + public static PlayList ToEntity(CreatePlayListCommand command, string originalKey, string smallKey, string mediumKey, string largeKey) + { + return new PlayList + { + UserId = command.UserId, + Name = command.Name, + NormalizedName = command.Name.Trim().ToUpperInvariant(), + Description = command.Description, + OriginalPictureName = originalKey, + SmallPictureName = smallKey, + MediumPictureName = mediumKey, + LargePictureName = largeKey, + }; + } + } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 1bdd67d..edccb2d 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -6,6 +6,7 @@ using Musify.Application.Configuration; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Application.Extensions; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -19,101 +20,107 @@ namespace Musify.Application.PlayLists.Handlers public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken); - if (!userExists) { logger.LogWarning("User with Id={UserId} does not exist.", request.UserId); return Result.NotFound($"User with Id {request.UserId} does not exist."); } - var playList = new PlayList + var playListEntity = CreatePlayListCommand.ToEntity(request, + playListConfiguration.Routes.PresetOriginalPicture, + playListConfiguration.Routes.PresetSmallPicture, + playListConfiguration.Routes.PresetMediumPicture, + playListConfiguration.Routes.PresetLargePicture); + + if (request.Picture is not null) { - UserId = request.UserId, - Name = request.Name, - NormalizedName = request.Name.Trim().ToUpperInvariant(), - Description = request.Description, - OriginalPictureName = playListConfiguration.Routes.PresetOriginalPicture, - SmallPictureName = playListConfiguration.Routes.PresetSmallPicture, - MediumPictureName = playListConfiguration.Routes.PresetMediumPicture, - LargePictureName = playListConfiguration.Routes.PresetLargePicture, - }; + var uploadResult = await SetPicture(playListEntity, request.Picture, cancellationToken); + if (!uploadResult.IsSuccess) + return uploadResult; - var setPictureResult = request.Picture is null - ? Result.Success() - : await UploadPicture(playList, request.Picture, cancellationToken); - - if (!setPictureResult.IsSuccess) - return setPictureResult; - - await database.PlayLists.AddAsync(playList, cancellationToken); - await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playList.Id, request.UserId); - return Result.Created(PlayListResponse.FromEntity(playList)); - } - - async Task UploadPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) - { - var imageId = Guid.NewGuid(); - var pictureName = imageId + picture.FileType; - var pictureStorageKey = Path.Join(playListConfiguration.Routes.OriginalPicturesPath, - pictureName); - - var uploadResult = await UploadFile(picture, pictureStorageKey, 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."); + var publishResult = await PublishUpdateEvent(playListEntity, cancellationToken); + if (!publishResult.IsSuccess) + { + await RollbackPicture(playListEntity, cancellationToken); + return publishResult; + } } - playList.OriginalPictureName = pictureName; - logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); - try { - await PublishUpdateEvent(pictureStorageKey, playList, cancellationToken); + await database.PlayLists.AddAsync(playListEntity, cancellationToken); + await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish resize picture event for PlayList with id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); + await RollbackPicture(playListEntity, cancellationToken); + logger.LogError(exception, "Failed to create PlayList for user with id={UserId}.", request.UserId); + return Result.Error($"Failed to create PlayList for user with id {request.UserId}."); } + + logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playListEntity.Id, request.UserId); + return Result.Created(PlayListResponse.FromEntity(playListEntity)); + } + + async Task RollbackPicture(PlayList playList, CancellationToken cancellationToken) + { + try + { + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, + Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback picture upload for PlayList with Id={PlayListId}.", playList.Id); + } + } + + async Task SetPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) + { + var pictureName = Guid.NewGuid() + picture.FileType; + var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); + + var uploadResult = await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.BucketName, pictureKey, 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 uploadResult.ToErrorResult(); + } + + playList.OriginalPictureName = pictureName; + return Result.Success(); } - async Task> UploadFile(IFileData fileData, string storageKeyName, CancellationToken cancellationToken) + async Task PublishUpdateEvent(PlayList playList, CancellationToken cancellationToken) { - var uploadFileResult = await storageHandler.UploadFileAsync( - fileData.FileStream, - fileData.ContentType, - storageConfiguration.BucketName, - storageKeyName, - cancellationToken); - - return uploadFileResult; - } - - async Task PublishUpdateEvent(string originalPictureKey, PlayList playList, CancellationToken cancellationToken) - { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.BucketName, - originalPictureKey, - new ImageSize( - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)), - cancellationToken); + try + { + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + playList.OriginalPictureName, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish UpdatePlayListPictureEvent for PlayList with Id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish UpdatePlayListPictureEvent for PlayList with Id {playList.Id}."); + } } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 1151b5e..4f5be7c 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -14,8 +14,9 @@ namespace Musify.Application.PlayLists.Handlers { public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) { - var playList = await database.PlayLists.FindAsync(request.PlayListId, cancellationToken); - + var playList = await database.PlayLists.FindAsync( + [request.PlayListId], + cancellationToken); if (playList is null) { logger.LogDebug("PlayList with id={PlayListId} not found", request.PlayListId); @@ -28,33 +29,15 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } - var keysToRemove = new List(); - if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) - keysToRemove.Add(Path.Combine(playListConfiguration.Routes.SmallPicturesPath, - playList.SmallPictureName)); + await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.SmallPicturesPath, + playList.SmallPictureName), cancellationToken); if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) - keysToRemove.Add(Path.Combine(playListConfiguration.Routes.MediumPicturesPath, - playList.MediumPictureName)); + await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.MediumPicturesPath, + playList.MediumPictureName), cancellationToken); if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) - keysToRemove.Add(Path.Combine(playListConfiguration.Routes.LargePicturesPath, - playList.LargePictureName)); - - foreach (var key in keysToRemove) - { - var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); - - if (!publishResult.IsSuccess) - { - if (publishResult.IsNotFound()) - { - logger.LogWarning("File with key={Key} not found in storage while trying to publish remove file event for PlayList with id={PlayListId}.", key, playList.Id); - continue; - } - - return publishResult; - } - } + await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.LargePicturesPath, + playList.LargePictureName), cancellationToken); database.PlayLists.Remove(playList); await database.SaveChangesAsync(cancellationToken); @@ -62,14 +45,13 @@ namespace Musify.Application.PlayLists.Handlers return Result.NoContent(); } - async Task TryPublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) + async Task PublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) { try { await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, key), cancellationToken); - return Result.Success(); } catch (Exception exception) diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 0647ed9..4666966 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -1,11 +1,11 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Application.Extensions; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -19,38 +19,116 @@ namespace Musify.Application.PlayLists.Handlers { public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { - var playList = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); - - if (playList is null) + var playListEntity = await database.PlayLists.FindAsync( + [request.PlayListId], + cancellationToken); + if (playListEntity is null) { - logger.LogInformation("PlayList with id={PlayListId} not found", request.PlayListId); + logger.LogInformation("PlayList with id={PlayListId} not found.", request.PlayListId); return Result.NotFound(); } - if (playList.UserId != request.UserId) + if (playListEntity.UserId != request.UserId) { - logger.LogWarning("User with id={UserId} is not the owner of the PlayList with id={PlayListId}", request.UserId, request.PlayListId); + logger.LogWarning("User with id={UserId} is not the owner of the PlayList with id={PlayListId}.", request.UserId, request.PlayListId); return Result.Unauthorized(); } - if (!string.IsNullOrWhiteSpace(request.NewName)) - { - playList.Name = request.NewName; - playList.NormalizedName = request.NewName.Trim().ToUpperInvariant(); - } + UpdateName(playListEntity, request.NewName); + UpdateDescription(playListEntity, request.NewDescription); + var updatePictureResult = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); + if (!updatePictureResult.IsSuccess) + return updatePictureResult.ToErrorResult(); - if (!string.IsNullOrWhiteSpace(request.NewDescription)) - playList.Description = request.NewDescription; - - if (request.NewPicture is null) + try { - database.PlayLists.Update(playList); + database.PlayLists.Update(playListEntity); await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("Updated PlayList with id={PlayListId} without picture changes.", playList.Id); - return Result.Success(PlayListResponse.FromEntity(playList)); + await PublishPlayListPictureEvent(playListEntity, updatePictureResult.Value, cancellationToken); + await RemoveOldPictures(playListEntity, cancellationToken); + } + catch (Exception exception) + { + await Rollback(updatePictureResult.Value, cancellationToken); + logger.LogError(exception, "Failed to update PlayList with id={PlayListId}.", playListEntity.Id); + return Result.Error($"Failed to update PlayList with id {playListEntity.Id}."); } + logger.LogInformation("Successfully updated PlayList with id={PlayListId}.", playListEntity.Id); + return Result.Success(PlayListResponse.FromEntity(playListEntity)); + } + + void UpdateName(PlayList playList, string? newName) + { + if (!string.IsNullOrWhiteSpace(newName)) + { + playList.Name = newName; + playList.NormalizedName = newName.Trim().ToUpperInvariant(); + } + } + + void UpdateDescription(PlayList playList, string? newDescription) + { + if (!string.IsNullOrWhiteSpace(newDescription)) + playList.Description = newDescription; + } + + async Task> UpdatePicture(PlayList playList, IFileData? newPicture, CancellationToken cancellationToken) + { + if (newPicture is null) + return Result.Success(); + + var pictureName = Guid.NewGuid() + newPicture.FileType; + var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); + var uploadResult = await storageHandler.UploadFileAsync( + newPicture.FileStream, + newPicture.ContentType, + storageConfiguration.BucketName, + pictureKey, + 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."); + } + playList.OriginalPictureName = pictureName; + return Result.Success(pictureKey); + } + + async Task PublishPlayListPictureEvent(PlayList playList, string pictureKey, CancellationToken cancellationToken) + { + try + { + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + pictureKey, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish UpdatePlayListPictureEvent for PlayList with id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish UpdatePlayListPictureEvent for PlayList with id {playList.Id}."); + } + } + + async Task RemoveOldPictures(PlayList playList, CancellationToken cancellationToken) + { if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) await PublishRemoveFileEvent(Path.Combine( playListConfiguration.Routes.SmallPicturesPath, @@ -66,34 +144,15 @@ namespace Musify.Application.PlayLists.Handlers playListConfiguration.Routes.LargePicturesPath, playList.LargePictureName), cancellationToken); + } - var newImageId = Guid.NewGuid(); - var pictureName = newImageId + request.NewPicture.FileType; - var newImageStorageKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); - - var uploadResult = await UploadFile(request.NewPicture, newImageStorageKey, 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."); - } - - try - { - await PublishUpdateEvent(newImageStorageKey, playList, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish update picture event for PlayList with id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish update picture event for PlayList with id {playList.Id}."); - } - - database.PlayLists.Update(playList); - await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("Updated PlayList with id={PlayListId} and picture key={PictureKey}.", playList.Id, newImageStorageKey); - return Result.Success(PlayListResponse.FromEntity(playList)); + async Task Rollback(string pictureKey, CancellationToken cancellationToken) + { + await storageHandler.RemoveFileAsync( + storageConfiguration.BucketName, + pictureKey, + cancellationToken); + logger.LogInformation("Rolled back uploaded picture with key={PictureKey} from storage.", pictureKey); } async Task PublishRemoveFileEvent(string key, CancellationToken cancellationToken) @@ -102,10 +161,7 @@ namespace Musify.Application.PlayLists.Handlers { await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, - Path.Combine( - playListConfiguration.Routes.ParentFolders, - key)), cancellationToken); - + key), cancellationToken); return Result.NoContent(); } catch (Exception exception) @@ -114,38 +170,5 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error($"Failed to publish remove file event for key {key}."); } } - - async Task> UploadFile(IFileData picture, string pictureKeyName, CancellationToken cancellationToken) - { - var uploadFileResult = await storageHandler.UploadFileAsync( - picture.FileStream, - picture.ContentType, - storageConfiguration.BucketName, - pictureKeyName, - cancellationToken); - - return uploadFileResult; - } - - async Task PublishUpdateEvent(string originalPictureKey, PlayList playList, CancellationToken cancellationToken) - { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.BucketName, - originalPictureKey, - new ImageSize( - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)), - cancellationToken); - } } } \ No newline at end of file diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index 2a6c939..fc762cd 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -2,6 +2,7 @@ using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; using Musify.Application.Tracks.Responses; +using Musify.Domain.Entities; namespace Musify.Application.Tracks.Commands { @@ -10,5 +11,20 @@ namespace Musify.Application.Tracks.Commands string Title, IFileData Picture, IFileData Audio) - : IRequest >>; + : IRequest >> + { + public static Track ToEntity(CreateTrackCommand command, string smallPictureName, string mediumPictureName, string largePictureName) + { + return new Track + { + Title = command.Title, + NormalizedTitle = command.Title.ToUpperInvariant(), + OriginalPictureName = Guid.NewGuid() + command.Picture.FileType, + OriginalAudioName = Guid.NewGuid() + command.Audio.FileType, + SmallPictureName = smallPictureName, + MediumPictureName = mediumPictureName, + LargePictureName = largePictureName + }; + } + } } \ No newline at end of file diff --git a/Application/Tracks/Commands/DeleteTrackCommand.cs b/Application/Tracks/Commands/DeleteTrackCommand.cs new file mode 100644 index 0000000..5351410 --- /dev/null +++ b/Application/Tracks/Commands/DeleteTrackCommand.cs @@ -0,0 +1,10 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; + +namespace Musify.Application.Tracks.Commands +{ + public record DeleteTrackCommand( + Guid UserId, + Guid TrackId) + : IRequest>; +} \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 423ff4d..893f644 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -2,7 +2,6 @@ using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; @@ -21,111 +20,118 @@ namespace Musify.Application.Tracks.Handler if (userExists is null) { - logger.LogWarning("User with id={UserId} not found", request.UserId); - return Result.NotFound($"User with id {request.UserId} not found"); + logger.LogWarning("User with id={UserId} not found.", request.UserId); + return Result.NotFound($"User with id {request.UserId} not found."); } - var originalPictureFileName = Guid.NewGuid() + request.Picture.FileType; - var originalAudioFileName = Guid.NewGuid() + request.Audio.FileType; - var track = new Track - { - Title = request.Title, - NormalizedTitle = request.Title.Trim().ToUpperInvariant(), - OriginalPictureName = originalPictureFileName, - OriginalAudioName = originalAudioFileName, - SmallPictureName = trackConfiguration.Routes.PresetMediumPicture, - MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, - LargePictureName = trackConfiguration.Routes.PresetLargePicture, - }; + var trackEntity = CreateTrackCommand.ToEntity( + request, + trackConfiguration.Routes.PresetSmallPicture, + trackConfiguration.Routes.PresetMediumPicture, + trackConfiguration.Routes.PresetLargePicture); - var originalPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, - originalPictureFileName); - var pictureUploadResult = await UploadFile(request.Picture, originalPictureKey, cancellationToken); - if (!pictureUploadResult.IsSuccess) - { - logger.LogError("Failed to upload picture for track={TrackTitle}", request.Title); - return Result.Error($"Failed to upload picture for track {request.Title}"); - } + var fullPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, trackEntity.OriginalPictureName); + var fullAudioKey = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, trackEntity.OriginalAudioName); + await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken); - var audioKey = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, - Guid.NewGuid() + request.Audio.FileType); - var audioUploadResult = await UploadFile(request.Audio, audioKey, cancellationToken); - if (!audioUploadResult.IsSuccess) - { - logger.LogError("Failed to upload audio for track={TrackTitle}", request.Title); - return Result.Error($"Failed to upload audio for track {request.Title}"); - } + await PublishUpdateEvent(trackEntity, cancellationToken); + await PublishTranscodeEvent(trackEntity, cancellationToken); - try - { - await PublishUpdateEvent(originalPictureKey, track, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish resize picture event for Track with id={TrackId}.", track.Id); - return Result.Error($"Failed to publish resize picture event for Track with id {track.Id}."); - } - - try - { - await PublishTranscodeEvent(track.Id, audioUploadResult, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish transcode audio event for Track with id={TrackId}.", track.Id); - return Result.Error($"An error occurred while processing the audio for Track with id {track.Id}."); - } - - await database.Tracks.AddAsync(track, cancellationToken); + await database.Tracks.AddAsync(trackEntity, cancellationToken); await database.SaveChangesAsync(cancellationToken); return Result.Success(); } - async Task> UploadFile(IFileData fileData, string key, CancellationToken cancellationToken) + async Task UploadFilesAsync( + CreateTrackCommand request, + string pictureKey, + string audioKey, + CancellationToken cancellationToken) { - var uploadFile = await storageHandler.UploadFileAsync( - fileData.FileStream, - fileData.ContentType, + var pictureUpload = await storageHandler.UploadFileAsync( + request.Picture.FileStream, + request.Picture.ContentType, storageConfiguration.BucketName, - key, + pictureKey, cancellationToken); - return uploadFile; - } + if (!pictureUpload.IsSuccess) + { + var errors = string.Join(", ", pictureUpload.Errors); + return Result.Error($"Failed to upload picture for track {request.Title}. Errors: {errors}."); + } - async Task PublishUpdateEvent(string originalPictureKey, Track track, CancellationToken cancellationToken) - { - await eventBus.PublishAsync(new UpdateTrackPictureEvent( - track.Id, + var audioUpload = await storageHandler.UploadFileAsync( + request.Audio.FileStream, + request.Audio.ContentType, storageConfiguration.BucketName, - originalPictureKey, - new ImageSize( - trackConfiguration.Routes.PresetSmallPicture, - trackConfiguration.PicturesSizes.SmallPictureWidth, - trackConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - trackConfiguration.Routes.PresetMediumPicture, - trackConfiguration.PicturesSizes.MediumPictureWidth, - trackConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - trackConfiguration.Routes.PresetLargePicture, - trackConfiguration.PicturesSizes.LargePictureWidth, - trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); + audioKey, + cancellationToken); + + if (!audioUpload.IsSuccess) + { + await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); + var errors = string.Join(", ", audioUpload.Errors); + return Result.Error($"Failed to upload audio for track {request.Title}. Errors: {errors}."); + } + + return Result.Success(); } - async Task PublishTranscodeEvent(Guid trackId, string audioKey, CancellationToken cancellationToken) + async Task RollbackFilesAsync(string pictureKey, string audioKey, CancellationToken cancellationToken) + { + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, pictureKey, cancellationToken); + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, audioKey, cancellationToken); + logger.LogInformation("Rolled back uploaded files={PictureKey}, {AudioKey}.", pictureKey, audioKey); + } + + async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken) + { + try + { + await eventBus.PublishAsync(new UpdateTrackPictureEvent( + track.Id, + storageConfiguration.BucketName, + track.OriginalPictureName, + new ImageSize( + trackConfiguration.Routes.PresetSmallPicture, + trackConfiguration.PicturesSizes.SmallPictureWidth, + trackConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + trackConfiguration.Routes.PresetMediumPicture, + trackConfiguration.PicturesSizes.MediumPictureWidth, + trackConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + trackConfiguration.Routes.PresetLargePicture, + trackConfiguration.PicturesSizes.LargePictureWidth, + trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish resize picture event for Track with id={TrackId}.", track.Id); + } + } + + async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken) { var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, Guid.NewGuid().ToString()); - await eventBus.PublishAsync(new UpdateTrackAudioEvent( - trackId, - storageConfiguration.BucketName, - audioKey, - storageConfiguration.BucketName, - destinationFolderAudio), cancellationToken); + try + { + await eventBus.PublishAsync(new UpdateTrackAudioEvent( + track.Id, + storageConfiguration.BucketName, + track.OriginalAudioName, + storageConfiguration.BucketName, + destinationFolderAudio), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish transcode audio event for Track with id={TrackId}.", track.Id); + } } } -} +} \ No newline at end of file diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs new file mode 100644 index 0000000..6d765ea --- /dev/null +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -0,0 +1,69 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Tracks.Commands; +using Musify.Domain.Entities; + +namespace Musify.Application.Tracks.Handler +{ + public class DeleteTrackCommandHandler(IDatabase database, IStorageHandler storageHandler, ILogger logger, + TrackConfiguration trackConfiguration, ApplicationStorageConfiguration storageConfiguration) + : IRequestHandler> + { + public async Task Handle(DeleteTrackCommand request, CancellationToken cancellationToken) + { + var track = await database.Tracks.FindAsync(request.TrackId, cancellationToken); + + if (track is null) + { + logger.LogInformation("Track with id={TrackId} not found.", request.TrackId); + return Result.NotFound(); + } + + if (track.Id != request.UserId) + { + logger.LogInformation("User with id={UserId} is not the owner of track with id={TrackId}.", request.UserId, request.TrackId); + return Result.Unauthorized(); + } + + await RemovePictures(track, cancellationToken); + await RemoveAudios(track, cancellationToken); + + return Result.NoContent(); + } + + async Task RemovePictures(Track track, CancellationToken cancellationToken) + { + await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, track.OriginalPictureName), + cancellationToken); + + if (track.SmallPictureName != trackConfiguration.Routes.PresetSmallPicture) + await RemoveFile(Path.Combine(trackConfiguration.Routes.SmallPicturesPath, track.SmallPictureName), + cancellationToken); + if (track.MediumPictureName != trackConfiguration.Routes.PresetMediumPicture) + await RemoveFile(Path.Combine(trackConfiguration.Routes.MediumPicturesPath, track.MediumPictureName), + cancellationToken); + if (track.LargePictureName != trackConfiguration.Routes.PresetLargePicture) + await RemoveFile(Path.Combine(trackConfiguration.Routes.LargePicturesPath, track.LargePictureName), + cancellationToken); + + } + + async Task RemoveAudios(Track track, CancellationToken cancellationToken) + { + await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), + cancellationToken); + + await RemoveFile(Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, track.AudioFolderName), + cancellationToken); + } + + async Task RemoveFile(string path, CancellationToken cancellationToken) + { + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, path, cancellationToken); + logger.LogDebug("File with path={Path} removed from storage.", path); + } + } +} \ No newline at end of file diff --git a/Application/Users/Commands/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs index afa6f51..fcc77b7 100644 --- a/Application/Users/Commands/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -1,9 +1,19 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Users.Responses; +using Musify.Domain.Entities; namespace Musify.Application.Users.Commands { public record CreateUserCommand(Guid Id) - : IRequest>>; + : IRequest> > + { + public static User ToEntity(CreateUserCommand command) + { + return new User + { + Id = command.Id + }; + } + } } \ No newline at end of file diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index a7debf7..585016f 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Commands; using Musify.Application.Users.Responses; -using Musify.Domain.Entities; namespace Musify.Application.Users.Handlers { @@ -14,27 +13,21 @@ namespace Musify.Application.Users.Handlers { public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) { - var existingUser = await database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - - if (existingUser is not null) + var userExists = await database.Users.AnyAsync(u => u.Id == request.Id, cancellationToken); + if (!userExists) { logger.LogInformation("User with Id={UserId} already exists.", request.Id); return Result.Conflict($"A user with Id {request.Id} already exists."); } var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) { logger.LogWarning("Keycloak user with Id={UserId} was not found.", request.Id); return Result.NotFound("Keycloak user not found."); } - var newUser = new User - { - Id = request.Id - }; - + var newUser = CreateUserCommand.ToEntity(request); await database.Users.AddAsync(newUser, cancellationToken); await database.SaveChangesAsync(cancellationToken); diff --git a/Application/Users/Handlers/DeleteUserCommandHandler.cs b/Application/Users/Handlers/DeleteUserCommandHandler.cs index 906bee3..dbab4e7 100644 --- a/Application/Users/Handlers/DeleteUserCommandHandler.cs +++ b/Application/Users/Handlers/DeleteUserCommandHandler.cs @@ -15,7 +15,6 @@ namespace Musify.Application.Users.Commands return Result.NotFound(); database.Users.Remove(existingUser); - await database.SaveChangesAsync(cancellationToken); return Result.NoContent(); From b39de0d87004d7c7fbc665193b543887cbafa1da Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 21:26:45 +0100 Subject: [PATCH 103/172] Changes on messages --- .../Handlers/CreatePlayListCommandHandler.cs | 20 +++--- .../Handlers/DeletePlayListCommandHandler.cs | 10 +-- .../Handlers/GetPlayListByIdQueryHandler.cs | 4 +- .../Handlers/UpdatePlayListCommandHandler.cs | 26 ++++---- .../Handler/CreateTrackCommandHandler.cs | 17 +++-- .../Handler/DeleteTrackCommandHandler.cs | 9 ++- .../Handlers/CreateUserCommandHandler.cs | 14 ++--- .../Handlers/DeleteUserCommandHandler.cs | 1 - .../Users/Handlers/GetUserByIdQueryHandler.cs | 8 +-- .../Handlers/GetUserByKeycloakQueryHandler.cs | 6 +- Infrastructure/Audio/AudioTranscoder.cs | 62 +++++++------------ .../Identity/KeycloakUserService.cs | 8 +-- .../GenerateAudioWorkflowPathsActivity.cs | 7 +-- .../Audio/TranscodeDashAudioActivity.cs | 10 +-- .../Audio/UpdateTrackAudioActivity.cs | 10 +-- .../Files/DownloadFileFromBucketActivity.cs | 8 +-- .../Files/RemoveFileFromBucketActivity.cs | 6 +- .../Files/TransferFilesToBucketActivity.cs | 8 +-- .../Files/UploadFileToBucketActivity.cs | 11 ++-- .../GeneratePictureWorkflowPathsActivity.cs | 7 +-- .../Pictures/ResizePictureActivity.cs | 12 ++-- .../Pictures/UpdatePlayListPictureActivity.cs | 10 +-- .../Pictures/UpdateTrackPictureActivity.cs | 10 +-- Infrastructure/Pictures/PictureHandler.cs | 10 +-- Infrastructure/Storage/StorageHandler.cs | 38 ++++++------ 25 files changed, 153 insertions(+), 179 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index edccb2d..2ca01c9 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -22,8 +22,8 @@ namespace Musify.Application.PlayLists.Handlers var userExists = await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken); if (!userExists) { - logger.LogWarning("User with Id={UserId} does not exist.", request.UserId); - return Result.NotFound($"User with Id {request.UserId} does not exist."); + logger.LogWarning("User {UserId} not found", request.UserId); + return Result.NotFound($"User {request.UserId} not found"); } var playListEntity = CreatePlayListCommand.ToEntity(request, @@ -54,11 +54,11 @@ namespace Musify.Application.PlayLists.Handlers catch (Exception exception) { await RollbackPicture(playListEntity, cancellationToken); - logger.LogError(exception, "Failed to create PlayList for user with id={UserId}.", request.UserId); - return Result.Error($"Failed to create PlayList for user with id {request.UserId}."); + logger.LogError(exception, "Failed to create playlist for user {UserId}", request.UserId); + return Result.Error($"Failed to create playlist for user {request.UserId}"); } - logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playListEntity.Id, request.UserId); + logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playListEntity.Id, request.UserId); return Result.Created(PlayListResponse.FromEntity(playListEntity)); } @@ -71,7 +71,7 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to rollback picture upload for PlayList with Id={PlayListId}.", playList.Id); + logger.LogError(exception, "Failed to rollback picture upload for playlist {PlayListId}", playList.Id); } } @@ -83,7 +83,7 @@ namespace Musify.Application.PlayLists.Handlers var uploadResult = await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.BucketName, pictureKey, cancellationToken); if (!uploadResult.IsSuccess) { - logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error={ErrorMessage}.", + logger.LogError("Failed to upload picture for playlist {PlayListId} to storage: {ErrorMessage}", playList.Id, string.Join(", ", uploadResult.Errors)); return uploadResult.ToErrorResult(); } @@ -118,8 +118,8 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish UpdatePlayListPictureEvent for PlayList with Id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish UpdatePlayListPictureEvent for PlayList with Id {playList.Id}."); + logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playList.Id); + return Result.Error($"Failed to publish playlist {playList.Id} picture update event"); } } } diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 4f5be7c..47c33fb 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; @@ -19,13 +19,13 @@ namespace Musify.Application.PlayLists.Handlers cancellationToken); if (playList is null) { - logger.LogDebug("PlayList with id={PlayListId} not found", request.PlayListId); + logger.LogDebug("Playlist {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); + logger.LogWarning("Playlist {PlayListId} does not belong to user {UserId}", request.PlayListId, request.UserId); return Result.Unauthorized(); } @@ -56,8 +56,8 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for PlayList id={PlayListId} and key={Key}.", playListId, key); - return Result.Error($"Failed to publish remove file event for PlayList with id {playListId}."); + logger.LogError(exception, "Failed to publish remove file event for playlist {PlayListId} with key {Key}", playListId, key); + return Result.Error($"Failed to publish remove file event for playlist {playListId}"); } } } diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index 55301ab..d007186 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; @@ -18,7 +18,7 @@ namespace Musify.Application.PlayLists.Handlers return playList is not null ? Result.Success(PlayListResponse.FromEntity(playList)) - : Result.NotFound("Playlist not found."); + : Result.NotFound("Playlist not found"); } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 4666966..326f19b 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; @@ -24,13 +24,13 @@ namespace Musify.Application.PlayLists.Handlers cancellationToken); if (playListEntity is null) { - logger.LogInformation("PlayList with id={PlayListId} not found.", request.PlayListId); + logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); return Result.NotFound(); } if (playListEntity.UserId != request.UserId) { - logger.LogWarning("User with id={UserId} is not the owner of the PlayList with id={PlayListId}.", request.UserId, request.PlayListId); + logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId); return Result.Unauthorized(); } @@ -50,11 +50,11 @@ namespace Musify.Application.PlayLists.Handlers catch (Exception exception) { await Rollback(updatePictureResult.Value, cancellationToken); - logger.LogError(exception, "Failed to update PlayList with id={PlayListId}.", playListEntity.Id); - return Result.Error($"Failed to update PlayList with id {playListEntity.Id}."); + logger.LogError(exception, "Failed to update playlist {PlayListId}", playListEntity.Id); + return Result.Error($"Failed to update playlist {playListEntity.Id}"); } - logger.LogInformation("Successfully updated PlayList with id={PlayListId}.", playListEntity.Id); + logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id); return Result.Success(PlayListResponse.FromEntity(playListEntity)); } @@ -89,9 +89,9 @@ namespace Musify.Application.PlayLists.Handlers if (!uploadResult.IsSuccess) { - logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error?={ErrorMessage}", + logger.LogError("Failed to upload picture for playlist {PlayListId} to storage: {ErrorMessage}", playList.Id, string.Join("; ", uploadResult.Errors)); - return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); + return Result.Error($"Failed to upload playlist {playList.Id} picture"); } playList.OriginalPictureName = pictureName; return Result.Success(pictureKey); @@ -122,8 +122,8 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish UpdatePlayListPictureEvent for PlayList with id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish UpdatePlayListPictureEvent for PlayList with id {playList.Id}."); + logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playList.Id); + return Result.Error($"Failed to publish playlist {playList.Id} picture update event"); } } @@ -152,7 +152,7 @@ namespace Musify.Application.PlayLists.Handlers storageConfiguration.BucketName, pictureKey, cancellationToken); - logger.LogInformation("Rolled back uploaded picture with key={PictureKey} from storage.", pictureKey); + logger.LogInformation("Rolled back uploaded picture {PictureKey} from storage", pictureKey); } async Task PublishRemoveFileEvent(string key, CancellationToken cancellationToken) @@ -166,8 +166,8 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for key={Key}.", key); - return Result.Error($"Failed to publish remove file event for key {key}."); + logger.LogError(exception, "Failed to publish remove file event for key {Key}", key); + return Result.Error($"Failed to publish remove file event for {key}"); } } } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 893f644..60fe524 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; @@ -17,11 +17,10 @@ namespace Musify.Application.Tracks.Handler public async Task> Handle(CreateTrackCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.FindAsync(request.UserId, cancellationToken); - if (userExists is null) { - logger.LogWarning("User with id={UserId} not found.", request.UserId); - return Result.NotFound($"User with id {request.UserId} not found."); + logger.LogWarning("User {UserId} not found", request.UserId); + return Result.NotFound($"User {request.UserId} not found"); } var trackEntity = CreateTrackCommand.ToEntity( @@ -60,7 +59,7 @@ namespace Musify.Application.Tracks.Handler if (!pictureUpload.IsSuccess) { var errors = string.Join(", ", pictureUpload.Errors); - return Result.Error($"Failed to upload picture for track {request.Title}. Errors: {errors}."); + return Result.Error($"Failed to upload picture for {request.Title}"); } var audioUpload = await storageHandler.UploadFileAsync( @@ -74,7 +73,7 @@ namespace Musify.Application.Tracks.Handler { await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); var errors = string.Join(", ", audioUpload.Errors); - return Result.Error($"Failed to upload audio for track {request.Title}. Errors: {errors}."); + return Result.Error($"Failed to upload audio for {request.Title}"); } return Result.Success(); @@ -84,7 +83,7 @@ namespace Musify.Application.Tracks.Handler { await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, pictureKey, cancellationToken); await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, audioKey, cancellationToken); - logger.LogInformation("Rolled back uploaded files={PictureKey}, {AudioKey}.", pictureKey, audioKey); + logger.LogInformation("Rolled back uploaded files {PictureKey}, {AudioKey}", pictureKey, audioKey); } async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken) @@ -110,7 +109,7 @@ namespace Musify.Application.Tracks.Handler } catch (Exception exception) { - logger.LogError(exception, "Failed to publish resize picture event for Track with id={TrackId}.", track.Id); + logger.LogError(exception, "Failed to publish track picture update event for track {TrackId}", track.Id); } } @@ -130,7 +129,7 @@ namespace Musify.Application.Tracks.Handler } catch (Exception exception) { - logger.LogError(exception, "Failed to publish transcode audio event for Track with id={TrackId}.", track.Id); + logger.LogError(exception, "Failed to publish track audio update event for track {TrackId}", track.Id); } } } diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 6d765ea..a18cf24 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; @@ -15,16 +15,15 @@ namespace Musify.Application.Tracks.Handler public async Task Handle(DeleteTrackCommand request, CancellationToken cancellationToken) { var track = await database.Tracks.FindAsync(request.TrackId, cancellationToken); - if (track is null) { - logger.LogInformation("Track with id={TrackId} not found.", request.TrackId); + logger.LogInformation("Track {TrackId} not found", request.TrackId); return Result.NotFound(); } if (track.Id != request.UserId) { - logger.LogInformation("User with id={UserId} is not the owner of track with id={TrackId}.", request.UserId, request.TrackId); + logger.LogWarning("User {UserId} unauthorized to delete track {TrackId}", request.UserId, request.TrackId); return Result.Unauthorized(); } @@ -63,7 +62,7 @@ namespace Musify.Application.Tracks.Handler async Task RemoveFile(string path, CancellationToken cancellationToken) { await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, path, cancellationToken); - logger.LogDebug("File with path={Path} removed from storage.", path); + logger.LogDebug("Removed file {Path} from storage", path); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index 585016f..f085d77 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -14,24 +14,24 @@ namespace Musify.Application.Users.Handlers public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AnyAsync(u => u.Id == request.Id, cancellationToken); - if (!userExists) + if (userExists) { - logger.LogInformation("User with Id={UserId} already exists.", request.Id); - return Result.Conflict($"A user with Id {request.Id} already exists."); + logger.LogInformation("User {UserId} already exists", request.Id); + return Result.Conflict($"User {request.Id} already exists"); } var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); if (!keycloakUser.IsSuccess) { - logger.LogWarning("Keycloak user with Id={UserId} was not found.", request.Id); - return Result.NotFound("Keycloak user not found."); + logger.LogWarning("Keycloak user {UserId} not found", request.Id); + return Result.NotFound("Keycloak user not found"); } var newUser = CreateUserCommand.ToEntity(request); await database.Users.AddAsync(newUser, cancellationToken); await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("User with Id={UserId} created successfully.", newUser.Id); + logger.LogInformation("Created user {UserId}", newUser.Id); return Result.Created(new UserResponse( newUser.Id.ToString(), keycloakUser.Value.Name, diff --git a/Application/Users/Handlers/DeleteUserCommandHandler.cs b/Application/Users/Handlers/DeleteUserCommandHandler.cs index dbab4e7..09b0c94 100644 --- a/Application/Users/Handlers/DeleteUserCommandHandler.cs +++ b/Application/Users/Handlers/DeleteUserCommandHandler.cs @@ -10,7 +10,6 @@ namespace Musify.Application.Users.Commands public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) { var existingUser = await database.Users.FindAsync(request.Id, cancellationToken); - if (existingUser is null) return Result.NotFound(); diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index 44e0008..1332472 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; @@ -15,14 +15,12 @@ namespace Musify.Application.Users.Handlers var user = await database.Users .AsNoTracking() .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - if (user is null) - return Result.NotFound("User not found."); + return Result.NotFound("User not found"); var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) - return Result.NotFound("Keycloak user not found."); + return Result.NotFound("Keycloak user not found"); return Result.Success(new UserResponse( user.Id.ToString(), diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index f8e89cc..d46792d 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -15,14 +15,12 @@ namespace Musify.Application.Users.Handlers var user = await database.Users .AsNoTracking() .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - if (user is null) - return Result.NotFound("User not found."); + return Result.NotFound("User not found"); var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) - return Result.NotFound("Keycloak user not found."); + return Result.NotFound("Keycloak user not found"); return Result.Success(new UserResponse( user.Id.ToString(), diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Audio/AudioTranscoder.cs index cfe5212..bb7cd06 100644 --- a/Infrastructure/Audio/AudioTranscoder.cs +++ b/Infrastructure/Audio/AudioTranscoder.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; @@ -17,11 +17,7 @@ namespace Musify.Infrastructure.Audio { Directory.CreateDirectory(destinationPath); - logger.LogInformation( - "Starting audio transcoding to DASH. DestinationPath={DestinationPath}, Timeout={Timeout}, Executable={Executable}", - destinationPath, - audioTranscoderConfiguration.TranscodingTimeout, - audioTranscoderConfiguration.Ffmpeg.ExecutableName); + logger.LogInformation("Starting audio transcoding to DASH for {DestinationPath}", destinationPath); var executionResult = await ExecuteFfmpegWithInputAsync( BuildDashArguments(destinationPath, audioTranscoderConfiguration.Ffmpeg), @@ -32,25 +28,22 @@ namespace Musify.Infrastructure.Audio if (executionResult.ExitCode != 0) { - logger.LogError( - "Audio transcoding to DASH format failed with exit code={ExitCode}. Standard Output={StandardOutput}, Standard Error={StandardError}", - executionResult.ExitCode, - executionResult.StandardOutput, - executionResult.StandardError); - return Result.Error($"Audio transcoding failed with exit code: {executionResult.ExitCode}. See logs for details."); + 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})"); } - logger.LogInformation("Audio transcoding to DASH format completed successfully. DestinationPath={DestinationPath}", destinationPath); + logger.LogInformation("Audio transcoding to DASH completed for {DestinationPath}", destinationPath); return Result.Success(); } catch (TimeoutException timeoutException) { - logger.LogError(timeoutException, "Audio transcoding timed out after={Timeout}. DestinationPath={DestinationPath}", audioTranscoderConfiguration.TranscodingTimeout, destinationPath); - return Result.Error("Audio transcoding timed out."); + logger.LogError(timeoutException, "Audio transcoding timed out after {Timeout} for {DestinationPath}", audioTranscoderConfiguration.TranscodingTimeout, destinationPath); + return Result.Error("Audio transcoding timed out"); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while transcoding audio to DASH format. DestinationPath={DestinationPath}", destinationPath); + logger.LogError(exception, "Failed to transcode audio to DASH for {DestinationPath}", destinationPath); return Result.Error(exception.Message); } } @@ -59,13 +52,13 @@ namespace Musify.Infrastructure.Audio { if (!File.Exists(filePath)) { - logger.LogWarning("Audio validation requested for a non-existing file. FilePath={FilePath}", filePath); + logger.LogWarning("Audio validation requested for non-existing file {FilePath}", filePath); return Result.Error($"Audio file not found: {filePath}"); } try { - logger.LogInformation("Starting audio file validation. FilePath={FilePath}, Timeout={Timeout}", filePath, audioTranscoderConfiguration.TranscodingTimeout); + logger.LogDebug("Validating audio file {FilePath}", filePath); var executionResult = await ExecuteFfmpegAsync( BuildValidateAudioArguments(filePath), @@ -76,26 +69,23 @@ namespace Musify.Infrastructure.Audio var isValid = executionResult.ExitCode == 0; if (!isValid) { - logger.LogWarning( - "Audio validation failed for file={FilePath}. Standard Error={StandardError}", - filePath, - executionResult.StandardError); + logger.LogWarning("Audio validation failed for {FilePath}: {StandardError}", filePath, executionResult.StandardError); } else { - logger.LogInformation("Audio validation completed successfully. FilePath={FilePath}", filePath); + logger.LogDebug("Audio validation successful for {FilePath}", filePath); } return Result.Success(isValid); } catch (TimeoutException timeoutException) { - logger.LogError(timeoutException, "Audio validation timed out after={Timeout} for file={FilePath}.", audioTranscoderConfiguration.TranscodingTimeout, filePath); + logger.LogError(timeoutException, "Audio validation timed out for {FilePath}", filePath); return Result.Error("Audio validation timed out."); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while validating audio file={FilePath}.", filePath); + logger.LogError(exception, "Failed to validate audio file {FilePath}", filePath); return Result.Error(exception.Message); } } @@ -110,7 +100,7 @@ namespace Musify.Infrastructure.Audio using var process = BuildProcess(arguments, workingDirectory, redirectStandardInput: true); process.Start(); - logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId={ProcessId}, WorkingDirectory={WorkingDirectory}", operationName, process.Id, workingDirectory); + logger.LogDebug("[{Operation}] ffmpeg started (PID: {ProcessId}) in {WorkingDirectory}", operationName, process.Id, workingDirectory); if (inputStream.CanSeek) inputStream.Position = 0; @@ -130,7 +120,7 @@ namespace Musify.Infrastructure.Audio using var process = BuildProcess(arguments, workingDirectory, redirectStandardInput: false); process.Start(); - logger.LogDebug("[{Operation}] ffmpeg process started. ProcessId={ProcessId}, WorkingDirectory={WorkingDirectory}", operationName, process.Id, workingDirectory); + logger.LogDebug("[{Operation}] ffmpeg started (PID: {ProcessId}) in {WorkingDirectory}", operationName, process.Id, workingDirectory); return await WaitForProcessResultAsync(process, operationName, cancellationToken); } @@ -168,11 +158,8 @@ namespace Musify.Infrastructure.Audio } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - logger.LogError( - "[{Operation}] ffmpeg timed out after={Timeout}. ProcessId={ProcessId}", - operationName, - audioTranscoderConfiguration.TranscodingTimeout, - process.Id); + logger.LogError("[{Operation}] ffmpeg timed out after {Timeout} (PID: {ProcessId})", + operationName, audioTranscoderConfiguration.TranscodingTimeout, process.Id); TryKillProcess(process); throw new TimeoutException($"ffmpeg execution exceeded timeout {audioTranscoderConfiguration.TranscodingTimeout}."); @@ -181,13 +168,8 @@ namespace Musify.Infrastructure.Audio var standardError = await errorTask; var standardOutput = await outputTask; - logger.LogDebug( - "[{Operation}] ffmpeg finished. ProcessId={ProcessId}, ExitCode={ExitCode}, StdOutLength={StdOutLength}, StdErrLength={StdErrLength}", - operationName, - process.Id, - process.ExitCode, - standardOutput.Length, - standardError.Length); + logger.LogDebug("[{Operation}] ffmpeg finished (PID: {ProcessId}). Exit code: {ExitCode}, Output: {OutputLength}B, Error: {ErrorLength}B", + operationName, process.Id, process.ExitCode, standardOutput.Length, standardError.Length); return new FfmpegExecutionResult(process.ExitCode, standardOutput, standardError); } @@ -201,7 +183,7 @@ namespace Musify.Infrastructure.Audio } catch (Exception exception) { - logger.LogError(exception, "Failed to kill ffmpeg process. ProcessId={ProcessId}", process.Id); + logger.LogError(exception, "Failed to kill ffmpeg process (PID: {ProcessId})", process.Id); } } diff --git a/Infrastructure/Identity/KeycloakUserService.cs b/Infrastructure/Identity/KeycloakUserService.cs index e725d1e..7efa649 100644 --- a/Infrastructure/Identity/KeycloakUserService.cs +++ b/Infrastructure/Identity/KeycloakUserService.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using Keycloak.Net; using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; @@ -23,12 +23,12 @@ namespace Musify.Infrastructure.Identity } catch (OperationCanceledException) { - logger.LogError("Operation to retrieve Keycloak user with id={KeycloakId} was cancelled.", keycloakId); - return Result.Error("Operation was cancelled."); + logger.LogWarning("Operation to retrieve Keycloak user {KeycloakId} was cancelled", keycloakId); + return Result.Error("Operation cancelled"); } catch (Exception exception) { - logger.LogWarning(exception, "Keycloak user with id={KeycloakId} could not be retrieved.", keycloakId); + logger.LogWarning(exception, "Failed to retrieve Keycloak user {KeycloakId}", keycloakId); return Result.NotFound(); } return Result.Success(KeycloakUserMapper.Map(keycloakUser)); diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index a006bfd..6b0a597 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -38,9 +38,8 @@ namespace Musify.Infrastructure.MassTransit.Activities Directory.CreateDirectory(workingDirectory); - logger.LogDebug("Generated audio workflow paths. WorkingDirectory={WorkingDirectory}, SourceFilePath={SourceFilePath}", - workingDirectory, - sourceFilePath); + logger.LogDebug("Generated audio workflow paths in {WorkingDirectory}", + workingDirectory); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new Dictionary @@ -53,7 +52,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error generating audio workflow paths for key={SourceKey}", + logger.LogError(exception, "Failed to generate audio workflow paths for {SourceKey}", executeContext.Arguments.SourceKey); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 9629e4b..063e641 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -48,7 +48,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!result.IsSuccess) { var errorMessage = string.Join("; ", result.Errors); - logger.LogError("Transcoding failed for file={SourceFilePath}. Errors={ErrorMessage}", + logger.LogError("Transcoding failed for {SourceFilePath}: {Errors}", sourceFilePath, errorMessage); throw new Exception(errorMessage); } @@ -60,7 +60,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error during transcoding activity for file={SourceFilePath}", + logger.LogError(exception, "Failed to transcode {SourceFilePath}", sourceFilePath); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -74,13 +74,13 @@ namespace Musify.Infrastructure.MassTransit.Activities if (Directory.Exists(compensateContext.Log.WorkingDirectory)) Directory.Delete(compensateContext.Log.WorkingDirectory, recursive: true); else - logger.LogWarning("Working directory={WorkingDirectory} does not exist during compensation", + logger.LogWarning("Working directory {WorkingDirectory} not found during compensation", compensateContext.Log.WorkingDirectory); return Task.FromResult(compensateContext.Compensated()); } catch (Exception exception) { - logger.LogError(exception, "Error compensating transcoded audio directory={WorkingDirectory}", compensateContext.Log.WorkingDirectory); + logger.LogError(exception, "Failed to compensate transcoded audio directory {WorkingDirectory}", compensateContext.Log.WorkingDirectory); return Task.FromResult(compensateContext.Failed(exception)); } } diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 9381b48..7521aef 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -39,7 +39,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio if (track is null) { - logger.LogWarning("Track with id={TrackId} not found", + logger.LogWarning("Track {TrackId} not found", executeContext.Arguments.TrackId); throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); } @@ -53,7 +53,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated Track={TrackId} audio", + logger.LogInformation("Updated track {TrackId} audio", executeContext.Arguments.TrackId); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); @@ -61,7 +61,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio } catch (Exception exception) { - logger.LogError(exception, "Error updating Track={TrackId} audio", + logger.LogError(exception, "Failed to update track {TrackId} audio", executeContext.Arguments.TrackId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -90,7 +90,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio } catch (Exception exception) { - logger.LogError(exception, "Error compensating Track={TrackId} audio", compensateContext.Log.TrackId); + logger.LogError(exception, "Failed to compensate track {TrackId} audio", compensateContext.Log.TrackId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 62172f4..2a25174 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -44,7 +44,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!getFile.IsSuccess) { var errorMessage = string.Join("; ", getFile.Errors); - logger.LogWarning("Failed to download file from bucket. Bucket={Bucket}, Key={Key}, Errors={Errors}", + logger.LogWarning("Failed to download {Bucket}/{Key}: {Errors}", executeContext.Arguments.Bucket, executeContext.Arguments.Key, errorMessage); @@ -70,7 +70,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error downloading file from bucket"); + logger.LogError(exception, "Failed to download file from bucket"); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } @@ -85,7 +85,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating downloaded file={DestinationFilePath}", compensateContext.Log.DestinationFilePath); + logger.LogError(exception, "Failed to compensate downloaded file {DestinationFilePath}", compensateContext.Log.DestinationFilePath); return Task.FromResult(compensateContext.Failed(exception)); } } diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 6d889b5..0bf6530 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -40,7 +40,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!removeFile.IsSuccess) { var errorMessage = string.Join("; ", removeFile.Errors); - logger.LogWarning("Failed to remove file={Key} from bucket={Bucket}. Errors={Errors}", + logger.LogWarning("Failed to remove {Key} from bucket {Bucket}: {Errors}", executeContext.Arguments.Key, executeContext.Arguments.Bucket, errorMessage); @@ -51,7 +51,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error removing file={Key} from bucket={Bucket}", + logger.LogError(exception, "Failed to remove {Key} from bucket {Bucket}", executeContext.Arguments.Key, executeContext.Arguments.Bucket); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index c3500c7..81b2101 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -51,7 +51,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!transferFilesResult.IsSuccess) { var errorMessage = string.Join("; ", transferFilesResult.Errors); - logger.LogWarning("Failed to transfer files from={FolderPath} to bucket={DestinationBucket}, key={DestinationKey}. Errors={Errors}", + logger.LogWarning("Failed to transfer files from {FolderPath} to {DestinationBucket}/{DestinationKey}: {Errors}", folderPath, executeContext.Arguments.DestinationBucket, destinationKey, @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error transferring files from={FolderPath} to bucket={DestinationBucket}", + logger.LogError(exception, "Failed to transfer files from {FolderPath} to {DestinationBucket}", folderPath, executeContext.Arguments.DestinationBucket); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); @@ -88,7 +88,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating transferred files to bucket={DestinationBucket}", compensateContext.Log.DestinationBucket); + logger.LogError(exception, "Failed to compensate file transfers to {DestinationBucket}", compensateContext.Log.DestinationBucket); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 60585c4..32a51e6 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using MimeMapping; using Musify.Application.Contracts.Infrastructure; @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files if (!uploadResult.IsSuccess) { var errors = string.Join("; ", uploadResult.Errors); - logger.LogWarning("Failed to upload file to bucket. Bucket={Bucket}, Key={Key}, Errors={Errors}", + logger.LogWarning("Failed to upload file to bucket {Bucket}/{Route}: {Errors}", executeContext.Arguments.DestinationBucket, executeContext.Arguments.DestinationRoute, errors); @@ -60,7 +60,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files } catch (Exception exception) { - logger.LogError(exception, "An error occurred while uploading file to bucket for process={ProcessId} at step={StepId}", processId, stepId); + logger.LogError(exception, "Failed to upload file to bucket for process {ProcessId} step {StepId}", processId, stepId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } @@ -78,7 +78,8 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files if (!uploadResult.IsSuccess) { var errors = string.Join("; ", uploadResult.Errors); - logger.LogError($"Failed to remove file from bucket during compensation. Bucket={compensateContext.Log.DestinationBucket}, Key={compensateContext.Log.DestinationKey}, Errors={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}")); } @@ -86,7 +87,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files } catch (Exception exception) { - logger.LogError(exception, "An error occurred while compensating upload file to bucket activity for bucket={Bucket} and key={Key}", + logger.LogError(exception, "Error compensating file upload for {Bucket}/{Key}", compensateContext.Log.DestinationBucket, compensateContext.Log.DestinationKey); return compensateContext.Failed(exception); diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index f34a7bd..70d1eb8 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -47,9 +47,8 @@ namespace Musify.Infrastructure.MassTransit.Activities Directory.CreateDirectory(workingDirectory); - logger.LogDebug("Generated picture workflow paths. WorkingDirectory={WorkingDirectory}, SourceFilePath={SourceFilePath}", - workingDirectory, - sourceFilePath); + logger.LogDebug("Generated picture workflow paths in {WorkingDirectory}", + workingDirectory); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new Dictionary @@ -64,7 +63,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error generating picture workflow paths for key={SourceKey}", + logger.LogError(exception, "Failed to generate picture workflow paths for {SourceKey}", executeContext.Arguments.SourceKey); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 4768713..710d916 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -41,7 +41,7 @@ namespace Musify.Infrastructure.MassTransit.Activities { if (!File.Exists(sourceFilePath)) { - logger.LogWarning("Source file not found at={SourceFilePath}", sourceFilePath); + logger.LogWarning("Source file not found: {SourceFilePath}", sourceFilePath); throw new FileNotFoundException($"Source file not found: {sourceFilePath}"); } @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (!resizedPicture.IsSuccess) { var errorMessage = string.Join("; ", resizedPicture.Errors); - logger.LogError("Failed to resize picture={SourceFilePath} to={Width}x{Height}. Errors={Errors}", + logger.LogError("Failed to resize picture {SourceFilePath} to {Width}x{Height}: {Errors}", sourceFilePath, executeContext.Arguments.Width, executeContext.Arguments.Height, @@ -71,7 +71,7 @@ namespace Musify.Infrastructure.MassTransit.Activities await using var destinationStream = File.Create(destinationFilePath); await resizedPicture.Value.CopyToAsync(destinationStream, executeContext.CancellationToken); - logger.LogDebug("Picture resized from={SourceFilePath} to={DestinationFilePath}", + logger.LogDebug("Resized picture from {SourceFilePath} to {DestinationFilePath}", sourceFilePath, destinationFilePath); @@ -80,7 +80,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error resizing picture={SourceFilePath}", sourceFilePath); + logger.LogError(exception, "Failed to resize picture {SourceFilePath}", sourceFilePath); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } @@ -95,7 +95,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating resized file={DestinationFilePath}", compensateContext.Log.DestinationFilePath); + logger.LogError(exception, "Failed to compensate resized file {DestinationFilePath}", compensateContext.Log.DestinationFilePath); return Task.FromResult(compensateContext.Failed(exception)); } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 2901432..37e729c 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (playList is null) { - logger.LogWarning("PlayList with id={PlayListId} not found", + logger.LogWarning("Playlist {PlayListId} not found", executeContext.Arguments.PlayListId); throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); } @@ -66,7 +66,7 @@ namespace Musify.Infrastructure.MassTransit.Activities database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated PlayList={PlayListId} pictures", + logger.LogInformation("Updated playlist {PlayListId} pictures", executeContext.Arguments.PlayListId); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); @@ -74,7 +74,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error updating PlayList={PlayListId} pictures", + logger.LogError(exception, "Failed to update playlist {PlayListId} pictures", executeContext.Arguments.PlayListId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -106,7 +106,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating PlayList={PlayListId} pictures", compensateContext.Log.PlayListId); + logger.LogError(exception, "Failed to compensate playlist {PlayListId} pictures", compensateContext.Log.PlayListId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 0ec8e1a..7b2463a 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,4 +1,4 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -46,7 +46,7 @@ namespace Musify.Infrastructure.MassTransit.Activities if (track is null) { - logger.LogWarning("Track with id={TrackId} not found", + logger.LogWarning("Track {TrackId} not found", executeContext.Arguments.TrackId); throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); } @@ -66,7 +66,7 @@ namespace Musify.Infrastructure.MassTransit.Activities database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated Track={TrackId} pictures", + logger.LogInformation("Updated track {TrackId} pictures", executeContext.Arguments.TrackId); await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); @@ -74,7 +74,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error updating Track={TrackId} pictures", + logger.LogError(exception, "Failed to update track {TrackId} pictures", executeContext.Arguments.TrackId); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; @@ -106,7 +106,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } catch (Exception exception) { - logger.LogError(exception, "Error compensating Track={TrackId} pictures", compensateContext.Log.TrackId); + logger.LogError(exception, "Failed to compensate track {TrackId} pictures", compensateContext.Log.TrackId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index 4bbf97c..b1ab220 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using SixLabors.ImageSharp; @@ -13,7 +13,7 @@ namespace Musify.Infrastructure.Pictures { try { - logger.LogDebug("Resizing picture to={Width}x{Height}", width, height); + logger.LogDebug("Resizing picture to {Width}x{Height}", width, height); if (pictureStream.CanSeek && pictureStream.Position > 0) pictureStream.Position = 0; @@ -31,13 +31,13 @@ namespace Musify.Infrastructure.Pictures memoryStream.Position = 0; - logger.LogDebug("Picture resized successfully. OutputLength={Length}", memoryStream.Length); + logger.LogDebug("Picture resized to {Width}x{Height}, length: {Length}", width, height, memoryStream.Length); return Result.Success(memoryStream); } catch (Exception exception) { - logger.LogError(exception, "An error occurred while resizing the picture to={Width}x{Height}.", width, height); - return Result.Error("An error occurred while resizing the picture."); + logger.LogError(exception, "Failed to resize picture to {Width}x{Height}", width, height); + return Result.Error("Failed to resize picture"); } } } diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index 3293281..4a92921 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -1,4 +1,4 @@ -using Amazon.S3; +using Amazon.S3; using Amazon.S3.Model; using Amazon.S3.Transfer; using Ardalis.Result; @@ -21,19 +21,19 @@ namespace Musify.Infrastructure.Storage Key = key, }; - logger.LogDebug("Getting file from S3. Bucket={Bucket}, Key={Key}", bucket, key); + try { var response = await amazonS3.GetObjectAsync(request, cancellationToken); - logger.LogDebug("Successfully got file from S3 with bucket={Bucket} and key={Key}", bucket, key); + logger.LogDebug("Retrieved file from S3 {Bucket}/{Key}", bucket, key); return Result.Success(response.ResponseStream); } catch (Exception exception) { - logger.LogWarning(exception, "Failed to get file from S3. Bucket={Bucket}, Key={Key}", bucket, key); - return Result.NotFound($"File not found in S3 with bucket: {bucket} and key: {key}"); + logger.LogWarning(exception, "Failed to get file from S3 {Bucket}/{Key}", bucket, key); + return Result.NotFound($"File {bucket}/{key} not found"); } } @@ -47,7 +47,7 @@ namespace Musify.Infrastructure.Storage Protocol = storageClientConfiguration.UseHttp ? Protocol.HTTP : Protocol.HTTPS }; - logger.LogDebug("Getting pre-signed URL from S3. Bucket={BucketName}, Key={Key}, Expires={Expires}", bucket, key, request.Expires); + logger.LogDebug("Generating pre-signed URL for {Bucket}/{Key}", bucket, key); var url = await amazonS3.GetPreSignedURLAsync(request); return url; @@ -61,7 +61,7 @@ namespace Musify.Infrastructure.Storage Key = key, }; - logger.LogDebug("Removing file from S3. Bucket={Bucket}, Key={Key}", bucket, key); + logger.LogDebug("Removing file from S3 {Bucket}/{Key}", bucket, key); var response = await amazonS3.DeleteObjectAsync(request, cancellationToken); return Result.NoContent(); @@ -96,18 +96,18 @@ namespace Musify.Infrastructure.Storage await trasnsferUtility.UploadAsync(filePath, bucket, key, cancellationToken); upload.State = UploadState.Successful; successCount++; - logger.LogDebug("Transferred file to S3. Bucket={Bucket}, Key={Key}", bucket, key); + logger.LogDebug("Transferred file to S3 {Bucket}/{Key}", bucket, key); } catch (Exception exception) { failedCount++; upload.State = UploadState.Failed; - logger.LogError(exception, "Failed to transfer file to S3. Bucket={Bucket}, key={key}", bucket, key); + logger.LogError(exception, "Failed to transfer file to S3 {Bucket}/{Key}", bucket, key); } } await database.SaveChangesAsync(cancellationToken); - logger.LogInformation("TransferFiles completed. Bucket={Bucket}, Route={Route}, Success={SuccessCount}, Failed={FailedCount}", bucket, route, successCount, failedCount); + logger.LogInformation("File transfer completed to {Bucket}/{Route}. Success: {SuccessCount}, Failed: {FailedCount}", bucket, route, successCount, failedCount); return Result.Success(); } @@ -122,8 +122,8 @@ namespace Musify.Infrastructure.Storage } catch (Exception exception) { - logger.LogError(exception, "Failed to upload file to S3 with bucket={Bucket} and key={Key}", bucket, key); - return Result.Error($"Failed to upload file to S3 with bucket: {bucket} and key: {key}"); + logger.LogError(exception, "Failed to upload file to S3 {Bucket}/{Key}", bucket, key); + return Result.Error($"Failed to upload to {bucket}/{key}"); } } @@ -149,18 +149,18 @@ namespace Musify.Infrastructure.Storage try { var response = await amazonS3.PutObjectAsync(request, cancellationToken); - logger.LogDebug("Uploaded file to S3. Bucket?={Bucket}, Key={Key}", bucket, key); + logger.LogDebug("Uploaded file to S3 {Bucket}/{Key}", bucket, key); } catch (Exception exception) { - logger.LogError(exception, "Failed to upload file to S3 with bucket={Bucket} and key={Key}", bucket, key); + logger.LogError(exception, "Failed to upload file to S3 {Bucket}/{Key}", bucket, key); upload.State = UploadState.Failed; - return Result.Error($"Failed to upload file to S3 with bucket: {bucket} and key: {key}"); + return Result.Error($"Failed to upload to {bucket}/{key}"); } upload.State = UploadState.Successful; await database.SaveChangesAsync(cancellationToken); - logger.LogDebug("Saved upload record to database. Bucket={Bucket}, Key={Key}", bucket, key); + logger.LogDebug("Saved upload record for {Bucket}/{Key}", bucket, key); return Result.Success(key); } @@ -179,15 +179,15 @@ namespace Musify.Infrastructure.Storage try { var response = await amazonS3.CopyObjectAsync(request, cancellationToken); - logger.LogInformation("Successfully copied file from S3 with bucket={SourceBucket} and key={SourceKey} to bucket={DestinationBucket} and key={DestinationKey}", + 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 S3 with bucket={SourceBucket} and key={SourceKey} to bucket={DestinationBucket} and key={DestinationKey}", + logger.LogError(exception, "Failed to copy file from {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}", sourceBucket, sourceKey, destinationBucket, destinationKey); - return Result.Error($"Failed to copy file from S3 with bucket: {sourceBucket} and key: {sourceKey} to bucket: {destinationBucket} and key: {destinationKey}"); + return Result.Error($"Failed to copy from {sourceBucket}/{sourceKey} to {destinationBucket}/{destinationKey}"); } } } From c6f919ee898a25ae1cc9a8f64cde60d7f2abb233 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 00:59:27 +0100 Subject: [PATCH 104/172] Spaces --- Infrastructure/Storage/StorageHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index 4a92921..5b7c6f1 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -21,8 +21,6 @@ namespace Musify.Infrastructure.Storage Key = key, }; - - try { var response = await amazonS3.GetObjectAsync(request, cancellationToken); From 697eed4e587895a48f824cd78c5b7801ba3f9b55 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 17:15:39 +0200 Subject: [PATCH 105/172] Experimental changes --- .../Infrastructure/IAudioTranscoder.cs | 11 --- .../Infrastructure/IAudioTranscoderService.cs | 9 +++ .../Infrastructure/IKeycloakUserService.cs | 5 +- .../Infrastructure/IPictureHandler.cs | 9 --- .../Infrastructure/IPictureService.cs | 7 ++ .../Infrastructure/IStorageHandler.cs | 22 ----- .../Infrastructure/IStorageService.cs | 20 +++++ .../Handlers/CreatePlayListCommandHandler.cs | 14 ++-- .../Handlers/UpdatePlayListCommandHandler.cs | 25 +++--- .../Handler/CreateTrackCommandHandler.cs | 41 +++++----- .../Handler/DeleteTrackCommandHandler.cs | 2 +- .../Handlers/CreateUserCommandHandler.cs | 15 ++-- .../Users/Handlers/GetUserByIdQueryHandler.cs | 18 +++-- .../Handlers/GetUserByKeycloakQueryHandler.cs | 16 ++-- .../AudioTranscoderDependencyInjection.cs | 14 ---- .../ConfigurationDependencyInjection.cs | 40 --------- ...uration.cs => MassTransitConfiguration.cs} | 2 +- .../Identity/KeycloakDependencyInjection.cs | 30 ------- Infrastructure/Identity/KeycloakUserMapper.cs | 17 ---- .../InfrastructureDependencyInjection.cs | 50 ------------ .../Audio/TranscodeDashAudioActivity.cs | 12 +-- .../Files/DownloadFileFromBucketActivity.cs | 15 +--- .../Files/RemoveFileFromBucketActivity.cs | 13 +-- .../Files/TransferFilesToBucketActivity.cs | 15 +--- .../Files/UploadFileToBucketActivity.cs | 25 +----- .../Pictures/ResizePictureActivity.cs | 15 +--- .../MassTransitDependencyInjection.cs | 6 +- .../DatabaseDependencyInjection.cs | 10 +++ .../Pictures/PictureDependencyInjection.cs | 14 ---- .../AudioTranscoderService.cs} | 36 ++++----- .../KeycloakUserService.cs | 22 +++-- .../PictureService.cs} | 16 ++-- .../Services/ServicesDependencyInjection.cs | 81 +++++++++++++++++++ .../StorageService.cs} | 49 +++++------ .../Storage/StorageDependencyInjection.cs | 28 ------- 35 files changed, 279 insertions(+), 445 deletions(-) delete mode 100644 Application/Contracts/Infrastructure/IAudioTranscoder.cs create mode 100644 Application/Contracts/Infrastructure/IAudioTranscoderService.cs delete mode 100644 Application/Contracts/Infrastructure/IPictureHandler.cs create mode 100644 Application/Contracts/Infrastructure/IPictureService.cs delete mode 100644 Application/Contracts/Infrastructure/IStorageHandler.cs create mode 100644 Application/Contracts/Infrastructure/IStorageService.cs delete mode 100644 Infrastructure/Audio/AudioTranscoderDependencyInjection.cs delete mode 100644 Infrastructure/Configuration/ConfigurationDependencyInjection.cs rename Infrastructure/Configuration/{MessagingConfiguration.cs => MassTransitConfiguration.cs} (90%) delete mode 100644 Infrastructure/Identity/KeycloakDependencyInjection.cs delete mode 100644 Infrastructure/Identity/KeycloakUserMapper.cs delete mode 100644 Infrastructure/InfrastructureDependencyInjection.cs delete mode 100644 Infrastructure/Pictures/PictureDependencyInjection.cs rename Infrastructure/{Audio/AudioTranscoder.cs => Services/AudioTranscoderService.cs} (88%) rename Infrastructure/{Identity => Services}/KeycloakUserService.cs (68%) rename Infrastructure/{Pictures/PictureHandler.cs => Services/PictureService.cs} (74%) create mode 100644 Infrastructure/Services/ServicesDependencyInjection.cs rename Infrastructure/{Storage/StorageHandler.cs => Services/StorageService.cs} (74%) delete mode 100644 Infrastructure/Storage/StorageDependencyInjection.cs diff --git a/Application/Contracts/Infrastructure/IAudioTranscoder.cs b/Application/Contracts/Infrastructure/IAudioTranscoder.cs deleted file mode 100644 index f302d1b..0000000 --- a/Application/Contracts/Infrastructure/IAudioTranscoder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ardalis.Result; - -namespace Musify.Application.Contracts.Infrastructure -{ - public interface IAudioTranscoder - { - Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); - - Task> IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IAudioTranscoderService.cs b/Application/Contracts/Infrastructure/IAudioTranscoderService.cs new file mode 100644 index 0000000..3c74cd5 --- /dev/null +++ b/Application/Contracts/Infrastructure/IAudioTranscoderService.cs @@ -0,0 +1,9 @@ +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IAudioTranscoderService + { + Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); + + Task IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IKeycloakUserService.cs b/Application/Contracts/Infrastructure/IKeycloakUserService.cs index f78c4a3..d11728b 100644 --- a/Application/Contracts/Infrastructure/IKeycloakUserService.cs +++ b/Application/Contracts/Infrastructure/IKeycloakUserService.cs @@ -1,11 +1,10 @@ -using Ardalis.Result; -using Musify.Application.Users.Responses; +using Musify.Application.Users.Responses; namespace Musify.Application.Contracts.Infrastructure { public interface IKeycloakUserService { - Task> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken); + Task GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken); Task> GetUsersAsync( CancellationToken cancellationToken, diff --git a/Application/Contracts/Infrastructure/IPictureHandler.cs b/Application/Contracts/Infrastructure/IPictureHandler.cs deleted file mode 100644 index 2662fc1..0000000 --- a/Application/Contracts/Infrastructure/IPictureHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ardalis.Result; - -namespace Musify.Application.Contracts.Infrastructure -{ - public interface IPictureHandler - { - Task> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IPictureService.cs b/Application/Contracts/Infrastructure/IPictureService.cs new file mode 100644 index 0000000..66bc445 --- /dev/null +++ b/Application/Contracts/Infrastructure/IPictureService.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IPictureService + { + Task ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs deleted file mode 100644 index cb5d2df..0000000 --- a/Application/Contracts/Infrastructure/IStorageHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Ardalis.Result; - -namespace Musify.Application.Contracts.Infrastructure -{ - public interface IStorageHandler - { - Task> GetFileAsync(string bucket, string key, CancellationToken cancellationToken); - - Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); - - Task> UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken); - - Task> UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken); - - Task TransferFilesAsync(string sourceDirectory, string bucket, string route, CancellationToken cancellationToken); - - Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken); - - Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, - CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IStorageService.cs b/Application/Contracts/Infrastructure/IStorageService.cs new file mode 100644 index 0000000..beb5c85 --- /dev/null +++ b/Application/Contracts/Infrastructure/IStorageService.cs @@ -0,0 +1,20 @@ +namespace Musify.Application.Contracts.Infrastructure +{ + public interface IStorageService + { + Task GetFileAsync(string bucket, string key, CancellationToken cancellationToken); + + Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); + + Task UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken); + + Task UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken); + + Task TransferFilesAsync(string sourceDirectory, string bucket, string route, CancellationToken cancellationToken); + + Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken); + + Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 2ca01c9..0df4aa5 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -13,7 +13,7 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { - public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, + public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { @@ -80,16 +80,16 @@ namespace Musify.Application.PlayLists.Handlers var pictureName = Guid.NewGuid() + picture.FileType; var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); - var uploadResult = await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.BucketName, pictureKey, cancellationToken); - if (!uploadResult.IsSuccess) + try { - logger.LogError("Failed to upload picture for playlist {PlayListId} to storage: {ErrorMessage}", - playList.Id, string.Join(", ", uploadResult.Errors)); - return uploadResult.ToErrorResult(); + await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.BucketName, pictureKey, cancellationToken); + } + catch + { + return Result.Error($"An error occurred while uploading picture for playlist {playList.Id} to storage."); } playList.OriginalPictureName = pictureName; - return Result.Success(); } diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 326f19b..965edaf 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -12,7 +12,7 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { - public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageHandler storageHandler, + public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > @@ -80,19 +80,20 @@ namespace Musify.Application.PlayLists.Handlers var pictureName = Guid.NewGuid() + newPicture.FileType; var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); - var uploadResult = await storageHandler.UploadFileAsync( - newPicture.FileStream, - newPicture.ContentType, - storageConfiguration.BucketName, - pictureKey, - cancellationToken); - - if (!uploadResult.IsSuccess) + try { - logger.LogError("Failed to upload picture for playlist {PlayListId} to storage: {ErrorMessage}", - playList.Id, string.Join("; ", uploadResult.Errors)); - return Result.Error($"Failed to upload playlist {playList.Id} picture"); + await storageHandler.UploadFileAsync( + newPicture.FileStream, + newPicture.ContentType, + storageConfiguration.BucketName, + pictureKey, + cancellationToken); } + catch + { + return Result.Error("Failed to upload picture to storage"); + } + playList.OriginalPictureName = pictureName; return Result.Success(pictureKey); } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 60fe524..3a16270 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -10,7 +10,7 @@ using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler { - public class CreateTrackCommandHandler(IDatabase database, IEventBus eventBus, IStorageHandler storageHandler, + public class CreateTrackCommandHandler(IDatabase database, IEventBus eventBus, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) : IRequestHandler >> { @@ -49,33 +49,34 @@ namespace Musify.Application.Tracks.Handler string audioKey, CancellationToken cancellationToken) { - var pictureUpload = await storageHandler.UploadFileAsync( - request.Picture.FileStream, - request.Picture.ContentType, - storageConfiguration.BucketName, - pictureKey, - cancellationToken); - - if (!pictureUpload.IsSuccess) + try + { + await storageHandler.UploadFileAsync( + request.Picture.FileStream, + request.Picture.ContentType, + storageConfiguration.BucketName, + pictureKey, + cancellationToken); + } + catch { - var errors = string.Join(", ", pictureUpload.Errors); return Result.Error($"Failed to upload picture for {request.Title}"); } - var audioUpload = await storageHandler.UploadFileAsync( - request.Audio.FileStream, - request.Audio.ContentType, - storageConfiguration.BucketName, - audioKey, - cancellationToken); - - if (!audioUpload.IsSuccess) + try + { + await storageHandler.UploadFileAsync( + request.Audio.FileStream, + request.Audio.ContentType, + storageConfiguration.BucketName, + audioKey, + cancellationToken); + } + catch { await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); - var errors = string.Join(", ", audioUpload.Errors); return Result.Error($"Failed to upload audio for {request.Title}"); } - return Result.Success(); } diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index a18cf24..fd57090 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -8,7 +8,7 @@ using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler { - public class DeleteTrackCommandHandler(IDatabase database, IStorageHandler storageHandler, ILogger logger, + public class DeleteTrackCommandHandler(IDatabase database, IStorageService storageHandler, ILogger logger, TrackConfiguration trackConfiguration, ApplicationStorageConfiguration storageConfiguration) : IRequestHandler> { diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index f085d77..fab1b64 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -20,10 +20,13 @@ namespace Musify.Application.Users.Handlers return Result.Conflict($"User {request.Id} already exists"); } - var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) + KeycloakUserResponse keycloakUser; + try + { + keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); + } + catch { - logger.LogWarning("Keycloak user {UserId} not found", request.Id); return Result.NotFound("Keycloak user not found"); } @@ -34,9 +37,9 @@ namespace Musify.Application.Users.Handlers logger.LogInformation("Created user {UserId}", newUser.Id); return Result.Created(new UserResponse( newUser.Id.ToString(), - keycloakUser.Value.Name, - keycloakUser.Value.FirstName, - keycloakUser.Value.SecondName, + keycloakUser.Name, + keycloakUser.FirstName, + keycloakUser.SecondName, newUser.CreatedAt, newUser.UpdatedAt)); } diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index 1332472..47dd1ce 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -18,15 +18,21 @@ namespace Musify.Application.Users.Handlers if (user is null) return Result.NotFound("User not found"); - var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) - return Result.NotFound("Keycloak user not found"); + KeycloakUserResponse keycloakUser; + try + { + keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); + } + catch + { + return Result.Error("Keycloak user not found"); + } return Result.Success(new UserResponse( user.Id.ToString(), - keycloakUser.Value.Name, - keycloakUser.Value.FirstName, - keycloakUser.Value.SecondName, + keycloakUser.Name, + keycloakUser.FirstName, + keycloakUser.SecondName, user.CreatedAt, user.UpdatedAt)); } diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index d46792d..b8e59f0 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -18,15 +18,21 @@ namespace Musify.Application.Users.Handlers if (user is null) return Result.NotFound("User not found"); - var keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) + KeycloakUserResponse keycloakUser; + try + { + keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); + } + catch + { return Result.NotFound("Keycloak user not found"); + } return Result.Success(new UserResponse( user.Id.ToString(), - keycloakUser.Value.Name, - keycloakUser.Value.FirstName, - keycloakUser.Value.SecondName, + keycloakUser.Name, + keycloakUser.FirstName, + keycloakUser.SecondName, user.CreatedAt, user.UpdatedAt)); } diff --git a/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs b/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs deleted file mode 100644 index f7e04c2..0000000 --- a/Infrastructure/Audio/AudioTranscoderDependencyInjection.cs +++ /dev/null @@ -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(); - return serviceDescriptors; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs deleted file mode 100644 index b89a9eb..0000000 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ /dev/null @@ -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(configuration, DatabaseConfiguration.SectionName) - .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) - .AddValidatedConfiguration(configuration, InfrastructureStorageConfiguration.SectionName) - .AddValidatedConfiguration(configuration, MessagingConfiguration.SectionName) - .AddValidatedConfiguration(configuration, OpenTelemetryConfiguration.SectionName) - .AddValidatedConfiguration(configuration, AudioTranscoderConfiguration.SectionName) - .AddValidatedConfiguration(configuration, WorkerConfiguration.SectionName); - - return serviceDescriptors; - } - - static IServiceCollection AddValidatedConfiguration( - this IServiceCollection serviceDescriptors, - IConfiguration configuration, - string sectionName) - where TConfiguration : class - { - serviceDescriptors - .AddOptionsWithValidateOnStart() - .Bind(configuration.GetRequiredSection(sectionName)) - .ValidateDataAnnotations(); - - serviceDescriptors.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService>().Value); - - return serviceDescriptors; - } - } -} diff --git a/Infrastructure/Configuration/MessagingConfiguration.cs b/Infrastructure/Configuration/MassTransitConfiguration.cs similarity index 90% rename from Infrastructure/Configuration/MessagingConfiguration.cs rename to Infrastructure/Configuration/MassTransitConfiguration.cs index 045ea0c..e92ccd6 100644 --- a/Infrastructure/Configuration/MessagingConfiguration.cs +++ b/Infrastructure/Configuration/MassTransitConfiguration.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; namespace Musify.Infrastructure.Configuration { - public class MessagingConfiguration + public class MassTransitConfiguration { public const string SectionName = "Messaging"; diff --git a/Infrastructure/Identity/KeycloakDependencyInjection.cs b/Infrastructure/Identity/KeycloakDependencyInjection.cs deleted file mode 100644 index ff03f43..0000000 --- a/Infrastructure/Identity/KeycloakDependencyInjection.cs +++ /dev/null @@ -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() - .Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName)) - .ValidateDataAnnotations(); - - serviceDescriptors.AddScoped(serviceProvider => - { - var keycloakConfiguration = serviceProvider.GetRequiredService(); - - return new KeycloakClient( - keycloakConfiguration.BaseUrl, - keycloakConfiguration.Username, - keycloakConfiguration.Password); - } ); - serviceDescriptors.AddScoped(); - return serviceDescriptors; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakUserMapper.cs b/Infrastructure/Identity/KeycloakUserMapper.cs deleted file mode 100644 index fdac511..0000000 --- a/Infrastructure/Identity/KeycloakUserMapper.cs +++ /dev/null @@ -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.")); - } - } -} \ No newline at end of file diff --git a/Infrastructure/InfrastructureDependencyInjection.cs b/Infrastructure/InfrastructureDependencyInjection.cs deleted file mode 100644 index d460c83..0000000 --- a/Infrastructure/InfrastructureDependencyInjection.cs +++ /dev/null @@ -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() - .AddScoped(); - - return serviceDescriptors; - } - - public static IServiceCollection AddInfrastructureWorkerModules(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - serviceDescriptors - .AddInfrastructureConfigurations(configuration) - .AddDatabase(configuration) - .AddStorageHandler() - .AddPictureHandler() - .AddAudioTranscoder() - .AddInfrastructureOpenTelemetry() - .AddMassTransitConsumers() - .AddScoped() - .AddScoped(); - - return serviceDescriptors; - } - } -} diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 063e641..c14fe92 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Logs; namespace Musify.Infrastructure.MassTransit.Activities { public class TranscodeDashAudioActivity( - IAudioTranscoder audioTranscoder, + IAudioTranscoderService audioTranscoder, ILogger logger, IProcessTrackingStore processTrackingStore) : IActivity @@ -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); diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 2a25174..df6bd52 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Logs; namespace Musify.Infrastructure.MassTransit.Activities { public class DownloadFileFromBucketActivity( - IStorageHandler storageHandler, + IStorageService storageHandler, ILogger logger, IProcessTrackingStore processTrackingStore) : IActivity @@ -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; diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 0bf6530..e1ddbf8 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -7,7 +7,7 @@ using Musify.Infrastructure.MassTransit.Activities.Arguments; namespace Musify.Infrastructure.MassTransit.Activities { public class RemoveFileFromBucketActivity( - IStorageHandler storageHandler, + IStorageService storageHandler, ILogger logger, IProcessTrackingStore processTrackingStore) : IExecuteActivity @@ -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(); } diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 81b2101..f0bc572 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -8,7 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Logs; namespace Musify.Infrastructure.MassTransit.Activities { public class TransferFilesToBucketActivity( - IStorageHandler storageHandler, + IStorageService storageHandler, ILogger logger, IProcessTrackingStore processTrackingStore) : IActivity @@ -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(); } diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 32a51e6..99849c1 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -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 logger, IProcessTrackingStore processTrackingStore) : IActivity @@ -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) diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 710d916..c264d41 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -9,7 +9,7 @@ using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; namespace Musify.Infrastructure.MassTransit.Activities { public class ResizePictureActivity( - IPictureHandler pictureHandler, + IPictureService pictureHandler, ILogger logger, IProcessTrackingStore processTrackingStore) : IActivity @@ -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, diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 2da7293..d55724b 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -23,7 +23,7 @@ namespace Musify.Infrastructure.MassTransit options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => ConfigureRabbitMqHost( busFactoryConfigurator, - busRegistrationContext.GetRequiredService())); + busRegistrationContext.GetRequiredService())); } ); return serviceDescriptors; @@ -60,7 +60,7 @@ namespace Musify.Infrastructure.MassTransit { ConfigureRabbitMqHost( busFactoryConfigurator, - busRegistrationContext.GetRequiredService()); + busRegistrationContext.GetRequiredService()); 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 => { diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index 4152976..0a3f01c 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -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() + .Bind(configuration.GetRequiredSection(DatabaseConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + serviceDescriptors.AddDbContext(); serviceDescriptors.AddScoped(serviceProvider => serviceProvider.GetRequiredService()); diff --git a/Infrastructure/Pictures/PictureDependencyInjection.cs b/Infrastructure/Pictures/PictureDependencyInjection.cs deleted file mode 100644 index 4d955c7..0000000 --- a/Infrastructure/Pictures/PictureDependencyInjection.cs +++ /dev/null @@ -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(); - return serviceDescriptors; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Audio/AudioTranscoder.cs b/Infrastructure/Services/AudioTranscoderService.cs similarity index 88% rename from Infrastructure/Audio/AudioTranscoder.cs rename to Infrastructure/Services/AudioTranscoderService.cs index bb7cd06..48c38d8 100644 --- a/Infrastructure/Audio/AudioTranscoder.cs +++ b/Infrastructure/Services/AudioTranscoderService.cs @@ -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 logger, + public class AudioTranscoderService( + ILogger logger, AudioTranscoderConfiguration audioTranscoderConfiguration) - : IAudioTranscoder + : IAudioTranscoderService { - public async Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken) + record FfmpegExecutionResult(int ExitCode, string StandardOutput, string StandardError); + + public async Task 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> IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken) + public async Task IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken) { if (!File.Exists(filePath)) { logger.LogWarning("Audio validation requested for non-existing file {FilePath}", filePath); - return Result.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.Success(isValid); + return isValid; } catch (TimeoutException timeoutException) { logger.LogError(timeoutException, "Audio validation timed out for {FilePath}", filePath); - return Result.Error("Audio validation timed out."); } catch (Exception exception) { logger.LogError(exception, "Failed to validate audio file {FilePath}", filePath); - return Result.Error(exception.Message); } + return false; } async Task 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); } } \ No newline at end of file diff --git a/Infrastructure/Identity/KeycloakUserService.cs b/Infrastructure/Services/KeycloakUserService.cs similarity index 68% rename from Infrastructure/Identity/KeycloakUserService.cs rename to Infrastructure/Services/KeycloakUserService.cs index 7efa649..681f870 100644 --- a/Infrastructure/Identity/KeycloakUserService.cs +++ b/Infrastructure/Services/KeycloakUserService.cs @@ -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 logger) : IKeycloakUserService { - public async Task> GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken) + public async Task 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> 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.")); + } } } \ No newline at end of file diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Services/PictureService.cs similarity index 74% rename from Infrastructure/Pictures/PictureHandler.cs rename to Infrastructure/Services/PictureService.cs index b1ab220..f50f9ba 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Services/PictureService.cs @@ -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 logger) - : IPictureHandler + public class PictureService(ILogger logger) + : IPictureService { - public async Task> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) + public async Task 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.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; } } } diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs new file mode 100644 index 0000000..e9cba44 --- /dev/null +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -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() + .Bind(configuration.GetRequiredSection(InfrastructureStorageConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + serviceDescriptors.AddScoped(serviceProvider => + { + var configuration = serviceProvider.GetRequiredService(); + var s3Configuration = new AmazonS3Config + { + ServiceURL = configuration.ServiceUrl, + ForcePathStyle = configuration.ForcePathStyle, + UseHttp = configuration.UseHttp, + }; + return new AmazonS3Client(configuration.AccessKey, configuration.SecretAccessKey, s3Configuration); + } ); + + serviceDescriptors.AddScoped(); + return serviceDescriptors; + } + + public static IServiceCollection AddPictureService(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors.AddScoped(); + return serviceDescriptors; + } + + public static IServiceCollection AddKeycloakService(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors.AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + serviceDescriptors.AddScoped(serviceProvider => + { + var keycloakConfiguration = serviceProvider.GetRequiredService(); + + return new KeycloakClient( + keycloakConfiguration.BaseUrl, + keycloakConfiguration.Username, + keycloakConfiguration.Password); + } ); + serviceDescriptors.AddScoped(); + return serviceDescriptors; + } + + public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(AudioTranscoderConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + serviceDescriptors.AddScoped(); + return serviceDescriptors; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Services/StorageService.cs similarity index 74% rename from Infrastructure/Storage/StorageHandler.cs rename to Infrastructure/Services/StorageService.cs index 5b7c6f1..8f19a6d 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Services/StorageService.cs @@ -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 logger) - : IStorageHandler + public class StorageService(IDatabase database, IAmazonS3 amazonS3, InfrastructureStorageConfiguration storageClientConfiguration, ILogger logger) + : IStorageService { - public async Task> GetFileAsync(string bucket, string key, CancellationToken cancellationToken) + public async Task 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 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 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> 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> 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 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; } } } diff --git a/Infrastructure/Storage/StorageDependencyInjection.cs b/Infrastructure/Storage/StorageDependencyInjection.cs deleted file mode 100644 index 5e2f4ae..0000000 --- a/Infrastructure/Storage/StorageDependencyInjection.cs +++ /dev/null @@ -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(serviceProvider => - { - var configuration = serviceProvider.GetRequiredService(); - var s3Configuration = new AmazonS3Config - { - ServiceURL = configuration.ServiceUrl, - ForcePathStyle = configuration.ForcePathStyle, - UseHttp = configuration.UseHttp, - }; - return new AmazonS3Client(configuration.AccessKey, configuration.SecretAccessKey, s3Configuration); - } ); - - serviceDescriptors.AddScoped(); - return serviceDescriptors; - } - } -} \ No newline at end of file From 32582b6019669e1e2aa53d525162a36c5edab31e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 18:21:56 +0200 Subject: [PATCH 106/172] Map --- Application/Extensions/ResultExtensions.cs | 24 ------------------- .../Handlers/UpdatePlayListCommandHandler.cs | 3 +-- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 Application/Extensions/ResultExtensions.cs diff --git a/Application/Extensions/ResultExtensions.cs b/Application/Extensions/ResultExtensions.cs deleted file mode 100644 index d4f0ad5..0000000 --- a/Application/Extensions/ResultExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Ardalis.Result; - -namespace Musify.Application.Extensions -{ - public static class ResultExtensions - { - public static Result ToErrorResult(this Result result) - { - var newResult = result.Status switch - { - ResultStatus.Error => Result.Error(new ErrorList(result.Errors)), - ResultStatus.Forbidden => Result.Forbidden(result.Errors.ToArray()), - ResultStatus.Unauthorized => Result.Unauthorized(result.Errors.ToArray()), - ResultStatus.Invalid => Result.Invalid(result.ValidationErrors.ToArray()), - ResultStatus.NotFound => Result.NotFound(result.Errors.ToArray()), - ResultStatus.Conflict => Result.Conflict(result.Errors.ToArray()), - ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), - ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), - _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported.") - }; - return newResult; - } - } -} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 965edaf..5e05081 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -5,7 +5,6 @@ using Musify.Application.Configuration; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; -using Musify.Application.Extensions; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -38,7 +37,7 @@ namespace Musify.Application.PlayLists.Handlers UpdateDescription(playListEntity, request.NewDescription); var updatePictureResult = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); if (!updatePictureResult.IsSuccess) - return updatePictureResult.ToErrorResult(); + return updatePictureResult.Map(_ => PlayListResponse.FromEntity(playListEntity)); try { From aef2031cf5b57bb0b12d2055e90b9937f3bfd1cf Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 18:23:13 +0200 Subject: [PATCH 107/172] Format --- Infrastructure/Services/AudioTranscoderService.cs | 3 +-- Infrastructure/Services/KeycloakUserService.cs | 6 ++---- Infrastructure/Services/StorageService.cs | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Infrastructure/Services/AudioTranscoderService.cs b/Infrastructure/Services/AudioTranscoderService.cs index 48c38d8..1bec040 100644 --- a/Infrastructure/Services/AudioTranscoderService.cs +++ b/Infrastructure/Services/AudioTranscoderService.cs @@ -5,8 +5,7 @@ using System.Diagnostics; namespace Musify.Infrastructure.Services { - public class AudioTranscoderService( - ILogger logger, + public class AudioTranscoderService(ILogger logger, AudioTranscoderConfiguration audioTranscoderConfiguration) : IAudioTranscoderService { diff --git a/Infrastructure/Services/KeycloakUserService.cs b/Infrastructure/Services/KeycloakUserService.cs index 681f870..8705aeb 100644 --- a/Infrastructure/Services/KeycloakUserService.cs +++ b/Infrastructure/Services/KeycloakUserService.cs @@ -7,10 +7,8 @@ using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Services { - public class KeycloakUserService( - KeycloakClient keycloakClient, - KeycloakConfiguration keycloakConfiguration, - ILogger logger) : IKeycloakUserService + public class KeycloakUserService(KeycloakClient keycloakClient, ILogger logger, + KeycloakConfiguration keycloakConfiguration) : IKeycloakUserService { public async Task GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken) { diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 8f19a6d..ee5faf1 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -9,7 +9,8 @@ using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Services { - public class StorageService(IDatabase database, IAmazonS3 amazonS3, InfrastructureStorageConfiguration storageClientConfiguration, ILogger logger) + public class StorageService(IDatabase database, IAmazonS3 amazonS3, ILogger logger, + InfrastructureStorageConfiguration storageClientConfiguration) : IStorageService { public async Task GetFileAsync(string bucket, string key, CancellationToken cancellationToken) From 747989f9dc0ec784cc42ad0dd21ebe56102563ea Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 18:26:44 +0200 Subject: [PATCH 108/172] Rename --- .../PlayLists/Handlers/CreatePlayListCommandHandler.cs | 1 - .../Configuration/InfrastructureStorageConfiguration.cs | 2 +- Infrastructure/Configuration/KeycloakConfiguration.cs | 2 +- Infrastructure/Configuration/MassTransitConfiguration.cs | 2 +- .../DependencyInjection/MassTransitDependencyInjection.cs | 2 +- Infrastructure/Services/ServicesDependencyInjection.cs | 4 ++-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 0df4aa5..e0268e7 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -6,7 +6,6 @@ using Musify.Application.Configuration; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; -using Musify.Application.Extensions; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; diff --git a/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs b/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs index 18cdd2a..4321c01 100644 --- a/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs +++ b/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs @@ -8,7 +8,7 @@ namespace Musify.Infrastructure.Configuration [Required] [Url] - public required string ServiceUrl { get; set; } + public required string Address { get; set; } [Required] [MinLength(3)] diff --git a/Infrastructure/Configuration/KeycloakConfiguration.cs b/Infrastructure/Configuration/KeycloakConfiguration.cs index e370097..7c1e1d4 100644 --- a/Infrastructure/Configuration/KeycloakConfiguration.cs +++ b/Infrastructure/Configuration/KeycloakConfiguration.cs @@ -7,7 +7,7 @@ namespace Musify.Infrastructure.Configuration public const string SectionName = "Keycloak"; [Required] - public required string BaseUrl { get; set; } + public required string Address { get; set; } [Required] public required string Realm { get; set; } diff --git a/Infrastructure/Configuration/MassTransitConfiguration.cs b/Infrastructure/Configuration/MassTransitConfiguration.cs index e92ccd6..35f9666 100644 --- a/Infrastructure/Configuration/MassTransitConfiguration.cs +++ b/Infrastructure/Configuration/MassTransitConfiguration.cs @@ -7,7 +7,7 @@ namespace Musify.Infrastructure.Configuration public const string SectionName = "Messaging"; [Required] - public required string Host { get; set; } + public required string Address { get; set; } [Required] public required string Username { get; set; } diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index d55724b..30ab738 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -143,7 +143,7 @@ namespace Musify.Infrastructure.MassTransit IRabbitMqBusFactoryConfigurator busFactoryConfigurator, MassTransitConfiguration massTransitConfiguration) { - busFactoryConfigurator.Host(massTransitConfiguration.Host, options => + busFactoryConfigurator.Host(massTransitConfiguration.Address, options => { options.Username(massTransitConfiguration.Username); options.Password(massTransitConfiguration.Password); diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index e9cba44..8b13651 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -25,7 +25,7 @@ namespace Musify.Infrastructure.Services var configuration = serviceProvider.GetRequiredService(); var s3Configuration = new AmazonS3Config { - ServiceURL = configuration.ServiceUrl, + ServiceURL = configuration.Address, ForcePathStyle = configuration.ForcePathStyle, UseHttp = configuration.UseHttp, }; @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.Services var keycloakConfiguration = serviceProvider.GetRequiredService(); return new KeycloakClient( - keycloakConfiguration.BaseUrl, + keycloakConfiguration.Address, keycloakConfiguration.Username, keycloakConfiguration.Password); } ); From bc3736ce1b46fe725c4c19ad698f16a923727cca Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 18:31:21 +0200 Subject: [PATCH 109/172] Configurations --- .../MassTransitDependencyInjection.cs | 12 +++++++++++- .../OpenTelemetryDependencyInjection.cs | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 30ab738..ef8d41b 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -11,6 +11,8 @@ using Musify.Infrastructure.MassTransit.Activities.Logs; using Musify.Infrastructure.MassTransit.Filters; using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; using Musify.Infrastructure.MassTransit.Activities.Files; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; namespace Musify.Infrastructure.MassTransit { @@ -29,8 +31,16 @@ namespace Musify.Infrastructure.MassTransit return serviceDescriptors; } - public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors) + public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(WorkerConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + serviceDescriptors.AddScoped(); serviceDescriptors.AddScoped(); serviceDescriptors.AddScoped(); diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs index 67b30d5..cc923d0 100644 --- a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -1,15 +1,26 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Musify.Infrastructure.Configuration; using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; +using SixLabors.ImageSharp; namespace Musify.Infrastructure.Observability { public static class OpenTelemetryDependencyInjection { - public static IServiceCollection AddInfrastructureOpenTelemetry(this IServiceCollection serviceDescriptors) + public static IServiceCollection AddInfrastructureOpenTelemetry(this IServiceCollection serviceDescriptors, IConfiguration configuration) { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(OpenTelemetryConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + serviceDescriptors .AddOptions() .Configure((exporterOptions, openTelemetryConfiguration) => From dc039d8a3ec570f3d0067303e276430b796f5158 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 18:59:50 +0200 Subject: [PATCH 110/172] Fix and missing service --- .../MassTransitDependencyInjection.cs | 38 ++++++++++++++----- .../DatabaseDependencyInjection.cs | 6 +++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index ef8d41b..059b1c4 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -1,25 +1,35 @@ using MassTransit; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.MassTransit.Activities.Audio; -using Musify.Infrastructure.MassTransit.Arguments; -using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.MassTransit.Logs; using Musify.Infrastructure.MassTransit.Activities; using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; -using Musify.Infrastructure.MassTransit.Filters; -using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.Activities.Audio; using Musify.Infrastructure.MassTransit.Activities.Files; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; +using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Consumers; +using Musify.Infrastructure.MassTransit.Filters; +using Musify.Infrastructure.MassTransit.Logs; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; namespace Musify.Infrastructure.MassTransit { public static class MassTransitDependencyInjection { - public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors) + public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors, IConfiguration configuration) { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(MassTransitConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + serviceDescriptors.AddScoped(); serviceDescriptors.AddMassTransit(options => { options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => @@ -33,6 +43,14 @@ namespace Musify.Infrastructure.MassTransit public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(MassTransitConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + serviceDescriptors .AddOptionsWithValidateOnStart() .Bind(configuration.GetRequiredSection(WorkerConfiguration.SectionName)) diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index 0a3f01c..ac1af6e 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -23,5 +23,11 @@ namespace Musify.Infrastructure.Persistence return serviceDescriptors; } + + public static IServiceCollection AddProcessTrackingStore(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.AddScoped(); + return serviceDescriptors; + } } } \ No newline at end of file From 001ec2dfdb6e19bc86081fe0f6e44bb3ccd44c00 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 19:10:24 +0200 Subject: [PATCH 111/172] Fixes --- .../Handlers/CreatePlayListCommandHandler.cs | 2 +- .../Handlers/UpdatePlayListCommandHandler.cs | 14 +++++++------- .../Tracks/Handler/CreateTrackCommandHandler.cs | 10 +++++----- .../Tracks/Handler/DeleteTrackCommandHandler.cs | 1 - 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index e0268e7..bb0fb85 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -99,7 +99,7 @@ namespace Musify.Application.PlayLists.Handlers await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.BucketName, - playList.OriginalPictureName, + Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 5e05081..06182a6 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -37,18 +37,18 @@ namespace Musify.Application.PlayLists.Handlers UpdateDescription(playListEntity, request.NewDescription); var updatePictureResult = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); if (!updatePictureResult.IsSuccess) - return updatePictureResult.Map(_ => PlayListResponse.FromEntity(playListEntity)); + return updatePictureResult; try { database.PlayLists.Update(playListEntity); await database.SaveChangesAsync(cancellationToken); - await PublishPlayListPictureEvent(playListEntity, updatePictureResult.Value, cancellationToken); + await PublishPlayListPictureEvent(playListEntity, cancellationToken); await RemoveOldPictures(playListEntity, cancellationToken); } catch (Exception exception) { - await Rollback(updatePictureResult.Value, cancellationToken); + await Rollback(playListEntity.OriginalPictureName, cancellationToken); logger.LogError(exception, "Failed to update playlist {PlayListId}", playListEntity.Id); return Result.Error($"Failed to update playlist {playListEntity.Id}"); } @@ -72,7 +72,7 @@ namespace Musify.Application.PlayLists.Handlers playList.Description = newDescription; } - async Task> UpdatePicture(PlayList playList, IFileData? newPicture, CancellationToken cancellationToken) + async Task UpdatePicture(PlayList playList, IFileData? newPicture, CancellationToken cancellationToken) { if (newPicture is null) return Result.Success(); @@ -94,17 +94,17 @@ namespace Musify.Application.PlayLists.Handlers } playList.OriginalPictureName = pictureName; - return Result.Success(pictureKey); + return Result.Success(); } - async Task PublishPlayListPictureEvent(PlayList playList, string pictureKey, CancellationToken cancellationToken) + async Task PublishPlayListPictureEvent(PlayList playList, CancellationToken cancellationToken) { try { await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.BucketName, - pictureKey, + Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 3a16270..8274e0e 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -94,17 +94,17 @@ namespace Musify.Application.Tracks.Handler await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, storageConfiguration.BucketName, - track.OriginalPictureName, + Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, track.OriginalPictureName), new ImageSize( - trackConfiguration.Routes.PresetSmallPicture, + trackConfiguration.Routes.SmallPicturesPath, trackConfiguration.PicturesSizes.SmallPictureWidth, trackConfiguration.PicturesSizes.SmallPictureHeight), new ImageSize( - trackConfiguration.Routes.PresetMediumPicture, + trackConfiguration.Routes.MediumPicturesPath, trackConfiguration.PicturesSizes.MediumPictureWidth, trackConfiguration.PicturesSizes.MediumPictureHeight), new ImageSize( - trackConfiguration.Routes.PresetLargePicture, + trackConfiguration.Routes.LargePicturesPath, trackConfiguration.PicturesSizes.LargePictureWidth, trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); } @@ -124,7 +124,7 @@ namespace Musify.Application.Tracks.Handler await eventBus.PublishAsync(new UpdateTrackAudioEvent( track.Id, storageConfiguration.BucketName, - track.OriginalAudioName, + Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), storageConfiguration.BucketName, destinationFolderAudio), cancellationToken); } diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index fd57090..599fa2a 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -54,7 +54,6 @@ namespace Musify.Application.Tracks.Handler { await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), cancellationToken); - await RemoveFile(Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, track.AudioFolderName), cancellationToken); } From 95704c2753ce23d4addee891a7d510d1a06c92ce Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 20:17:32 +0200 Subject: [PATCH 112/172] Remove DispatchR and use Mediator --- Application/Application.csproj | 6 +++++- Application/PlayLists/Commands/CreatePlayListCommand.cs | 4 ++-- Application/PlayLists/Commands/DeletePlayListCommand.cs | 4 ++-- Application/PlayLists/Commands/UpdatePlayListCommand.cs | 4 ++-- .../PlayLists/Handlers/CreatePlayListCommandHandler.cs | 6 +++--- .../PlayLists/Handlers/DeletePlayListCommandHandler.cs | 6 +++--- .../PlayLists/Handlers/GetPlayListByIdQueryHandler.cs | 6 +++--- .../PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs | 6 +++--- Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs | 6 +++--- .../PlayLists/Handlers/UpdatePlayListCommandHandler.cs | 6 +++--- Application/PlayLists/Queries/GetPlayListByIdQuery.cs | 4 ++-- Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs | 4 ++-- Application/PlayLists/Queries/GetPlayListsQuery.cs | 4 ++-- Application/Tracks/Commands/CreateTrackCommand.cs | 4 ++-- Application/Tracks/Commands/DeleteTrackCommand.cs | 4 ++-- Application/Tracks/Handler/CreateTrackCommandHandler.cs | 6 +++--- Application/Tracks/Handler/DeleteTrackCommandHandler.cs | 6 +++--- Application/Tracks/Handler/GetTrackByIdQueryHandler.cs | 6 +++--- Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs | 6 +++--- Application/Tracks/Handler/GetTracksQueryHandler.cs | 6 +++--- Application/Tracks/Queries/GetTrackByIdQuery.cs | 4 ++-- Application/Tracks/Queries/GetTracksByUserIdQuery.cs | 4 ++-- Application/Tracks/Queries/GetTracksQuery.cs | 4 ++-- Application/Users/Commands/CreateUserCommand.cs | 4 ++-- Application/Users/Commands/DeleteUserCommand.cs | 4 ++-- Application/Users/Handlers/CreateUserCommandHandler.cs | 6 +++--- Application/Users/Handlers/DeleteUserCommandHandler.cs | 6 +++--- Application/Users/Handlers/GetUserByIdQueryHandler.cs | 6 +++--- Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs | 6 +++--- Application/Users/Handlers/GetUsersQueryHandler.cs | 6 +++--- Application/Users/Queries/GetUserByIdQuery.cs | 4 ++-- Application/Users/Queries/GetUserByKeycloakQuery.cs | 4 ++-- Application/Users/Queries/GetUsersQuery.cs | 4 ++-- 33 files changed, 85 insertions(+), 81 deletions(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index cde91d6..cfe5605 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -9,8 +9,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index cda4122..5b6bdbb 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -11,7 +11,7 @@ namespace Musify.Application.PlayLists.Commands string Name, string Description, IFileData? Picture) - : IRequest> > + : ICommand> { public static PlayList ToEntity(CreatePlayListCommand command, string originalKey, string smallKey, string mediumKey, string largeKey) { diff --git a/Application/PlayLists/Commands/DeletePlayListCommand.cs b/Application/PlayLists/Commands/DeletePlayListCommand.cs index d4fd556..c695479 100644 --- a/Application/PlayLists/Commands/DeletePlayListCommand.cs +++ b/Application/PlayLists/Commands/DeletePlayListCommand.cs @@ -1,8 +1,8 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; namespace Musify.Application.PlayLists.Commands { public record DeletePlayListCommand(Guid UserId, Guid PlayListId) - : IRequest >; + : ICommand; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayListCommand.cs index 32442ef..3aac73c 100644 --- a/Application/PlayLists/Commands/UpdatePlayListCommand.cs +++ b/Application/PlayLists/Commands/UpdatePlayListCommand.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Responses; @@ -11,5 +11,5 @@ namespace Musify.Application.PlayLists.Commands string? NewName, string? NewDescription, IFileData? NewPicture) - : IRequest> >; + : ICommand>; } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index bb0fb85..51e37d9 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; @@ -14,9 +14,9 @@ namespace Musify.Application.PlayLists.Handlers { public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) - : IRequestHandler> > + : ICommandHandler> { - public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken); if (!userExists) diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 47c33fb..48e634a 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; @@ -10,9 +10,9 @@ namespace Musify.Application.PlayLists.Handlers { public class DeletePlayListCommandHandler(IDatabase database, IEventBus eventBus, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) - : IRequestHandler> + : ICommandHandler { - public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) + public async ValueTask Handle(DeletePlayListCommand request, CancellationToken cancellationToken) { var playList = await database.PlayLists.FindAsync( [request.PlayListId], diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index d007186..d8558f6 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -1,16 +1,16 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; +using Mediator; namespace Musify.Application.PlayLists.Handlers { public class GetPlayListByIdQueryHandler(IDatabase database) - : IRequestHandler> > + : IQueryHandler> { - public async Task> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken) { var playList = await database.PlayLists .AsNoTracking() diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index 18c1fda..29ea4cf 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -1,18 +1,18 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; +using Mediator; namespace Musify.Application.PlayLists.Handlers { public class GetPlayListsByUserIdQueryHandler(IDatabase database) - : IRequestHandler > >> + : IQueryHandler > > { - public async Task >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; diff --git a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index 8cb758e..308d32b 100644 --- a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -1,18 +1,18 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; +using Mediator; namespace Musify.Application.PlayLists.Handlers { public class GetPlayListsQueryHandler(IDatabase database) - : IRequestHandler >> > + : IQueryHandler >> { - public async Task >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 06182a6..70bfa21 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Application; @@ -14,9 +14,9 @@ namespace Musify.Application.PlayLists.Handlers public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) - : IRequestHandler> > + : ICommandHandler> { - public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { var playListEntity = await database.PlayLists.FindAsync( [request.PlayListId], diff --git a/Application/PlayLists/Queries/GetPlayListByIdQuery.cs b/Application/PlayLists/Queries/GetPlayListByIdQuery.cs index 4bf4360..27cc8c0 100644 --- a/Application/PlayLists/Queries/GetPlayListByIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListByIdQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries { public record GetPlayListByIdQuery(Guid Id) - : IRequest>>; + : IQuery>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index 5610553..c745a09 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Responses; @@ -10,5 +10,5 @@ namespace Musify.Application.PlayLists.Queries string Name = "", int PageNumber = 1, int PageSize = 10) - : IRequest> >>; + : IQuery> >; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayListsQuery.cs index 9764acd..c8ae5de 100644 --- a/Application/PlayLists/Queries/GetPlayListsQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsQuery.cs @@ -1,10 +1,10 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries { public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10) - : IRequest> >>; + : IQuery> >; } \ No newline at end of file diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index fc762cd..cc65340 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.Tracks.Responses; using Musify.Domain.Entities; @@ -11,7 +11,7 @@ namespace Musify.Application.Tracks.Commands string Title, IFileData Picture, IFileData Audio) - : IRequest >> + : ICommand > { public static Track ToEntity(CreateTrackCommand command, string smallPictureName, string mediumPictureName, string largePictureName) { diff --git a/Application/Tracks/Commands/DeleteTrackCommand.cs b/Application/Tracks/Commands/DeleteTrackCommand.cs index 5351410..6d7615f 100644 --- a/Application/Tracks/Commands/DeleteTrackCommand.cs +++ b/Application/Tracks/Commands/DeleteTrackCommand.cs @@ -1,10 +1,10 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; namespace Musify.Application.Tracks.Commands { public record DeleteTrackCommand( Guid UserId, Guid TrackId) - : IRequest>; + : ICommand; } \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 8274e0e..1a3a86f 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; @@ -12,9 +12,9 @@ namespace Musify.Application.Tracks.Handler { public class CreateTrackCommandHandler(IDatabase database, IEventBus eventBus, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) - : IRequestHandler >> + : ICommandHandler > { - public async Task> Handle(CreateTrackCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateTrackCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.FindAsync(request.UserId, cancellationToken); if (userExists is null) diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 599fa2a..5cc943e 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; @@ -10,9 +10,9 @@ namespace Musify.Application.Tracks.Handler { public class DeleteTrackCommandHandler(IDatabase database, IStorageService storageHandler, ILogger logger, TrackConfiguration trackConfiguration, ApplicationStorageConfiguration storageConfiguration) - : IRequestHandler> + : ICommandHandler { - public async Task Handle(DeleteTrackCommand request, CancellationToken cancellationToken) + public async ValueTask Handle(DeleteTrackCommand request, CancellationToken cancellationToken) { var track = await database.Tracks.FindAsync(request.TrackId, cancellationToken); if (track is null) diff --git a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs index c0d4b2e..2d4172b 100644 --- a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; @@ -7,9 +7,9 @@ using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Handler { public class GetTrackByIdQueryHandler(IDatabase database) - : IRequestHandler >> + : IQueryHandler > { - public async Task> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) { var track = await database.Tracks.FindAsync(request.TrackId); diff --git a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs index 4dd81b8..54b8973 100644 --- a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; @@ -10,9 +10,9 @@ using X.PagedList.EF; namespace Musify.Application.Tracks.Handler { public class GetTracksByUserIdQueryHandler(IDatabase database) - : IRequestHandler> >> + : IQueryHandler> > { - public async Task >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; diff --git a/Application/Tracks/Handler/GetTracksQueryHandler.cs b/Application/Tracks/Handler/GetTracksQueryHandler.cs index 59e43e3..7cc21cb 100644 --- a/Application/Tracks/Handler/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksQueryHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; @@ -10,9 +10,9 @@ using X.PagedList.EF; namespace Musify.Application.Tracks.Handler { public class GetTracksQueryHandler(IDatabase database) - : IRequestHandler> > > + : IQueryHandler> > { - public async Task >> Handle(GetTracksQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetTracksQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; diff --git a/Application/Tracks/Queries/GetTrackByIdQuery.cs b/Application/Tracks/Queries/GetTrackByIdQuery.cs index 1008b99..97a6ee6 100644 --- a/Application/Tracks/Queries/GetTrackByIdQuery.cs +++ b/Application/Tracks/Queries/GetTrackByIdQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries { public record GetTrackByIdQuery(Guid TrackId) - : IRequest >>; + : IQuery>; } \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs index 25afa9f..240cbbf 100644 --- a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs +++ b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.Tracks.Responses; @@ -10,5 +10,5 @@ namespace Musify.Application.Tracks.Queries string Name = "", int PageNumber = 1, int PageSize = 10) - : IRequest > >>; + : IQuery > >; } \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksQuery.cs b/Application/Tracks/Queries/GetTracksQuery.cs index 3a10860..d67698b 100644 --- a/Application/Tracks/Queries/GetTracksQuery.cs +++ b/Application/Tracks/Queries/GetTracksQuery.cs @@ -1,10 +1,10 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries { public record GetTracksQuery(int PageNumber = 1, int PageSize = 10) - : IRequest >> >; + : IQuery >>; } \ No newline at end of file diff --git a/Application/Users/Commands/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs index fcc77b7..2f7d89b 100644 --- a/Application/Users/Commands/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -1,12 +1,12 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Users.Responses; using Musify.Domain.Entities; namespace Musify.Application.Users.Commands { public record CreateUserCommand(Guid Id) - : IRequest> > + : ICommand> { public static User ToEntity(CreateUserCommand command) { diff --git a/Application/Users/Commands/DeleteUserCommand.cs b/Application/Users/Commands/DeleteUserCommand.cs index 2773d0a..5a64998 100644 --- a/Application/Users/Commands/DeleteUserCommand.cs +++ b/Application/Users/Commands/DeleteUserCommand.cs @@ -1,8 +1,8 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; namespace Musify.Application.Users.Commands { public record DeleteUserCommand(Guid Id) - : IRequest>; + : ICommand; } \ No newline at end of file diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index fab1b64..aae7176 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; @@ -9,9 +9,9 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { public class CreateUserCommandHandler(IDatabase database, IKeycloakUserService keycloakUserClient, ILogger logger) - : IRequestHandler >> + : ICommandHandler > { - public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AnyAsync(u => u.Id == request.Id, cancellationToken); if (userExists) diff --git a/Application/Users/Handlers/DeleteUserCommandHandler.cs b/Application/Users/Handlers/DeleteUserCommandHandler.cs index 09b0c94..86d9c62 100644 --- a/Application/Users/Handlers/DeleteUserCommandHandler.cs +++ b/Application/Users/Handlers/DeleteUserCommandHandler.cs @@ -1,13 +1,13 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Infrastructure; namespace Musify.Application.Users.Commands { public class DeleteUserCommandHandler(IDatabase database) - : IRequestHandler> + : ICommandHandler { - public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) + public async ValueTask Handle(DeleteUserCommand request, CancellationToken cancellationToken) { var existingUser = await database.Users.FindAsync(request.Id, cancellationToken); if (existingUser is null) diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index 47dd1ce..bc9a311 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; @@ -8,9 +8,9 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) - : IRequestHandler >> + : IQueryHandler > { - public async Task> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { var user = await database.Users .AsNoTracking() diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index b8e59f0..4dbd18b 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; @@ -8,9 +8,9 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) - : IRequestHandler >> + : IQueryHandler > { - public async Task> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) { var user = await database.Users .AsNoTracking() diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index 8c5d856..d52a32e 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -1,5 +1,5 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; @@ -11,9 +11,9 @@ using X.PagedList.Extensions; namespace Musify.Application.Users.Handlers { public class GetUsersQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) - : IRequestHandler > >> + : IQueryHandler > > { - public async Task >> Handle(GetUsersQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetUsersQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; diff --git a/Application/Users/Queries/GetUserByIdQuery.cs b/Application/Users/Queries/GetUserByIdQuery.cs index fbdf6fd..b81c047 100644 --- a/Application/Users/Queries/GetUserByIdQuery.cs +++ b/Application/Users/Queries/GetUserByIdQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { public record GetUserByIdQuery(Guid Id) - : IRequest >>; + : IQuery >; } \ No newline at end of file diff --git a/Application/Users/Queries/GetUserByKeycloakQuery.cs b/Application/Users/Queries/GetUserByKeycloakQuery.cs index e53d449..4f19a7d 100644 --- a/Application/Users/Queries/GetUserByKeycloakQuery.cs +++ b/Application/Users/Queries/GetUserByKeycloakQuery.cs @@ -1,9 +1,9 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { public record GetUserByKeycloakQuery(Guid Id) - : IRequest >>; + : IQuery >; } diff --git a/Application/Users/Queries/GetUsersQuery.cs b/Application/Users/Queries/GetUsersQuery.cs index 338e01f..62ba046 100644 --- a/Application/Users/Queries/GetUsersQuery.cs +++ b/Application/Users/Queries/GetUsersQuery.cs @@ -1,10 +1,10 @@ using Ardalis.Result; -using DispatchR.Abstractions.Send; +using Mediator; using Musify.Application.Contracts.Application; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { public record GetUsersQuery(int PageNumber, int PageSize, string UsernameSearch) - : IRequest >> >; + : IQuery >>; } From 8a07644a91b563f95a07fb4226915ea64af3dd87 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 29 Mar 2026 20:37:40 +0200 Subject: [PATCH 113/172] Configuration mediator --- Application/MusifyApplication.cs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Application/MusifyApplication.cs diff --git a/Application/MusifyApplication.cs b/Application/MusifyApplication.cs new file mode 100644 index 0000000..845ff9b --- /dev/null +++ b/Application/MusifyApplication.cs @@ -0,0 +1,4 @@ +using Mediator; +using Microsoft.Extensions.DependencyInjection; + +[assembly: MediatorOptions(ServiceLifetime = ServiceLifetime.Scoped)] \ No newline at end of file From adb5b8277a74db79f6d09b75b06d0ded780bcc1f Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 30 Mar 2026 09:43:53 +0200 Subject: [PATCH 114/172] Remove --- Infrastructure/Infrastructure.csproj | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index b2d0c01..4dce091 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -33,16 +33,4 @@ - - - - - - - - - - - - From 0ca4923a5133beb1e82749c648eecd1c250cc4e7 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 30 Mar 2026 09:51:59 +0200 Subject: [PATCH 115/172] Configure packages --- Application/Application.csproj | 4 ++++ Domain/Domain.csproj | 4 ++++ Infrastructure/Infrastructure.csproj | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/Application/Application.csproj b/Application/Application.csproj index cfe5605..28d2ce4 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -5,6 +5,10 @@ enable enable Musify.Application + Musify.Application + 1.0.0 + NotS1mp + https://github.com/XShine555/MusifyBackend diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index c79d803..b71cc67 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -5,6 +5,10 @@ enable enable Musify.Domain + Musify.Domain + 1.0.0 + NotS1mp + https://github.com/XShine555/MusifyBackend diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 4dce091..6dc2bd1 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -6,6 +6,11 @@ enable Musify.Infrastructure 07d419fc-9ade-471a-bc0e-aebece785b30 + Musify.Infrastructure + Musify.Infrastructure + 1.0.0 + NotS1mp + https://github.com/XShine555/MusifyBackend From b83206252a6f1f73507231e7af8a113c23e3f1eb Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 30 Mar 2026 10:07:08 +0200 Subject: [PATCH 116/172] Remove unused file --- .../ConfigurationDependencyInjection.cs | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 Application/Configuration/ConfigurationDependencyInjection.cs diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs deleted file mode 100644 index b82a141..0000000 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Musify.Application.Configuration -{ - public static class ConfigurationDependencyInjection - { - public static IServiceCollection AddApplicationConfigurations(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - serviceDescriptors - .AddValidatedConfiguration(configuration, ApplicationStorageConfiguration.SectionName) - .AddValidatedConfiguration(configuration, PlayListConfiguration.SectionName) - .AddValidatedConfiguration(configuration, TrackConfiguration.SectionName); - - return serviceDescriptors; - } - - static IServiceCollection AddValidatedConfiguration( - this IServiceCollection serviceDescriptors, - IConfiguration configuration, - string sectionName) - where TConfiguration : class - { - serviceDescriptors - .AddOptionsWithValidateOnStart() - .Bind(configuration.GetRequiredSection(sectionName)) - .ValidateDataAnnotations(); - - serviceDescriptors.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService>().Value); - - return serviceDescriptors; - } - } -} From e6c68581f6c65bf6b79f6d43f31fd76f1e09cde0 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 30 Mar 2026 10:12:51 +0200 Subject: [PATCH 117/172] Rename --- .../ApplicationStorageConfiguration.cs | 2 +- Application/Configuration/PlayListConfiguration.cs | 2 +- Application/Configuration/TrackConfiguration.cs | 2 +- .../Handlers/CreatePlayListCommandHandler.cs | 6 +++--- .../Handlers/DeletePlayListCommandHandler.cs | 2 +- .../Handlers/UpdatePlayListCommandHandler.cs | 8 ++++---- .../Tracks/Handler/CreateTrackCommandHandler.cs | 14 +++++++------- .../Tracks/Handler/DeleteTrackCommandHandler.cs | 2 +- .../Configuration/MassTransitConfiguration.cs | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Application/Configuration/ApplicationStorageConfiguration.cs b/Application/Configuration/ApplicationStorageConfiguration.cs index 2439621..4ac0723 100644 --- a/Application/Configuration/ApplicationStorageConfiguration.cs +++ b/Application/Configuration/ApplicationStorageConfiguration.cs @@ -9,6 +9,6 @@ namespace Musify.Application.Configuration [Required] [MinLength(3)] [MaxLength(63)] - public required string BucketName { get; set; } + public required string Bucket { get; set; } } } diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index d887fd3..ca6d7ff 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -4,7 +4,7 @@ namespace Musify.Application.Configuration { public class PlayListConfiguration { - public const string SectionName = "PlayListConfiguration"; + public const string SectionName = "PlayList"; [Required] public PlayListRoutes Routes { get; set; } = new PlayListRoutes(); diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index 58cf2ce..5928fac 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -4,7 +4,7 @@ namespace Musify.Application.Configuration { public class TrackConfiguration { - public const string SectionName = "TrackConfiguration"; + public const string SectionName = "Track"; [Required] public TrackRoutes Routes { get; set; } = new TrackRoutes(); diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 51e37d9..924dd7f 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -65,7 +65,7 @@ namespace Musify.Application.PlayLists.Handlers { try { - await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, + await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), cancellationToken); } catch (Exception exception) @@ -81,7 +81,7 @@ namespace Musify.Application.PlayLists.Handlers try { - await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.BucketName, pictureKey, cancellationToken); + await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.Bucket, pictureKey, cancellationToken); } catch { @@ -98,7 +98,7 @@ namespace Musify.Application.PlayLists.Handlers { await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, - storageConfiguration.BucketName, + storageConfiguration.Bucket, Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 48e634a..c2ca6ba 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -50,7 +50,7 @@ namespace Musify.Application.PlayLists.Handlers try { await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, + storageConfiguration.Bucket, key), cancellationToken); return Result.Success(); } diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 70bfa21..a882b5b 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -84,7 +84,7 @@ namespace Musify.Application.PlayLists.Handlers await storageHandler.UploadFileAsync( newPicture.FileStream, newPicture.ContentType, - storageConfiguration.BucketName, + storageConfiguration.Bucket, pictureKey, cancellationToken); } @@ -103,7 +103,7 @@ namespace Musify.Application.PlayLists.Handlers { await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, - storageConfiguration.BucketName, + storageConfiguration.Bucket, Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, @@ -149,7 +149,7 @@ namespace Musify.Application.PlayLists.Handlers async Task Rollback(string pictureKey, CancellationToken cancellationToken) { await storageHandler.RemoveFileAsync( - storageConfiguration.BucketName, + storageConfiguration.Bucket, pictureKey, cancellationToken); logger.LogInformation("Rolled back uploaded picture {PictureKey} from storage", pictureKey); @@ -160,7 +160,7 @@ namespace Musify.Application.PlayLists.Handlers try { await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.BucketName, + storageConfiguration.Bucket, key), cancellationToken); return Result.NoContent(); } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 1a3a86f..a197ad5 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -54,7 +54,7 @@ namespace Musify.Application.Tracks.Handler await storageHandler.UploadFileAsync( request.Picture.FileStream, request.Picture.ContentType, - storageConfiguration.BucketName, + storageConfiguration.Bucket, pictureKey, cancellationToken); } @@ -68,7 +68,7 @@ namespace Musify.Application.Tracks.Handler await storageHandler.UploadFileAsync( request.Audio.FileStream, request.Audio.ContentType, - storageConfiguration.BucketName, + storageConfiguration.Bucket, audioKey, cancellationToken); } @@ -82,8 +82,8 @@ namespace Musify.Application.Tracks.Handler async Task RollbackFilesAsync(string pictureKey, string audioKey, CancellationToken cancellationToken) { - await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, pictureKey, cancellationToken); - await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, audioKey, cancellationToken); + await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, pictureKey, cancellationToken); + await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, audioKey, cancellationToken); logger.LogInformation("Rolled back uploaded files {PictureKey}, {AudioKey}", pictureKey, audioKey); } @@ -93,7 +93,7 @@ namespace Musify.Application.Tracks.Handler { await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, - storageConfiguration.BucketName, + storageConfiguration.Bucket, Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, track.OriginalPictureName), new ImageSize( trackConfiguration.Routes.SmallPicturesPath, @@ -123,9 +123,9 @@ namespace Musify.Application.Tracks.Handler { await eventBus.PublishAsync(new UpdateTrackAudioEvent( track.Id, - storageConfiguration.BucketName, + storageConfiguration.Bucket, Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), - storageConfiguration.BucketName, + storageConfiguration.Bucket, destinationFolderAudio), cancellationToken); } catch (Exception exception) diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 5cc943e..8736249 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -60,7 +60,7 @@ namespace Musify.Application.Tracks.Handler async Task RemoveFile(string path, CancellationToken cancellationToken) { - await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, path, cancellationToken); + await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, path, cancellationToken); logger.LogDebug("Removed file {Path} from storage", path); } } diff --git a/Infrastructure/Configuration/MassTransitConfiguration.cs b/Infrastructure/Configuration/MassTransitConfiguration.cs index 35f9666..df216ee 100644 --- a/Infrastructure/Configuration/MassTransitConfiguration.cs +++ b/Infrastructure/Configuration/MassTransitConfiguration.cs @@ -4,7 +4,7 @@ namespace Musify.Infrastructure.Configuration { public class MassTransitConfiguration { - public const string SectionName = "Messaging"; + public const string SectionName = "MassTransit"; [Required] public required string Address { get; set; } From 760cb9fc40145ea08b112554de51fa426e1bc454 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 30 Mar 2026 10:20:32 +0200 Subject: [PATCH 118/172] Version --- Application/Application.csproj | 2 +- Domain/Domain.csproj | 2 +- Infrastructure/Infrastructure.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index 28d2ce4..e1d9a0d 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -6,7 +6,7 @@ enable Musify.Application Musify.Application - 1.0.0 + 0.1.0 NotS1mp https://github.com/XShine555/MusifyBackend diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index b71cc67..c00f482 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -6,7 +6,7 @@ enable Musify.Domain Musify.Domain - 1.0.0 + 0.1.0 NotS1mp https://github.com/XShine555/MusifyBackend diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 6dc2bd1..a8167e4 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -8,7 +8,7 @@ 07d419fc-9ade-471a-bc0e-aebece785b30 Musify.Infrastructure Musify.Infrastructure - 1.0.0 + 0.1.0 NotS1mp https://github.com/XShine555/MusifyBackend From 01361a3efef50b444f515e33a1299503d49675b1 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 30 Mar 2026 15:55:44 +0200 Subject: [PATCH 119/172] Missing configuration DI --- Application/Application.csproj | 2 +- .../ConfigurationDependencyInjection.cs | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 Application/Configuration/ConfigurationDependencyInjection.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index e1d9a0d..ac3e192 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -6,7 +6,7 @@ enable Musify.Application Musify.Application - 0.1.0 + 0.1.1 NotS1mp https://github.com/XShine555/MusifyBackend diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs new file mode 100644 index 0000000..c7e2504 --- /dev/null +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Musify.Application.Configuration +{ + public static class ConfigurationDependencyInjection + { + public static IServiceCollection AddApplicationStorageConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(ApplicationStorageConfiguration.SectionName)) + .ValidateDataAnnotations(); + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } + + public static IServiceCollection AddPlayListConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(PlayListConfiguration.SectionName)) + .ValidateDataAnnotations(); + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } + + public static IServiceCollection AddTrackConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(TrackConfiguration.SectionName)) + .ValidateDataAnnotations(); + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } + } +} \ No newline at end of file From c01a0b2032ba577b34f44d02bd375fe24cd7f16c Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 1 Apr 2026 09:50:30 +0200 Subject: [PATCH 120/172] UTC --- Domain/Entities/User.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 2703c2a..eca06c3 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -10,10 +10,10 @@ namespace Musify.Domain.Entities public required Guid Id { get; set; } [Required] - public DateTime CreatedAt { get; set; } = DateTime.Now; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [Required] - public DateTime UpdatedAt { get; set; } = DateTime.Now; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public ICollection UserTracks { get; set; } = new List(); From c4d99eec66f98f139ab1b672de8fc228d6a32790 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 1 Apr 2026 09:50:53 +0200 Subject: [PATCH 121/172] Version bump --- Domain/Domain.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index c00f482..b2ee78d 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -6,7 +6,7 @@ enable Musify.Domain Musify.Domain - 0.1.0 + 0.1.1 NotS1mp https://github.com/XShine555/MusifyBackend From aee2226127bcdb9ee8b4b1960656cc7f1f676bae Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 8 May 2026 21:48:31 +0200 Subject: [PATCH 122/172] Update grafana --- Infrastructure/Infrastructure.csproj | 7 +++++- .../OpenTelemetryDependencyInjection.cs | 24 +++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index a8167e4..8c26de5 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -28,8 +28,13 @@ - + + + + + + diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs index cc923d0..83c5c1f 100644 --- a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Musify.Infrastructure.Configuration; using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; using SixLabors.ImageSharp; @@ -30,16 +31,25 @@ namespace Musify.Infrastructure.Observability serviceDescriptors .AddOpenTelemetry() - .WithTracing(tracing => + .ConfigureResource(resource => { - tracing.AddSource("MassTransit"); - tracing.AddOtlpExporter(); + resource.AddService("Musify"); } ) + .WithTracing(tracing => + tracing.AddSource("MassTransit") + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation() + .AddOtlpExporter() + ) .WithMetrics(metrics => - { - metrics.AddMeter("MassTransit"); - metrics.AddOtlpExporter(); - } ); + metrics.AddMeter("MassTransit") + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddProcessInstrumentation() + .AddOtlpExporter() + ); return serviceDescriptors; } From 01cedd52ba0755ee38b829789c2fed0bfa785a4a Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 9 May 2026 02:56:43 +0200 Subject: [PATCH 123/172] Change --- .../Handlers/CreatePlayListCommandHandler.cs | 97 +++++++++++-------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 924dd7f..c84cc8c 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -12,9 +12,14 @@ using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { - public class CreatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, - ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) - : ICommandHandler> + public class CreatePlayListCommandHandler( + IEventBus eventBus, + IDatabase database, + IStorageService storageService, + ILogger logger, + ApplicationStorageConfiguration storageConfiguration, + PlayListConfiguration playListConfiguration) + : ICommandHandler> { public async ValueTask> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) { @@ -25,7 +30,8 @@ namespace Musify.Application.PlayLists.Handlers return Result.NotFound($"User {request.UserId} not found"); } - var playListEntity = CreatePlayListCommand.ToEntity(request, + var playList = CreatePlayListCommand.ToEntity( + request, playListConfiguration.Routes.PresetOriginalPicture, playListConfiguration.Routes.PresetSmallPicture, playListConfiguration.Routes.PresetMediumPicture, @@ -33,70 +39,57 @@ namespace Musify.Application.PlayLists.Handlers if (request.Picture is not null) { - var uploadResult = await SetPicture(playListEntity, request.Picture, cancellationToken); + var uploadResult = await UploadPictureAsync(playList, request.Picture, cancellationToken); if (!uploadResult.IsSuccess) return uploadResult; - - var publishResult = await PublishUpdateEvent(playListEntity, cancellationToken); - if (!publishResult.IsSuccess) - { - await RollbackPicture(playListEntity, cancellationToken); - return publishResult; - } } try { - await database.PlayLists.AddAsync(playListEntity, cancellationToken); + await database.PlayLists.AddAsync(playList, cancellationToken); await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { - await RollbackPicture(playListEntity, cancellationToken); - logger.LogError(exception, "Failed to create playlist for user {UserId}", request.UserId); + logger.LogError(exception, "Failed to save playlist for user {UserId}", request.UserId); + await RollbackAllPicturesAsync(playList, cancellationToken); return Result.Error($"Failed to create playlist for user {request.UserId}"); } - logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playListEntity.Id, request.UserId); - return Result.Created(PlayListResponse.FromEntity(playListEntity)); + if (request.Picture is not null) + { + var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken); + if (!publishResult.IsSuccess) + return publishResult; + } + + logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId); + return Result.Created(PlayListResponse.FromEntity(playList)); } - async Task RollbackPicture(PlayList playList, CancellationToken cancellationToken) - { - try - { - await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, - Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback picture upload for playlist {PlayListId}", playList.Id); - } - } - - async Task SetPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) + async Task UploadPictureAsync(PlayList playList, IFileData picture, CancellationToken cancellationToken) { var pictureName = Guid.NewGuid() + picture.FileType; var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); try { - await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.Bucket, pictureKey, cancellationToken); + await storageService.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.Bucket, pictureKey, cancellationToken); + playList.OriginalPictureName = pictureName; + return Result.Success(); } - catch + catch (Exception exception) { - return Result.Error($"An error occurred while uploading picture for playlist {playList.Id} to storage."); + logger.LogError(exception, "Failed to upload picture for playlist {PlayListId}", playList.Id); + return Result.Error($"Failed to upload picture for playlist {playList.Id}"); } - - playList.OriginalPictureName = pictureName; - return Result.Success(); } - async Task PublishUpdateEvent(PlayList playList, CancellationToken cancellationToken) + async Task PublishPictureUpdateEventAsync(PlayList playList, CancellationToken cancellationToken) { try { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + var updateEvent = new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.Bucket, Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), @@ -111,14 +104,32 @@ namespace Musify.Application.PlayLists.Handlers new ImageSize( playListConfiguration.Routes.LargePicturesPath, playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)), - cancellationToken); + playListConfiguration.PicturesSizes.LargePictureHeight)); + + await eventBus.PublishAsync(updateEvent, cancellationToken); return Result.Success(); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playList.Id); - return Result.Error($"Failed to publish playlist {playList.Id} picture update event"); + logger.LogError(exception, "Failed to publish picture update event for playlist {PlayListId}", playList.Id); + return Result.Error($"Failed to publish picture update event for playlist {playList.Id}"); + } + } + + async Task RollbackAllPicturesAsync(PlayList playList, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(playList.OriginalPictureName)) + return; + + var originalPicturePath = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName); + + try + { + await storageService.RemoveFileAsync(storageConfiguration.Bucket, originalPicturePath, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback picture upload for playlist {PlayListId}", playList.Id); } } } From 4b0d94b7429729156a6c38041f596041001b414e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 9 May 2026 18:47:57 +0200 Subject: [PATCH 124/172] Fix stringLength --- Application/Configuration/ApplicationStorageConfiguration.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Application/Configuration/ApplicationStorageConfiguration.cs b/Application/Configuration/ApplicationStorageConfiguration.cs index 4ac0723..1458cc3 100644 --- a/Application/Configuration/ApplicationStorageConfiguration.cs +++ b/Application/Configuration/ApplicationStorageConfiguration.cs @@ -7,8 +7,7 @@ namespace Musify.Application.Configuration public const string SectionName = "ApplicationStorage"; [Required] - [MinLength(3)] - [MaxLength(63)] + [StringLength(128, MinimumLength = 1)] public required string Bucket { get; set; } } } From 8662819def0dc70d084976060f1efe53daeeab24 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 9 May 2026 18:49:39 +0200 Subject: [PATCH 125/172] AsNoTracking --- .../PlayLists/Handlers/CreatePlayListCommandHandler.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index c84cc8c..e9f4c94 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -23,7 +23,9 @@ namespace Musify.Application.PlayLists.Handlers { public async ValueTask> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) { - var userExists = await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken); + var userExists = await database.Users + .AsNoTracking() + .AnyAsync(u => u.Id == request.UserId, cancellationToken); if (!userExists) { logger.LogWarning("User {UserId} not found", request.UserId); From ef483f0ef3444d860ddd317a7c63d01d84f4e6bd Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 9 May 2026 18:57:16 +0200 Subject: [PATCH 126/172] Changes on routes --- .../Configuration/PlayListConfiguration.cs | 8 ++++++++ .../Handlers/CreatePlayListCommandHandler.cs | 6 +++--- .../Handlers/DeletePlayListCommandHandler.cs | 20 ++++++++++--------- .../Handlers/UpdatePlayListCommandHandler.cs | 18 ++++++++++++----- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index ca6d7ff..dd1964d 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -55,6 +55,14 @@ namespace Musify.Application.Configuration public string PresetLargePicturePath => Path.Combine(ParentFolders, PresetLargePicture); public string OriginalPicturesPath => Path.Combine(ParentFolders, OriginalPicturesFolder); + + public string BuildOriginalPicturePath(string pictureName) => Path.Combine(ParentFolders, OriginalPicturesFolder, pictureName); + + public string BuildSmallPicturePath(string pictureName) => Path.Combine(ParentFolders, SmallPicturesFolder, pictureName); + + public string BuildMediumPicturePath(string pictureName) => Path.Combine(ParentFolders, MediumPicturesFolder, pictureName); + + public string BuildLargePicturePath(string pictureName) => Path.Combine(ParentFolders, LargePicturesFolder, pictureName); } public class PlayListPicturesSizes diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index e9f4c94..0bdb882 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -72,7 +72,7 @@ namespace Musify.Application.PlayLists.Handlers async Task UploadPictureAsync(PlayList playList, IFileData picture, CancellationToken cancellationToken) { var pictureName = Guid.NewGuid() + picture.FileType; - var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); + var pictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(pictureName); try { @@ -94,7 +94,7 @@ namespace Musify.Application.PlayLists.Handlers var updateEvent = new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.Bucket, - Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), + playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, @@ -123,7 +123,7 @@ namespace Musify.Application.PlayLists.Handlers if (string.IsNullOrEmpty(playList.OriginalPictureName)) return; - var originalPicturePath = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName); + var originalPicturePath = playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName); try { diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index c2ca6ba..db0c188 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using Mediator; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; @@ -14,9 +15,7 @@ namespace Musify.Application.PlayLists.Handlers { public async ValueTask Handle(DeletePlayListCommand request, CancellationToken cancellationToken) { - var playList = await database.PlayLists.FindAsync( - [request.PlayListId], - cancellationToken); + var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == request.PlayListId, cancellationToken); if (playList is null) { logger.LogDebug("Playlist {PlayListId} not found", request.PlayListId); @@ -29,15 +28,18 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } + if (!string.IsNullOrWhiteSpace(playList.OriginalPictureName)) + await PublishRemoveFileEventAsync(playList.Id, + playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName), cancellationToken); if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) - await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.SmallPicturesPath, - playList.SmallPictureName), cancellationToken); + await PublishRemoveFileEventAsync(playList.Id, + playListConfiguration.Routes.BuildSmallPicturePath(playList.SmallPictureName), cancellationToken); if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) - await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.MediumPicturesPath, - playList.MediumPictureName), cancellationToken); + await PublishRemoveFileEventAsync(playList.Id, + playListConfiguration.Routes.BuildMediumPicturePath(playList.MediumPictureName), cancellationToken); if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) - await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.LargePicturesPath, - playList.LargePictureName), cancellationToken); + await PublishRemoveFileEventAsync(playList.Id, + playListConfiguration.Routes.BuildLargePicturePath(playList.LargePictureName), cancellationToken); database.PlayLists.Remove(playList); await database.SaveChangesAsync(cancellationToken); diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index a882b5b..2e643ec 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using Mediator; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Application; @@ -18,9 +19,7 @@ namespace Musify.Application.PlayLists.Handlers { public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { - var playListEntity = await database.PlayLists.FindAsync( - [request.PlayListId], - cancellationToken); + var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); if (playListEntity is null) { logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); @@ -43,8 +42,6 @@ namespace Musify.Application.PlayLists.Handlers { database.PlayLists.Update(playListEntity); await database.SaveChangesAsync(cancellationToken); - await PublishPlayListPictureEvent(playListEntity, cancellationToken); - await RemoveOldPictures(playListEntity, cancellationToken); } catch (Exception exception) { @@ -53,6 +50,17 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error($"Failed to update playlist {playListEntity.Id}"); } + try + { + await PublishPlayListPictureEvent(playListEntity, cancellationToken); + await RemoveOldPictures(playListEntity, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish playlist picture update event or remove old pictures for playlist {PlayListId}", playListEntity.Id); + return Result.Error($"Failed to update playlist {playListEntity.Id}"); + } + logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id); return Result.Success(PlayListResponse.FromEntity(playListEntity)); } From 275ba57c0c1ff2e9096e6d9d7448ad53fa32eab9 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 9 May 2026 19:09:10 +0200 Subject: [PATCH 127/172] Update routes and changes on queries --- .../Configuration/TrackConfiguration.cs | 12 +++++++++ .../Handlers/GetPlayListByIdQueryHandler.cs | 5 ++-- .../Handler/CreateTrackCommandHandler.cs | 23 +++++++++------- .../Handler/DeleteTrackCommandHandler.cs | 26 ++++++++++++------- .../Handler/GetTrackByIdQueryHandler.cs | 9 ++++--- 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index 5928fac..c9658ef 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -65,6 +65,18 @@ namespace Musify.Application.Configuration public string OriginalAudiosPath => Path.Combine(ParentFolder, OriginalAudiosFolder); public string ProcessedAudiosPath => Path.Combine(ParentFolder, ProcessedAudioFolder); + + public string BuildOriginalPicturePath(string pictureName) => Path.Combine(OriginalPicturesPath, pictureName); + + public string BuildSmallPicturePath(string pictureName) => Path.Combine(SmallPicturesPath, pictureName); + + public string BuildMediumPicturePath(string pictureName) => Path.Combine(MediumPicturesPath, pictureName); + + public string BuildLargePicturePath(string pictureName) => Path.Combine(LargePicturesPath, pictureName); + + public string BuildOriginalAudioPath(string audioName) => Path.Combine(OriginalAudiosPath, audioName); + + public string BuildProcessedAudioPath(string audioName) => Path.Combine(ProcessedAudiosPath, audioName); } public class TrackPicturesSizes diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index d8558f6..6b430c7 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -14,11 +14,10 @@ namespace Musify.Application.PlayLists.Handlers { var playList = await database.PlayLists .AsNoTracking() + .Select(p => PlayListResponse.FromEntity(p)) .SingleOrDefaultAsync(p => p.Id == request.Id, cancellationToken); - return playList is not null - ? Result.Success(PlayListResponse.FromEntity(playList)) - : Result.NotFound("Playlist not found"); + return playList ?? Result.NotFound(); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index a197ad5..68c79ec 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using Mediator; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; @@ -16,8 +17,9 @@ namespace Musify.Application.Tracks.Handler { public async ValueTask> Handle(CreateTrackCommand request, CancellationToken cancellationToken) { - var userExists = await database.Users.FindAsync(request.UserId, cancellationToken); - if (userExists is null) + var userExists = await database.Users.AsNoTracking() + .AnyAsync(u => u.Id == request.UserId, cancellationToken); + if (!userExists) { logger.LogWarning("User {UserId} not found", request.UserId); return Result.NotFound($"User {request.UserId} not found"); @@ -29,8 +31,8 @@ namespace Musify.Application.Tracks.Handler trackConfiguration.Routes.PresetMediumPicture, trackConfiguration.Routes.PresetLargePicture); - var fullPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, trackEntity.OriginalPictureName); - var fullAudioKey = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, trackEntity.OriginalAudioName); + var fullPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(trackEntity.OriginalPictureName); + var fullAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(trackEntity.OriginalAudioName); await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken); @@ -58,8 +60,9 @@ namespace Musify.Application.Tracks.Handler pictureKey, cancellationToken); } - catch + catch (Exception exception) { + logger.LogError(exception, "Failed to upload picture for track {Title}", request.Title); return Result.Error($"Failed to upload picture for {request.Title}"); } @@ -72,8 +75,9 @@ namespace Musify.Application.Tracks.Handler audioKey, cancellationToken); } - catch + catch (Exception exception) { + logger.LogError(exception, "Failed to upload audio for track {Title}", request.Title); await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); return Result.Error($"Failed to upload audio for {request.Title}"); } @@ -94,7 +98,7 @@ namespace Musify.Application.Tracks.Handler await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, storageConfiguration.Bucket, - Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, track.OriginalPictureName), + trackConfiguration.Routes.BuildOriginalPicturePath(track.OriginalPictureName), new ImageSize( trackConfiguration.Routes.SmallPicturesPath, trackConfiguration.PicturesSizes.SmallPictureWidth, @@ -116,15 +120,14 @@ namespace Musify.Application.Tracks.Handler async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken) { - var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, - Guid.NewGuid().ToString()); + var destinationFolderAudio = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString()); try { await eventBus.PublishAsync(new UpdateTrackAudioEvent( track.Id, storageConfiguration.Bucket, - Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), + trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), storageConfiguration.Bucket, destinationFolderAudio), cancellationToken); } diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 8736249..5737708 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using Mediator; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; @@ -14,7 +15,7 @@ namespace Musify.Application.Tracks.Handler { public async ValueTask Handle(DeleteTrackCommand request, CancellationToken cancellationToken) { - var track = await database.Tracks.FindAsync(request.TrackId, cancellationToken); + var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken); if (track is null) { logger.LogInformation("Track {TrackId} not found", request.TrackId); @@ -35,33 +36,40 @@ namespace Musify.Application.Tracks.Handler async Task RemovePictures(Track track, CancellationToken cancellationToken) { - await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, track.OriginalPictureName), + await RemoveFile(trackConfiguration.Routes.BuildOriginalPicturePath(track.OriginalPictureName), cancellationToken); if (track.SmallPictureName != trackConfiguration.Routes.PresetSmallPicture) - await RemoveFile(Path.Combine(trackConfiguration.Routes.SmallPicturesPath, track.SmallPictureName), + await RemoveFile(trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName), cancellationToken); if (track.MediumPictureName != trackConfiguration.Routes.PresetMediumPicture) - await RemoveFile(Path.Combine(trackConfiguration.Routes.MediumPicturesPath, track.MediumPictureName), + await RemoveFile(trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName), cancellationToken); if (track.LargePictureName != trackConfiguration.Routes.PresetLargePicture) - await RemoveFile(Path.Combine(trackConfiguration.Routes.LargePicturesPath, track.LargePictureName), + await RemoveFile(trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName), cancellationToken); } async Task RemoveAudios(Track track, CancellationToken cancellationToken) { - await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), + await RemoveFile(trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), cancellationToken); - await RemoveFile(Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, track.AudioFolderName), + await RemoveFile(trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName), cancellationToken); } async Task RemoveFile(string path, CancellationToken cancellationToken) { - await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, path, cancellationToken); - logger.LogDebug("Removed file {Path} from storage", path); + try + { + await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, path, cancellationToken); + logger.LogDebug("Removed file {Path} from storage", path); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to remove file {Path} from storage", path); + } } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs index 2d4172b..f529bab 100644 --- a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs @@ -1,5 +1,6 @@ using Ardalis.Result; using Mediator; +using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; @@ -11,11 +12,11 @@ namespace Musify.Application.Tracks.Handler { public async ValueTask> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) { - var track = await database.Tracks.FindAsync(request.TrackId); + var track = await database.Tracks.AsNoTracking() + .Select(t => TrackResponse.FromEntity(t)) + .SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken); - return track is null - ? Result.NotFound() - : Result.Success(TrackResponse.FromEntity(track)); + return track ?? Result.NotFound(); } } } \ No newline at end of file From 5761f6da693d6f405ccc5c06d2d328985d745fe6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 9 May 2026 19:36:49 +0200 Subject: [PATCH 128/172] Remove keycloak and add missing functions --- .../Application/PaginatedResponse.cs | 19 +++++- .../Infrastructure/IKeycloakUserService.cs | 21 ------ .../GetPlayListsByUserIdQueryHandler.cs | 11 +-- .../Handlers/GetPlayListsQueryHandler.cs | 11 +-- .../Handler/CreateTrackCommandHandler.cs | 2 +- .../Handler/GetTracksByUserIdQueryHandler.cs | 11 +-- .../Tracks/Handler/GetTracksQueryHandler.cs | 12 +--- .../Users/Commands/CreateUserCommand.cs | 11 ++- .../Users/Commands/DeleteUserCommand.cs | 8 --- .../Handlers/CreateUserCommandHandler.cs | 20 +----- .../Handlers/DeleteUserCommandHandler.cs | 22 ------ .../Users/Handlers/GetUserByIdQueryHandler.cs | 23 +------ .../Handlers/GetUserByKeycloakQueryHandler.cs | 23 +------ .../Users/Handlers/GetUsersQueryHandler.cs | 52 +++++--------- Application/Users/Responses/UserResponse.cs | 24 +++++-- Domain/Entities/User.cs | 14 ++++ .../InfrastructureStorageConfiguration.cs | 4 +- .../Configuration/KeycloakConfiguration.cs | 21 ------ .../OpenTelemetryConfiguration.cs | 2 +- Infrastructure/Infrastructure.csproj | 1 - .../Services/KeycloakUserService.cs | 67 ------------------- .../Services/ServicesDependencyInjection.cs | 23 ------- 22 files changed, 89 insertions(+), 313 deletions(-) delete mode 100644 Application/Contracts/Infrastructure/IKeycloakUserService.cs delete mode 100644 Application/Users/Commands/DeleteUserCommand.cs delete mode 100644 Application/Users/Handlers/DeleteUserCommandHandler.cs delete mode 100644 Infrastructure/Configuration/KeycloakConfiguration.cs delete mode 100644 Infrastructure/Services/KeycloakUserService.cs diff --git a/Application/Contracts/Application/PaginatedResponse.cs b/Application/Contracts/Application/PaginatedResponse.cs index 8106a4b..9f9b5ac 100644 --- a/Application/Contracts/Application/PaginatedResponse.cs +++ b/Application/Contracts/Application/PaginatedResponse.cs @@ -1,4 +1,6 @@ -namespace Musify.Application.Contracts.Application +using X.PagedList; + +namespace Musify.Application.Contracts.Application { public record PaginatedResponse( IReadOnlyCollection Items, @@ -7,5 +9,18 @@ int PageCount, int TotalItemCount, bool HasNextPage, - bool HasPreviousPage); + bool HasPreviousPage) + { + public static PaginatedResponse FromPagedList(IPagedList pagedList) + { + return new PaginatedResponse( + pagedList.ToArray(), + pagedList.PageNumber, + pagedList.PageSize, + pagedList.PageCount, + pagedList.TotalItemCount, + pagedList.HasNextPage, + pagedList.HasPreviousPage); + } + } } diff --git a/Application/Contracts/Infrastructure/IKeycloakUserService.cs b/Application/Contracts/Infrastructure/IKeycloakUserService.cs deleted file mode 100644 index d11728b..0000000 --- a/Application/Contracts/Infrastructure/IKeycloakUserService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Musify.Application.Users.Responses; - -namespace Musify.Application.Contracts.Infrastructure -{ - public interface IKeycloakUserService - { - Task GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken); - - Task> GetUsersAsync( - CancellationToken cancellationToken, - string search = "", - int first = 0, - int max = 20, - string username = ""); - - Task GetUsersCountAsync( - CancellationToken cancellationToken, - string search = "", - string username = ""); - } -} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index 29ea4cf..3803418 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -32,16 +32,7 @@ namespace Musify.Application.PlayLists.Handlers .Select(p => PlayListResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var response = new PaginatedResponse( - pagedPlayLists.ToArray(), - pagedPlayLists.PageNumber, - pagedPlayLists.PageSize, - pagedPlayLists.PageCount, - pagedPlayLists.TotalItemCount, - pagedPlayLists.HasNextPage, - pagedPlayLists.HasPreviousPage); - - return Result.Success(response); + return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index 308d32b..7eb435c 100644 --- a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -25,16 +25,7 @@ namespace Musify.Application.PlayLists.Handlers .Select(p => PlayListResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var response = new PaginatedResponse( - pagedPlayLists.ToArray(), - pagedPlayLists.PageNumber, - pagedPlayLists.PageSize, - pagedPlayLists.PageCount, - pagedPlayLists.TotalItemCount, - pagedPlayLists.HasNextPage, - pagedPlayLists.HasPreviousPage); - - return Result.Success(response); + return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 68c79ec..1144f76 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -42,7 +42,7 @@ namespace Musify.Application.Tracks.Handler await database.Tracks.AddAsync(trackEntity, cancellationToken); await database.SaveChangesAsync(cancellationToken); - return Result.Success(); + return Result.Created(TrackResponse.FromEntity(trackEntity)); } async Task UploadFilesAsync( diff --git a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs index 54b8973..3b107c6 100644 --- a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs @@ -33,16 +33,7 @@ namespace Musify.Application.Tracks.Handler .Select(t => TrackResponse.FromEntity(t.Track)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var response = new PaginatedResponse( - pagedTracks.ToArray(), - pagedTracks.PageNumber, - pagedTracks.PageSize, - pagedTracks.PageCount, - pagedTracks.TotalItemCount, - pagedTracks.HasNextPage, - pagedTracks.HasPreviousPage); - - return Result.Success(response); + return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/GetTracksQueryHandler.cs b/Application/Tracks/Handler/GetTracksQueryHandler.cs index 7cc21cb..8fc47f9 100644 --- a/Application/Tracks/Handler/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksQueryHandler.cs @@ -18,7 +18,6 @@ namespace Musify.Application.Tracks.Handler var pageSize = request.PageSize < 1 ? 10 : request.PageSize; var totalCount = await database.PlayLists.CountAsync(cancellationToken); - var pagedPlayLists = await database.UserHasTracks .AsNoTracking() .OrderBy(t => t.Id) @@ -26,16 +25,7 @@ namespace Musify.Application.Tracks.Handler .Select(t => TrackResponse.FromEntity(t.Track)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var response = new PaginatedResponse( - pagedPlayLists.ToArray(), - pagedPlayLists.PageNumber, - pagedPlayLists.PageSize, - pagedPlayLists.PageCount, - pagedPlayLists.TotalItemCount, - pagedPlayLists.HasNextPage, - pagedPlayLists.HasPreviousPage); - - return Result.Success(response); + return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } } } \ No newline at end of file diff --git a/Application/Users/Commands/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs index 2f7d89b..1b0224e 100644 --- a/Application/Users/Commands/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -5,14 +5,21 @@ using Musify.Domain.Entities; namespace Musify.Application.Users.Commands { - public record CreateUserCommand(Guid Id) + public record CreateUserCommand(Guid Id, + string Name, + string? FirstName, + string? SecondName) : ICommand> { public static User ToEntity(CreateUserCommand command) { return new User { - Id = command.Id + Id = command.Id, + Name = command.Name, + NormalizedName = command.Name.ToUpperInvariant(), + FirstName = command.FirstName, + SecondName = command.SecondName }; } } diff --git a/Application/Users/Commands/DeleteUserCommand.cs b/Application/Users/Commands/DeleteUserCommand.cs deleted file mode 100644 index 5a64998..0000000 --- a/Application/Users/Commands/DeleteUserCommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Ardalis.Result; -using Mediator; - -namespace Musify.Application.Users.Commands -{ - public record DeleteUserCommand(Guid Id) - : ICommand; -} \ No newline at end of file diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index aae7176..9d9e097 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -8,7 +8,7 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { - public class CreateUserCommandHandler(IDatabase database, IKeycloakUserService keycloakUserClient, ILogger logger) + public class CreateUserCommandHandler(IDatabase database, ILogger logger) : ICommandHandler > { public async ValueTask> Handle(CreateUserCommand request, CancellationToken cancellationToken) @@ -20,28 +20,12 @@ namespace Musify.Application.Users.Handlers return Result.Conflict($"User {request.Id} already exists"); } - KeycloakUserResponse keycloakUser; - try - { - keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); - } - catch - { - return Result.NotFound("Keycloak user not found"); - } - var newUser = CreateUserCommand.ToEntity(request); await database.Users.AddAsync(newUser, cancellationToken); await database.SaveChangesAsync(cancellationToken); logger.LogInformation("Created user {UserId}", newUser.Id); - return Result.Created(new UserResponse( - newUser.Id.ToString(), - keycloakUser.Name, - keycloakUser.FirstName, - keycloakUser.SecondName, - newUser.CreatedAt, - newUser.UpdatedAt)); + return Result.Created(UserResponse.FromEntity(newUser)); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/DeleteUserCommandHandler.cs b/Application/Users/Handlers/DeleteUserCommandHandler.cs deleted file mode 100644 index 86d9c62..0000000 --- a/Application/Users/Handlers/DeleteUserCommandHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Ardalis.Result; -using Mediator; -using Musify.Application.Contracts.Infrastructure; - -namespace Musify.Application.Users.Commands -{ - public class DeleteUserCommandHandler(IDatabase database) - : ICommandHandler - { - public async ValueTask Handle(DeleteUserCommand request, CancellationToken cancellationToken) - { - var existingUser = await database.Users.FindAsync(request.Id, cancellationToken); - if (existingUser is null) - return Result.NotFound(); - - database.Users.Remove(existingUser); - await database.SaveChangesAsync(cancellationToken); - - return Result.NoContent(); - } - } -} \ No newline at end of file diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index bc9a311..fe92751 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -7,34 +7,17 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { - public class GetUserByIdQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) + public class GetUserByIdQueryHandler(IDatabase database) : IQueryHandler > { public async ValueTask> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { var user = await database.Users .AsNoTracking() + .Select(u => UserResponse.FromEntity(u)) .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - if (user is null) - return Result.NotFound("User not found"); - KeycloakUserResponse keycloakUser; - try - { - keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); - } - catch - { - return Result.Error("Keycloak user not found"); - } - - return Result.Success(new UserResponse( - user.Id.ToString(), - keycloakUser.Name, - keycloakUser.FirstName, - keycloakUser.SecondName, - user.CreatedAt, - user.UpdatedAt)); + return user ?? Result.NotFound(); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index 4dbd18b..f46fb00 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -7,34 +7,17 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { - public class GetUserByKeycloakQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) + public class GetUserByKeycloakQueryHandler(IDatabase database) : IQueryHandler > { public async ValueTask> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) { var user = await database.Users .AsNoTracking() + .Select(u => UserResponse.FromEntity(u)) .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - if (user is null) - return Result.NotFound("User not found"); - KeycloakUserResponse keycloakUser; - try - { - keycloakUser = await keycloakUserClient.GetUserByIdAsync(user.Id.ToString(), cancellationToken); - } - catch - { - return Result.NotFound("Keycloak user not found"); - } - - return Result.Success(new UserResponse( - user.Id.ToString(), - keycloakUser.Name, - keycloakUser.FirstName, - keycloakUser.SecondName, - user.CreatedAt, - user.UpdatedAt)); + return user ?? Result.NotFound(); } } } diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index d52a32e..3f807d8 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -5,12 +5,12 @@ using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; -using X.PagedList; +using X.PagedList.EF; using X.PagedList.Extensions; namespace Musify.Application.Users.Handlers { - public class GetUsersQueryHandler(IDatabase database, IKeycloakUserService keycloakUserClient) + public class GetUsersQueryHandler(IDatabase database) : IQueryHandler > > { public async ValueTask >> Handle(GetUsersQuery request, CancellationToken cancellationToken) @@ -19,43 +19,21 @@ namespace Musify.Application.Users.Handlers var pageSize = request.PageSize < 1 ? 10 : request.PageSize; var first = (pageNumber - 1) * pageSize; - var keycloakUsers = await keycloakUserClient.GetUsersAsync( - cancellationToken, - first: first, - max: pageSize, - search: request.UsernameSearch); + var user = database.Users + .AsNoTracking() + .AsQueryable(); + if (!string.IsNullOrEmpty(request.UsernameSearch)) + { + var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant(); + user = user.Where(u => u.NormalizedName.Contains(normalizedUsername)); + } - var keycloakIds = keycloakUsers.Select(u => u.Id).ToList(); + var totalCount = await user.CountAsync(cancellationToken); + var pagedUser = await user + .Select(u => UserResponse.FromEntity(u)) + .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - var databaseUsers = await database.Users - .Where(u => keycloakIds.Contains(u.Id)) - .ToListAsync(cancellationToken); - - var users = databaseUsers - .Join(keycloakUsers, db => db.Id, kc => kc.Id, - (databaseSource, keycloakSource) => new UserResponse( - keycloakSource.Id.ToString(), - keycloakSource.Name, - keycloakSource.FirstName, - keycloakSource.SecondName, - databaseSource.CreatedAt, - databaseSource.UpdatedAt)) - .ToArray(); - - var totalItemCount = await keycloakUserClient.GetUsersCountAsync(cancellationToken, search: request.UsernameSearch); - - var pagedUsers = new StaticPagedList(users, pageNumber, pageSize, totalItemCount); - - var response = new PaginatedResponse( - pagedUsers.ToArray(), - pagedUsers.PageNumber, - pagedUsers.PageSize, - pagedUsers.PageCount, - pagedUsers.TotalItemCount, - pagedUsers.HasNextPage, - pagedUsers.HasPreviousPage); - - return Result.Success(response); + return Result.Success(PaginatedResponse.FromPagedList(pagedUser)); } } } \ No newline at end of file diff --git a/Application/Users/Responses/UserResponse.cs b/Application/Users/Responses/UserResponse.cs index 758153f..054af69 100644 --- a/Application/Users/Responses/UserResponse.cs +++ b/Application/Users/Responses/UserResponse.cs @@ -1,10 +1,24 @@ -namespace Musify.Application.Users.Responses +using Musify.Domain.Entities; + +namespace Musify.Application.Users.Responses { public record UserResponse( - string Id, + Guid Id, string Name, - string FirstName, - string SecondName, + string? FirstName, + string? SecondName, DateTime CreatedAt, - DateTime UpdatedAt); + DateTime UpdatedAt) + { + public static UserResponse FromEntity(User user) + { + return new UserResponse( + user.Id, + user.Name, + user.FirstName, + user.SecondName, + user.CreatedAt, + user.UpdatedAt); + } + } } \ No newline at end of file diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index eca06c3..c0be246 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -9,6 +9,20 @@ namespace Musify.Domain.Entities [Key] public required Guid Id { get; set; } + [Required] + [MaxLength(48)] + public required string Name { get; set; } + + [Required] + [MaxLength(48)] + public required string NormalizedName { get; set; } + + [MaxLength(48)] + public string? FirstName { get; set; } + + [MaxLength(48)] + public string? SecondName { get; set; } + [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs b/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs index 4321c01..2c2afb7 100644 --- a/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs +++ b/Infrastructure/Configuration/InfrastructureStorageConfiguration.cs @@ -6,16 +6,14 @@ namespace Musify.Infrastructure.Configuration { public const string SectionName = "InfrastructureStorage"; - [Required] [Url] + [Required] public required string Address { get; set; } [Required] - [MinLength(3)] public required string AccessKey { get; set; } [Required] - [MinLength(8)] public required string SecretAccessKey { get; set; } [Required] diff --git a/Infrastructure/Configuration/KeycloakConfiguration.cs b/Infrastructure/Configuration/KeycloakConfiguration.cs deleted file mode 100644 index 7c1e1d4..0000000 --- a/Infrastructure/Configuration/KeycloakConfiguration.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Musify.Infrastructure.Configuration -{ - public class KeycloakConfiguration - { - public const string SectionName = "Keycloak"; - - [Required] - public required string Address { get; set; } - - [Required] - public required string Realm { get; set; } - - [Required] - public required string Username { get; set; } - - [Required] - public required string Password { get; set; } - } -} \ No newline at end of file diff --git a/Infrastructure/Configuration/OpenTelemetryConfiguration.cs b/Infrastructure/Configuration/OpenTelemetryConfiguration.cs index ad7a232..e435db1 100644 --- a/Infrastructure/Configuration/OpenTelemetryConfiguration.cs +++ b/Infrastructure/Configuration/OpenTelemetryConfiguration.cs @@ -6,8 +6,8 @@ namespace Musify.Infrastructure.Configuration { public const string SectionName = "OpenTelemetry"; - [Required] [Url] + [Required] public required string OtlpEndpoint { get; set; } } } diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 8c26de5..30d947b 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -15,7 +15,6 @@ - diff --git a/Infrastructure/Services/KeycloakUserService.cs b/Infrastructure/Services/KeycloakUserService.cs deleted file mode 100644 index 8705aeb..0000000 --- a/Infrastructure/Services/KeycloakUserService.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Keycloak.Net; -using Keycloak.Net.Models.Users; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Users.Responses; -using Musify.Infrastructure.Configuration; - -namespace Musify.Infrastructure.Services -{ - public class KeycloakUserService(KeycloakClient keycloakClient, ILogger logger, - KeycloakConfiguration keycloakConfiguration) : IKeycloakUserService - { - public async Task GetUserByIdAsync(string keycloakId, CancellationToken cancellationToken) - { - User keycloakUser; - - try - { - keycloakUser = await keycloakClient.GetUserAsync(keycloakConfiguration.Realm, keycloakId, cancellationToken); - } - catch (OperationCanceledException) - { - logger.LogWarning("Operation to retrieve Keycloak user {KeycloakId} was cancelled", keycloakId); - throw; - } - catch (Exception exception) - { - logger.LogWarning(exception, "Failed to retrieve Keycloak user {KeycloakId}", keycloakId); - throw; - } - return ToResponse(keycloakUser); - } - - public async Task> GetUsersAsync(CancellationToken cancellationToken, string search = "", int first = 0, int max = 20, string username = "") - { - var users = await keycloakClient.GetUsersAsync( - realm: keycloakConfiguration.Realm, - first: first, - max: max, - search: search, - username: username, - cancellationToken: cancellationToken); - - return users - .Select(ToResponse) - .ToArray(); - } - - public Task GetUsersCountAsync(CancellationToken cancellationToken, string search = "", string username = "") - { - return keycloakClient.GetUsersCountAsync( - keycloakConfiguration.Realm, - search: search, - 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.")); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 8b13651..6d76ccf 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -1,5 +1,4 @@ using Amazon.S3; -using Keycloak.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -42,28 +41,6 @@ namespace Musify.Infrastructure.Services return serviceDescriptors; } - public static IServiceCollection AddKeycloakService(this IServiceCollection serviceDescriptors, IConfiguration configuration) - { - serviceDescriptors.AddOptionsWithValidateOnStart() - .Bind(configuration.GetRequiredSection(KeycloakConfiguration.SectionName)) - .ValidateDataAnnotations(); - - serviceDescriptors.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService>().Value); - - serviceDescriptors.AddScoped(serviceProvider => - { - var keycloakConfiguration = serviceProvider.GetRequiredService(); - - return new KeycloakClient( - keycloakConfiguration.Address, - keycloakConfiguration.Username, - keycloakConfiguration.Password); - } ); - serviceDescriptors.AddScoped(); - return serviceDescriptors; - } - public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors From d0e6eaaee8db1c0563b9f5cc97b7e37d99893324 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 13 May 2026 02:20:22 +0200 Subject: [PATCH 129/172] Reworks --- .../Infrastructure/IStorageService.cs | 2 + Application/Events/DeleteTrackEvent.cs | 6 + .../Handler/DeleteTrackCommandHandler.cs | 62 +++------- Domain/Entities/ProcessingStatus.cs | 10 ++ Domain/Entities/Track.cs | 34 ++++-- .../GenerateAudioWorkflowPathsActivity.cs | 53 ++++---- .../Audio/TranscodeDashAudioActivity.cs | 62 +++++----- .../Audio/UpdateTrackAudioActivity.cs | 101 ++++++++------- .../Files/DownloadFileFromBucketActivity.cs | 4 +- .../Files/RemoveFileFromBucketActivity.cs | 25 +--- .../Files/RemoveFolderFromBucketActivity.cs | 42 +++++++ .../Pictures/UpdateTrackPictureActivity.cs | 19 +-- .../Activities/Tracks/DeleteTrackActivity.cs | 115 ++++++++++++++++++ .../Activities/Tracks/DeleteTrackArguments.cs | 6 + .../GenerateAudioWorkflowPathsArguments.cs | 1 + .../RemoveFolderFromBucketArguments.cs | 6 + .../Arguments/TranscodeDashAudioArguments.cs | 1 + .../Arguments/UpdateTrackAudioArguments.cs | 2 +- .../Consumers/DeleteTrackConsumer.cs | 23 ++++ .../Consumers/RemoveFileConsumer.cs | 23 ---- .../MassTransitDependencyInjection.cs | 12 +- .../MassTransit/Logs/UpdateTrackAudioLog.cs | 2 +- .../MassTransit/RoutingSlip/ActivityNames.cs | 1 + .../AudioWorkflowRoutingSlipBuilder.cs | 5 +- ...er.cs => DeleteTrackRoutingSlipBuilder.cs} | 16 +-- .../Variables/RoutingSlipVariableNames.cs | 1 + Infrastructure/Services/StorageService.cs | 38 ++++++ 27 files changed, 445 insertions(+), 227 deletions(-) create mode 100644 Application/Events/DeleteTrackEvent.cs create mode 100644 Domain/Entities/ProcessingStatus.cs create mode 100644 Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs create mode 100644 Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs create mode 100644 Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs delete mode 100644 Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs rename Infrastructure/MassTransit/RoutingSlip/Builders/{RemoveFileFromBucketRoutingSlipBuilder.cs => DeleteTrackRoutingSlipBuilder.cs} (55%) diff --git a/Application/Contracts/Infrastructure/IStorageService.cs b/Application/Contracts/Infrastructure/IStorageService.cs index beb5c85..3127c35 100644 --- a/Application/Contracts/Infrastructure/IStorageService.cs +++ b/Application/Contracts/Infrastructure/IStorageService.cs @@ -16,5 +16,7 @@ Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, CancellationToken cancellationToken); + + Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/DeleteTrackEvent.cs b/Application/Events/DeleteTrackEvent.cs new file mode 100644 index 0000000..2f85a6a --- /dev/null +++ b/Application/Events/DeleteTrackEvent.cs @@ -0,0 +1,6 @@ +namespace Musify.Application.Events +{ + public record DeleteTrackEvent( + Guid TrackId, + Guid UserId); +} diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 5737708..fff04b9 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -2,15 +2,17 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Configuration; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Commands; using Musify.Domain.Entities; +using Musify.Application.Events; namespace Musify.Application.Tracks.Handler { - public class DeleteTrackCommandHandler(IDatabase database, IStorageService storageHandler, ILogger logger, - TrackConfiguration trackConfiguration, ApplicationStorageConfiguration storageConfiguration) + public class DeleteTrackCommandHandler( + IDatabase database, + IEventBus eventBus, + ILogger logger) : ICommandHandler { public async ValueTask Handle(DeleteTrackCommand request, CancellationToken cancellationToken) @@ -28,48 +30,22 @@ namespace Musify.Application.Tracks.Handler return Result.Unauthorized(); } - await RemovePictures(track, cancellationToken); - await RemoveAudios(track, cancellationToken); + if (track.AudioTranscodeProcessingStatus == ProcessingStatus.Processing + || track.AudioTranscodeProcessingStatus == ProcessingStatus.Pending) + { + logger.LogWarning("Track {TrackId} is currently being processed and cannot be deleted", request.TrackId); + return Result.Conflict("Track is currently being processed and cannot be deleted"); + } + + track.PicturesProcessingStatus = ProcessingStatus.Processing; + track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing; + await database.SaveChangesAsync(cancellationToken); + + await eventBus.PublishAsync( + new DeleteTrackEvent(track.Id, request.UserId), + cancellationToken); return Result.NoContent(); } - - async Task RemovePictures(Track track, CancellationToken cancellationToken) - { - await RemoveFile(trackConfiguration.Routes.BuildOriginalPicturePath(track.OriginalPictureName), - cancellationToken); - - if (track.SmallPictureName != trackConfiguration.Routes.PresetSmallPicture) - await RemoveFile(trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName), - cancellationToken); - if (track.MediumPictureName != trackConfiguration.Routes.PresetMediumPicture) - await RemoveFile(trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName), - cancellationToken); - if (track.LargePictureName != trackConfiguration.Routes.PresetLargePicture) - await RemoveFile(trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName), - cancellationToken); - - } - - async Task RemoveAudios(Track track, CancellationToken cancellationToken) - { - await RemoveFile(trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), - cancellationToken); - await RemoveFile(trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName), - cancellationToken); - } - - async Task RemoveFile(string path, CancellationToken cancellationToken) - { - try - { - await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, path, cancellationToken); - logger.LogDebug("Removed file {Path} from storage", path); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to remove file {Path} from storage", path); - } - } } } \ No newline at end of file diff --git a/Domain/Entities/ProcessingStatus.cs b/Domain/Entities/ProcessingStatus.cs new file mode 100644 index 0000000..583ac67 --- /dev/null +++ b/Domain/Entities/ProcessingStatus.cs @@ -0,0 +1,10 @@ +namespace Musify.Domain.Entities +{ + public enum ProcessingStatus + { + Pending = 0, + Processing = 1, + Completed = 2, + Failed = 3 + } +} \ No newline at end of file diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index b1f9592..82fa295 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; namespace Musify.Domain.Entities { @@ -20,24 +22,32 @@ namespace Musify.Domain.Entities [MaxLength(64)] public required string OriginalPictureName { get; set; } - [Required] [MaxLength(64)] - public required string SmallPictureName { get; set; } + public string? SmallPictureName { get; set; } - [Required] [MaxLength(64)] - public required string MediumPictureName { get; set; } + public string? MediumPictureName { get; set; } - [Required] [MaxLength(64)] - public required string LargePictureName { get; set; } + public string? LargePictureName { get; set; } [Required] [MaxLength(64)] public required string OriginalAudioName { get; set; } [MaxLength(64)] - public string AudioFolderName { get; set; } = string.Empty; + public string? AudioFolderName { get; set; } + + [Required] + public ProcessingStatus PicturesProcessingStatus { get; set; } = ProcessingStatus.Pending; + + [Required] + public ProcessingStatus AudioTranscodeProcessingStatus { get; set; } = ProcessingStatus.Pending; + + [Required] + public int RetryCount { get; set; } = 0; + + public DateTime? LastRetryAt { get; set; } [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; @@ -48,5 +58,15 @@ namespace Musify.Domain.Entities public ICollection UserTracks { get; set; } = new List(); public ICollection PlayListTracks { get; set; } = new List(); + + [NotMapped] + [MemberNotNullWhen(true, nameof(SmallPictureName)) ] + [MemberNotNullWhen(true, nameof(MediumPictureName)) ] + [MemberNotNullWhen(true, nameof(LargePictureName)) ] + public bool IsPicturesProcessed => PicturesProcessingStatus == ProcessingStatus.Completed; + + [NotMapped] + [MemberNotNullWhen(true, nameof(AudioFolderName)) ] + public bool IsAudioProcessed => AudioTranscodeProcessingStatus == ProcessingStatus.Completed; } } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 6b0a597..73f4dcc 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -1,4 +1,5 @@ using MassTransit; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -6,42 +7,41 @@ using Musify.Infrastructure.MassTransit.Activities.Arguments; namespace Musify.Infrastructure.MassTransit.Activities { - public class GenerateAudioWorkflowPathsActivity( - ILogger logger, - IProcessTrackingStore processTrackingStore) + internal class GenerateAudioWorkflowPathsActivity( + IDatabase database, + ILogger logger) : IExecuteActivity { public const string ExecuteEndpointName = "generate-audio-workflow-paths"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(GenerateAudioWorkflowPathsActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); + Track track; + try + { + var getTrack = await database.Tracks.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found."); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to retrieve track for TrackId {TrackId}", + executeContext.Arguments.TrackId); + throw; + } try { + track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing; var folderName = Guid.NewGuid().ToString(); var sourceFileName = Path.GetFileName(executeContext.Arguments.SourceKey); var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, folderName); var sourceFilePath = Path.Combine(workingDirectory, sourceFileName); Directory.CreateDirectory(workingDirectory); + await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogDebug("Generated audio workflow paths in {WorkingDirectory}", - workingDirectory); + logger.LogDebug("Generated audio workflow paths in {WorkingDirectory}", workingDirectory); - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new Dictionary { [RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory, @@ -54,9 +54,20 @@ namespace Musify.Infrastructure.MassTransit.Activities { logger.LogError(exception, "Failed to generate audio workflow paths for {SourceKey}", executeContext.Arguments.SourceKey); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + + try + { + track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + await database.SaveChangesAsync(executeContext.CancellationToken); + } + catch (Exception dbException) + { + logger.LogError(dbException, "Failed to update ProcessingStatus to Failed for track {TrackId}", + executeContext.Arguments.TrackId); + } + throw; } } } -} +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index c14fe92..4766ae5 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -1,4 +1,5 @@ using MassTransit; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -7,54 +8,59 @@ using Musify.Infrastructure.MassTransit.Activities.Logs; namespace Musify.Infrastructure.MassTransit.Activities { - public class TranscodeDashAudioActivity( + internal class TranscodeDashAudioActivity( + IDatabase database, IAudioTranscoderService audioTranscoder, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "transcode-dash-audio"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(TranscodeDashAudioActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var workingDirectory = executeContext.GetVariable(executeContext.Arguments.WorkingDirectoryVariable); ArgumentNullException.ThrowIfNull(workingDirectory, nameof(workingDirectory)); + Track track; try { - await using (var fileStream = File.OpenRead(sourceFilePath)) - { - await audioTranscoder.TranscodeToDashAsync( - fileStream, - workingDirectory, - executeContext.CancellationToken); - } + var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + track = getTrack ?? throw new InvalidOperationException($"Track with Id {executeContext.Arguments.TrackId} not found"); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to retrieve track with Id {TrackId} from database", executeContext.Arguments.TrackId); + throw; + } + + try + { + await using var fileStream = File.OpenRead(sourceFilePath); + await audioTranscoder.TranscodeToDashAsync( + fileStream, + workingDirectory, + executeContext.CancellationToken); File.Delete(sourceFilePath); - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { - logger.LogError(exception, "Failed to transcode {SourceFilePath}", - sourceFilePath); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + logger.LogError(exception, "Failed to transcode {SourceFilePath}", sourceFilePath); + + try + { + track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + await database.SaveChangesAsync(executeContext.CancellationToken); + } + catch (Exception dbException) + { + logger.LogError(dbException, "Failed to update ProcessingStatus to Failed for track {TrackId}", + executeContext.Arguments.TrackId); + } + throw; } } diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 7521aef..cf32e81 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -1,4 +1,5 @@ using MassTransit; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; @@ -7,63 +8,76 @@ using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities.Audio { - public class UpdateTrackAudioActivity( + internal class UpdateTrackAudioActivity( IDatabase database, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "update-track-audio"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); + Track track; + try + { + var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to retrieve track {TrackId}", + executeContext.Arguments.TrackId); + throw; + } - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(UpdateTrackAudioActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); + string? audioFolderKey; + try + { + audioFolderKey = executeContext.GetVariable(executeContext.Arguments.AudioFolderKeyVariable); + logger.LogDebug("Resolved audio folder key: {AudioFolderKey}", audioFolderKey); + } + catch (Exception exception) + { + logger.LogWarning(exception, "Audio folder key variable could not be resolved for track {TrackId}", track.Id); + return executeContext.Completed(); + } + + if (string.IsNullOrWhiteSpace(audioFolderKey)) + { + logger.LogWarning("Audio folder key is empty for track {TrackId}; skipping update", track.Id); + return executeContext.Completed(); + } try { - var track = await database.Tracks.FindAsync( - [executeContext.Arguments.TrackId], - cancellationToken: executeContext.CancellationToken); - - if (track is null) - { - logger.LogWarning("Track {TrackId} not found", - executeContext.Arguments.TrackId); - throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); - } - var log = new UpdateTrackAudioLog( track.Id, track.AudioFolderName); - track.AudioFolderName = Path.GetFileName(executeContext.Arguments.AudioFolderKey); - - database.Tracks.Update(track); + track.AudioFolderName = Path.GetFileName(audioFolderKey); + track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; await database.SaveChangesAsync(executeContext.CancellationToken); - logger.LogInformation("Updated track {TrackId} audio", - executeContext.Arguments.TrackId); + logger.LogInformation("Updated track {TrackId} audio", executeContext.Arguments.TrackId); - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); - return executeContext.Completed(); + return executeContext.Completed(log); } catch (Exception exception) { logger.LogError(exception, "Failed to update track {TrackId} audio", executeContext.Arguments.TrackId); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + + try + { + track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + await database.SaveChangesAsync(executeContext.CancellationToken); + } + catch (Exception dbException) + { + logger.LogError(dbException, "Failed to update ProcessingStatus to Failed for track {TrackId}", + executeContext.Arguments.TrackId); + } + throw; } } @@ -72,25 +86,22 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio { try { - var track = await database.Tracks.FindAsync( - [compensateContext.Log.TrackId], - cancellationToken: compensateContext.CancellationToken); - + var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == compensateContext.Log.TrackId, compensateContext.CancellationToken); if (track is null) - { - return compensateContext.Compensated(); - } + throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found"); - track.AudioFolderName = compensateContext.Log.PreviousFolderAudioKey; - - database.Tracks.Update(track); + track.AudioFolderName = compensateContext.Log.PreviousAudioFolderName; + track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(compensateContext.CancellationToken); + logger.LogInformation("Compensated track {TrackId} audio", compensateContext.Log.TrackId); + return compensateContext.Compensated(); } catch (Exception exception) { - logger.LogError(exception, "Failed to compensate track {TrackId} audio", compensateContext.Log.TrackId); + logger.LogError(exception, "Failed to compensate track {TrackId} audio", + compensateContext.Log.TrackId); return compensateContext.Failed(exception); } } diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index df6bd52..9bf4475 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -5,9 +5,9 @@ using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; -namespace Musify.Infrastructure.MassTransit.Activities +namespace Musify.Infrastructure.MassTransit.Activities.Files { - public class DownloadFileFromBucketActivity( + internal class DownloadFileFromBucketActivity( IStorageService storageHandler, ILogger logger, IProcessTrackingStore processTrackingStore) diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index e1ddbf8..9f8b29f 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -1,43 +1,25 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; -namespace Musify.Infrastructure.MassTransit.Activities +namespace Musify.Infrastructure.MassTransit.Activities.Files { - public class RemoveFileFromBucketActivity( + internal class RemoveFileFromBucketActivity( IStorageService storageHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IExecuteActivity { public const string ExecuteEndpointName = "remove-file-from-bucket"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(RemoveFileFromBucketActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - try { await storageHandler.RemoveFileAsync( executeContext.Arguments.Bucket, executeContext.Arguments.Key, executeContext.CancellationToken); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) @@ -45,7 +27,6 @@ namespace Musify.Infrastructure.MassTransit.Activities logger.LogError(exception, "Failed to remove {Key} from bucket {Bucket}", executeContext.Arguments.Key, executeContext.Arguments.Bucket); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs new file mode 100644 index 0000000..98f1289 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs @@ -0,0 +1,42 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.MassTransit.Arguments; + +namespace Musify.Infrastructure.MassTransit.Activities.Files +{ + internal class RemoveFolderFromBucketActivity( + IStorageService storageHandler, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "remove-folder-from-bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + var folderKey = executeContext.GetVariable(executeContext.Arguments.FolderKeyVariable); + ArgumentException.ThrowIfNullOrEmpty(folderKey, nameof(folderKey)); + + try + { + await storageHandler.RemoveFolderAsync( + executeContext.Arguments.Bucket, + folderKey, + executeContext.CancellationToken); + + logger.LogInformation("Removed folder {FolderKey} from bucket {Bucket}", + folderKey, + executeContext.Arguments.Bucket); + + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to remove folder {FolderKey} from bucket {Bucket}", + folderKey, + executeContext.Arguments.Bucket); + throw; + } + } + } +} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 7b2463a..44392a1 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -9,28 +9,13 @@ namespace Musify.Infrastructure.MassTransit.Activities { public class UpdateTrackPictureActivity( IDatabase database, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "update-track-picture"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(UpdateTrackPictureActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariable); ArgumentNullException.ThrowIfNull(smallResizedVariable, nameof(smallResizedVariable)); var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariable); @@ -69,14 +54,12 @@ namespace Musify.Infrastructure.MassTransit.Activities logger.LogInformation("Updated track {TrackId} pictures", executeContext.Arguments.TrackId); - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { logger.LogError(exception, "Failed to update track {TrackId} pictures", executeContext.Arguments.TrackId); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs new file mode 100644 index 0000000..f58f263 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs @@ -0,0 +1,115 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal class DeleteTrackActivity( + IDatabase database, + IStorageService storageService, + TrackConfiguration trackConfiguration, + ApplicationStorageConfiguration storageConfiguration, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "delete-track"; + + public async Task Execute(ExecuteContext executeContext) + { + Track track; + try + { + var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to retrieve track {TrackId} for deletion", executeContext.Arguments.TrackId); + throw; + } + + try + { + await RemovePicturesAsync(track, executeContext.CancellationToken); + track.PicturesProcessingStatus = ProcessingStatus.Completed; + await database.SaveChangesAsync(executeContext.CancellationToken); + + await RemoveAudiosAsync(track, executeContext.CancellationToken); + track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; + await database.SaveChangesAsync(executeContext.CancellationToken); + + database.Tracks.Remove(track); + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Deleted track {TrackId}", track.Id); + return executeContext.Completed(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to delete track {TrackId}", executeContext.Arguments.TrackId); + + try + { + track.PicturesProcessingStatus = ProcessingStatus.Failed; + track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + await database.SaveChangesAsync(executeContext.CancellationToken); + } + catch (Exception dbException) + { + logger.LogError(dbException, "Failed to update processing statuses to Failed for track {TrackId}", executeContext.Arguments.TrackId); + } + + throw; + } + } + + async Task RemovePicturesAsync(Track track, CancellationToken cancellationToken) + { + await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalPicturePath(track.OriginalPictureName), cancellationToken); + + if (!track.IsPicturesProcessed) + { + logger.LogInformation( + "Track {TrackId} pictures are not fully processed, skipping removal of small, medium, and large pictures", + track.Id); + return; + } + + await RemoveFileAsync(trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName), cancellationToken); + await RemoveFileAsync(trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName), cancellationToken); + await RemoveFileAsync(trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName), cancellationToken); + } + + async Task RemoveAudiosAsync(Track track, CancellationToken cancellationToken) + { + await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), cancellationToken); + + if (!track.IsAudioProcessed) + { + logger.LogInformation( + "Track {TrackId} audio is not fully processed, skipping removal of processed audio", + track.Id); + return; + } + + await RemoveFileAsync(trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName), cancellationToken); + } + + async Task RemoveFileAsync(string key, CancellationToken cancellationToken) + { + try + { + logger.LogDebug("Removing file {Key} from bucket {Bucket}", key, storageConfiguration.Bucket); + await storageService.RemoveFileAsync(storageConfiguration.Bucket, key, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to remove file {Key} from bucket {Bucket}", key, storageConfiguration.Bucket); + throw; + } + } + } +} diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs new file mode 100644 index 0000000..0caacb8 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal record DeleteTrackArguments( + Guid TrackId, + Guid UserId); +} diff --git a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs index 44e4f07..2acd8f7 100644 --- a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs @@ -1,6 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record GenerateAudioWorkflowPathsArguments( + Guid TrackId, string TemporaryRootDirectory, string SourceKey); } diff --git a/Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs new file mode 100644 index 0000000..5628a4f --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record RemoveFolderFromBucketArguments( + string Bucket, + string FolderKeyVariable); +} diff --git a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs index c445504..379a14c 100644 --- a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs @@ -1,6 +1,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Arguments { public record TranscodeDashAudioArguments( + Guid TrackId, string SourceFilePathVariable, string WorkingDirectoryVariable); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs index 10edb2b..aabb5da 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs @@ -2,5 +2,5 @@ { public record UpdateTrackAudioArguments( Guid TrackId, - string AudioFolderKey); + string AudioFolderKeyVariable); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs b/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs new file mode 100644 index 0000000..b5c3ee1 --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class DeleteTrackConsumer( + IBus bus, + DeleteTrackRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "delete-track"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message.TrackId, consumeContext.Message.UserId, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs deleted file mode 100644 index bde736d..0000000 --- a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MassTransit; -using Musify.Application.Events; -using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; - -namespace Musify.Infrastructure.MassTransit.Consumers -{ - public class RemoveFileConsumer( - IBus bus, - RemoveFileFromBucketRoutingSlipBuilder routingSlipBuilder) - : IConsumer - { - public const string QueueName = "remove-file"; - - public async Task Consume(ConsumeContext consumeContext) - { - var routingSlip = routingSlipBuilder - .Build(consumeContext.Message, consumeContext.CorrelationId) - .Build(); - - await bus.Execute(routingSlip); - } - } -} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 059b1c4..9c0f695 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -14,6 +14,7 @@ using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.MassTransit.Filters; using Musify.Infrastructure.MassTransit.Logs; using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; +using Musify.Infrastructure.MassTransit.Activities.Tracks; namespace Musify.Infrastructure.MassTransit { @@ -61,19 +62,20 @@ namespace Musify.Infrastructure.MassTransit serviceDescriptors.AddScoped(); serviceDescriptors.AddScoped(); - serviceDescriptors.AddScoped(); + serviceDescriptors.AddScoped(); serviceDescriptors.AddMassTransit(options => { options.AddConsumer(); - options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); + options.AddExecuteActivity(); options.AddActivity(); options.AddActivity(); @@ -95,15 +97,15 @@ namespace Musify.Infrastructure.MassTransit busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - busFactoryConfigurator.ReceiveEndpoint(UpdateTrackAudioConsumer.QueueName, endpointConfigurator => endpointConfigurator.ConfigureConsumer(busRegistrationContext)); busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => endpointConfigurator.ConfigureConsumer(busRegistrationContext)); + busFactoryConfigurator.ReceiveEndpoint(DeleteTrackConsumer.QueueName, endpointConfigurator => + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); + busFactoryConfigurator.ReceiveEndpoint(RoutingSlipCleanUpConsumer.QueueName, endpointConfigurator => endpointConfigurator.ConfigureConsumer(busRegistrationContext)); diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs index 395670b..355a755 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs @@ -2,5 +2,5 @@ { public record UpdateTrackAudioLog( Guid TrackId, - string PreviousFolderAudioKey); + string? PreviousAudioFolderName); } diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 85033b7..b5d9ff5 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -2,6 +2,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip { internal static class ActivityNames { + internal const string DeleteTrack = "DeleteTrack"; internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths"; internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths"; internal const string DownloadFile = "DownloadFile"; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 4d96026..b5713b3 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -3,6 +3,7 @@ using MassTransit.Courier.Contracts; using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities.Audio; +using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.MassTransit.Activities; @@ -25,6 +26,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders ActivityNames.GenerateAudioWorkflowPaths, EndpointHelper.BuildExecuteActivityUri(GenerateAudioWorkflowPathsActivity.ExecuteEndpointName), new GenerateAudioWorkflowPathsArguments( + message.TrackId, workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKey)); @@ -40,6 +42,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders ActivityNames.TranscodeAudio, EndpointHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), new TranscodeDashAudioArguments( + message.TrackId, RoutingSlipVariableNames.Audio.SourceFilePath, RoutingSlipVariableNames.Workflow.TemporalDirectory)); @@ -56,7 +59,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders EndpointHelper.BuildExecuteActivityUri(UpdateTrackAudioActivity.ExecuteEndpointName), new UpdateTrackAudioArguments( message.TrackId, - message.DestinationFolderKey)); + RoutingSlipVariableNames.Audio.TranscodedDirectory)); return routingSlipBuilder; } diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs similarity index 55% rename from Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs rename to Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index c6cb98d..942254d 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/RemoveFileFromBucketRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -1,15 +1,13 @@ using MassTransit; using MassTransit.Courier.Contracts; -using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.Activities.Tracks; using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.MassTransit.Activities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { - public class RemoveFileFromBucketRoutingSlipBuilder + public class DeleteTrackRoutingSlipBuilder { - public RoutingSlipBuilder Build(RemoveFileEvent message, Guid? correlationId) + public RoutingSlipBuilder Build(Guid trackId, Guid userId, Guid? correlationId) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); @@ -19,11 +17,9 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); routingSlipBuilder.AddActivity( - ActivityNames.RemoveFile, - EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), - new RemoveFileFromBucketArguments( - message.Bucket, - message.Key)); + ActivityNames.DeleteTrack, + EndpointHelper.BuildExecuteActivityUri(DeleteTrackActivity.ExecuteEndpointName), + new DeleteTrackArguments(trackId, userId)); return routingSlipBuilder; } diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs index 8ffad05..5392816 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -13,6 +13,7 @@ namespace Musify.Infrastructure.MassTransit internal const string SourceFilePath = "Audio.SourceFilePath"; internal const string WorkingDirectory = "Audio.WorkingDirectory"; internal const string TranscodedDirectory = "Audio.TranscodedDirectory"; + internal const string PreviousAudioFolderName = "Audio.PreviousAudioFolderName"; } internal static class Picture diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index ee5faf1..fc5dbd6 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -180,5 +180,43 @@ namespace Musify.Infrastructure.Services throw; } } + + public async Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken) + { + var prefix = folderKey.EndsWith('/') ? folderKey : $"{folderKey}/"; + var listRequest = new ListObjectsV2Request + { + BucketName = bucket, + Prefix = prefix + }; + + ListObjectsV2Response listResponse; + do + { + listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken); + + if (listResponse.S3Objects.Count == 0) + break; + + var deleteRequest = new DeleteObjectsRequest + { + BucketName = bucket, + Objects = listResponse.S3Objects + .Select(o => new KeyVersion { Key = o.Key } ) + .ToList() + }; + + var deleteResponse = await amazonS3.DeleteObjectsAsync(deleteRequest, cancellationToken); + + if (deleteResponse.DeleteErrors.Count > 0) + { + var errors = string.Join(", ", deleteResponse.DeleteErrors.Select(e => $"{e.Key}: {e.Message}")); + throw new InvalidOperationException($"Failed to delete some objects in folder {folderKey}: {errors}"); + } + + listRequest.ContinuationToken = listResponse.NextContinuationToken; + + } while (listResponse.IsTruncated.HasValue && listResponse.IsTruncated.Value); + } } } \ No newline at end of file From 7f0266d914b861fe4257cbd751624ed050f236ff Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 13 May 2026 21:56:34 +0200 Subject: [PATCH 130/172] Remove processingStatus --- .../Contracts/Infrastructure/IDatabase.cs | 4 - .../Infrastructure/IProcessTrackingStore.cs | 25 ---- Domain/Entities/ProcessExecution.cs | 36 ------ Domain/Entities/ProcessExecutionStatus.cs | 10 -- Domain/Entities/ProcessStepComponentType.cs | 8 -- Domain/Entities/ProcessStepExecution.cs | 40 ------ .../Files/DownloadFileFromBucketActivity.cs | 21 +-- .../Files/TransferFilesToBucketActivity.cs | 21 +-- .../Files/UploadFileToBucketActivity.cs | 23 +--- .../GeneratePictureWorkflowPathsActivity.cs | 22 +--- .../Pictures/ResizePictureActivity.cs | 21 +-- .../Pictures/UpdatePlayListPictureActivity.cs | 21 +-- .../MassTransitDependencyInjection.cs | 3 - .../Filters/ProcessTrackingConsumeFilter.cs | 48 ------- Infrastructure/Persistence/Database.cs | 21 --- .../DatabaseDependencyInjection.cs | 6 - .../Persistence/ProcessTrackingStore.cs | 122 ------------------ 17 files changed, 7 insertions(+), 445 deletions(-) delete mode 100644 Application/Contracts/Infrastructure/IProcessTrackingStore.cs delete mode 100644 Domain/Entities/ProcessExecution.cs delete mode 100644 Domain/Entities/ProcessExecutionStatus.cs delete mode 100644 Domain/Entities/ProcessStepComponentType.cs delete mode 100644 Domain/Entities/ProcessStepExecution.cs delete mode 100644 Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs delete mode 100644 Infrastructure/Persistence/ProcessTrackingStore.cs diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 9f49cf4..98ec9f4 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -17,10 +17,6 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Uploads { get; } - DbSet ProcessExecutions { get; } - - DbSet ProcessStepExecutions { get; } - Task SaveChangesAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IProcessTrackingStore.cs b/Application/Contracts/Infrastructure/IProcessTrackingStore.cs deleted file mode 100644 index 97921ca..0000000 --- a/Application/Contracts/Infrastructure/IProcessTrackingStore.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Musify.Domain.Entities; - -namespace Musify.Application.Contracts.Infrastructure -{ - public interface IProcessTrackingStore - { - Task GetOrCreateProcessAsync( - string processName, - Guid? correlationId, - Guid? conversationId, - Guid? messageId, - CancellationToken cancellationToken); - - Task StartStepAsync( - Guid processExecutionId, - string stepName, - ProcessStepComponentType componentType, - int attempt, - CancellationToken cancellationToken); - - Task CompleteStepAsync(Guid processExecutionId, Guid stepExecutionId, CancellationToken cancellationToken); - - Task FailStepAsync(Guid processExecutionId, Guid stepExecutionId, string errorMessage, CancellationToken cancellationToken); - } -} diff --git a/Domain/Entities/ProcessExecution.cs b/Domain/Entities/ProcessExecution.cs deleted file mode 100644 index cc6df6f..0000000 --- a/Domain/Entities/ProcessExecution.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Musify.Domain.Entities -{ -#pragma warning disable CS8618 - [Table("ProcessExecution")] - public class ProcessExecution - { - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - public Guid? CorrelationId { get; set; } - - public Guid? ConversationId { get; set; } - - public Guid? MessageId { get; set; } - - [Required] - [MaxLength(256)] - public required string ProcessName { get; set; } - - [Required] - public ProcessExecutionStatus Status { get; set; } = ProcessExecutionStatus.Running; - - [Required] - public DateTime StartedDateTime { get; set; } = DateTime.UtcNow; - - public DateTime? FinishedDateTime { get; set; } - - [MaxLength(2048)] - public string? ErrorMessage { get; set; } - - public ICollection Steps { get; set; } = new List(); - } -} diff --git a/Domain/Entities/ProcessExecutionStatus.cs b/Domain/Entities/ProcessExecutionStatus.cs deleted file mode 100644 index b609694..0000000 --- a/Domain/Entities/ProcessExecutionStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Musify.Domain.Entities -{ - public enum ProcessExecutionStatus - { - Running, - Succeeded, - Failed, - Compensated - } -} diff --git a/Domain/Entities/ProcessStepComponentType.cs b/Domain/Entities/ProcessStepComponentType.cs deleted file mode 100644 index 43c4848..0000000 --- a/Domain/Entities/ProcessStepComponentType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Musify.Domain.Entities -{ - public enum ProcessStepComponentType - { - Consumer, - Activity - } -} diff --git a/Domain/Entities/ProcessStepExecution.cs b/Domain/Entities/ProcessStepExecution.cs deleted file mode 100644 index 0847ecf..0000000 --- a/Domain/Entities/ProcessStepExecution.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Musify.Domain.Entities -{ -#pragma warning disable CS8618 - [Table("ProcessStepExecution")] - public class ProcessStepExecution - { - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - [Required] - public Guid ProcessExecutionId { get; set; } - - [Required] - [MaxLength(256)] - public required string StepName { get; set; } - - [Required] - public ProcessStepComponentType ComponentType { get; set; } - - [Required] - public ProcessExecutionStatus Status { get; set; } = ProcessExecutionStatus.Running; - - [Required] - public int Attempt { get; set; } - - [Required] - public DateTime StartedDateTime { get; set; } = DateTime.UtcNow; - - public DateTime? FinishedDateTime { get; set; } - - [MaxLength(2048)] - public string? ErrorMessage { get; set; } - - [ForeignKey(nameof(ProcessExecutionId)) ] - public ProcessExecution ProcessExecution { get; set; } - } -} diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 9bf4475..f7d7db8 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -1,7 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; @@ -9,28 +8,13 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files { internal class DownloadFileFromBucketActivity( IStorageService storageHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "download-file-from-bucket"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(DownloadFileFromBucketActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariable); ArgumentNullException.ThrowIfNull(destinationPath, nameof(destinationPath)); @@ -50,8 +34,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files using var destinationStream = File.Create(destinationPath); await fileStream.CopyToAsync(destinationStream, executeContext.CancellationToken); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(new DownloadFileFromBucketLog( executeContext.Arguments.Bucket, executeContext.Arguments.Key, @@ -60,7 +42,6 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files catch (Exception exception) { logger.LogError(exception, "Failed to download file from bucket"); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index f0bc572..5f6ceb8 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -1,7 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; @@ -9,28 +8,13 @@ namespace Musify.Infrastructure.MassTransit.Activities { public class TransferFilesToBucketActivity( IStorageService storageHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "transfer-files-to-bucket"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(TransferFilesToBucketActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var folderPath = executeContext.GetVariable(executeContext.Arguments.SourceDirectoryVariable); ArgumentNullException.ThrowIfNull(folderPath, nameof(folderPath)); var destinationKey = executeContext.Arguments.DestinationKey; @@ -47,8 +31,6 @@ namespace Musify.Infrastructure.MassTransit.Activities executeContext.Arguments.DestinationBucket, destinationKey, executeContext.CancellationToken); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) @@ -56,7 +38,6 @@ namespace Musify.Infrastructure.MassTransit.Activities logger.LogError(exception, "Failed to transfer files from {FolderPath} to {DestinationBucket}", folderPath, executeContext.Arguments.DestinationBucket); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 99849c1..415d68c 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -2,35 +2,19 @@ using MassTransit; using Microsoft.Extensions.Logging; using MimeMapping; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities.Files { public class UploadFileToBucketActivity(IStorageService storageHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "upload-file-to-bucket"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(UploadFileToBucketActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var fileName = Path.GetFileName(sourceFilePath); @@ -42,16 +26,13 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files executeContext.Arguments.DestinationBucket, Path.Combine(executeContext.Arguments.DestinationRoute, fileName), executeContext.CancellationToken); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new UploadFileToBucketLog( executeContext.Arguments.DestinationBucket, executeContext.Arguments.DestinationRoute)); } catch (Exception exception) { - logger.LogError(exception, "Failed to upload file to bucket for process {ProcessId} step {StepId}", processId, stepId); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); + logger.LogError(exception, "Failed to upload file to bucket"); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 70d1eb8..5d2570a 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -1,14 +1,11 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; namespace Musify.Infrastructure.MassTransit.Activities { public class GeneratePictureWorkflowPathsActivity( - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IExecuteActivity { public const string ExecuteEndpointName = "generate-picture-workflow-paths"; @@ -17,20 +14,6 @@ namespace Musify.Infrastructure.MassTransit.Activities public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(GeneratePictureWorkflowPathsActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - try { var destinationFolderName = Guid.NewGuid().ToString(); @@ -49,8 +32,6 @@ namespace Musify.Infrastructure.MassTransit.Activities logger.LogDebug("Generated picture workflow paths in {WorkingDirectory}", workingDirectory); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new Dictionary { [RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory, @@ -65,7 +46,6 @@ namespace Musify.Infrastructure.MassTransit.Activities { logger.LogError(exception, "Failed to generate picture workflow paths for {SourceKey}", executeContext.Arguments.SourceKey); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index c264d41..e066806 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -1,7 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; @@ -10,28 +9,13 @@ namespace Musify.Infrastructure.MassTransit.Activities { public class ResizePictureActivity( IPictureService pictureHandler, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "resize-picture"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(ResizePictureActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); var destinationFilePath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariable); @@ -63,14 +47,11 @@ namespace Musify.Infrastructure.MassTransit.Activities logger.LogDebug("Resized picture from {SourceFilePath} to {DestinationFilePath}", sourceFilePath, destinationFilePath); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(new ResizePictureLog(destinationFilePath)); } catch (Exception exception) { logger.LogError(exception, "Failed to resize picture {SourceFilePath}", sourceFilePath); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 37e729c..c6df282 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -1,7 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; @@ -9,28 +8,13 @@ namespace Musify.Infrastructure.MassTransit.Activities { public class UpdatePlayListPictureActivity( IDatabase database, - ILogger logger, - IProcessTrackingStore processTrackingStore) + ILogger logger) : IActivity { public const string ExecuteEndpointName = "update-playlist-picture"; public async Task Execute(ExecuteContext executeContext) { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - "RoutingSlip", - executeContext.CorrelationId ?? executeContext.TrackingNumber, - executeContext.ConversationId, - executeContext.MessageId, - executeContext.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - nameof(UpdatePlayListPictureActivity), - ProcessStepComponentType.Activity, - 0, - executeContext.CancellationToken); - var smallResizedVariable = executeContext.GetVariable(executeContext.Arguments.SmallPictureVariable); ArgumentNullException.ThrowIfNull(smallResizedVariable, nameof(smallResizedVariable)); var mediumResizedVariable = executeContext.GetVariable(executeContext.Arguments.MediumPictureVariable); @@ -68,15 +52,12 @@ namespace Musify.Infrastructure.MassTransit.Activities logger.LogInformation("Updated playlist {PlayListId} pictures", executeContext.Arguments.PlayListId); - - await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken); return executeContext.Completed(); } catch (Exception exception) { logger.LogError(exception, "Failed to update playlist {PlayListId} pictures", executeContext.Arguments.PlayListId); - await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken); throw; } } diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 9c0f695..499e8b0 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -11,7 +11,6 @@ using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Logs; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.MassTransit.Filters; using Musify.Infrastructure.MassTransit.Logs; using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; using Musify.Infrastructure.MassTransit.Activities.Tracks; @@ -92,8 +91,6 @@ namespace Musify.Infrastructure.MassTransit busFactoryConfigurator, busRegistrationContext.GetRequiredService()); - busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext); - busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => endpointConfigurator.ConfigureConsumer(busRegistrationContext)); diff --git a/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs b/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs deleted file mode 100644 index 5ac4a07..0000000 --- a/Infrastructure/MassTransit/Filters/ProcessTrackingConsumeFilter.cs +++ /dev/null @@ -1,48 +0,0 @@ -using MassTransit; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; - -namespace Musify.Infrastructure.MassTransit.Filters -{ - public class ProcessTrackingConsumeFilter(IProcessTrackingStore processTrackingStore) - : IFilter> - where TMessage : class - { - public async Task Send(ConsumeContext context, IPipe> next) - { - var processId = await processTrackingStore.GetOrCreateProcessAsync( - typeof(TMessage).Name, - context.CorrelationId ?? context.MessageId, - context.ConversationId, - context.MessageId, - context.CancellationToken); - - var stepId = await processTrackingStore.StartStepAsync( - processId, - $"{typeof(TMessage).Name}.Consume", - ProcessStepComponentType.Consumer, - 0, - context.CancellationToken); - - try - { - await next.Send(context); - await processTrackingStore.CompleteStepAsync(processId, stepId, context.CancellationToken); - } - catch (Exception exception) - { - await processTrackingStore.FailStepAsync( - processId, - stepId, - exception.Message, - context.CancellationToken); - throw; - } - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope(nameof(ProcessTrackingConsumeFilter<>)); - } - } -} diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 993fe77..8519719 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -24,26 +24,5 @@ namespace Musify.Infrastructure.Persistence public DbSet PlayListHasTracks => Set(); public DbSet Uploads => Set(); - - public DbSet ProcessExecutions => Set(); - - public DbSet ProcessStepExecutions => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .HasIndex(process => new { process.CorrelationId, process.ProcessName }); - - modelBuilder.Entity() - .HasIndex(step => new { step.ProcessExecutionId, step.StepName }); - - modelBuilder.Entity() - .HasMany(process => process.Steps) - .WithOne(step => step.ProcessExecution) - .HasForeignKey(step => step.ProcessExecutionId) - .OnDelete(DeleteBehavior.Cascade); - - base.OnModelCreating(modelBuilder); - } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index ac1af6e..0a3f01c 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -23,11 +23,5 @@ namespace Musify.Infrastructure.Persistence return serviceDescriptors; } - - public static IServiceCollection AddProcessTrackingStore(this IServiceCollection serviceDescriptors) - { - serviceDescriptors.AddScoped(); - return serviceDescriptors; - } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/ProcessTrackingStore.cs b/Infrastructure/Persistence/ProcessTrackingStore.cs deleted file mode 100644 index 3636609..0000000 --- a/Infrastructure/Persistence/ProcessTrackingStore.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; - -namespace Musify.Infrastructure.Persistence -{ - public class ProcessTrackingStore(IDatabase database) - : IProcessTrackingStore - { - public async Task GetOrCreateProcessAsync( - string processName, - Guid? correlationId, - Guid? conversationId, - Guid? messageId, - CancellationToken cancellationToken) - { - ProcessExecution? existingProcess = null; - - if (correlationId.HasValue) - { - existingProcess = await database.ProcessExecutions - .OrderByDescending(process => process.StartedDateTime) - .FirstOrDefaultAsync( - process => process.CorrelationId == correlationId && process.ProcessName == processName, - cancellationToken); - } - - if (existingProcess is not null) - { - return existingProcess.Id; - } - - var newProcess = new ProcessExecution - { - ProcessName = processName, - CorrelationId = correlationId, - ConversationId = conversationId, - MessageId = messageId, - Status = ProcessExecutionStatus.Running, - StartedDateTime = DateTime.UtcNow, - }; - - await database.ProcessExecutions.AddAsync(newProcess, cancellationToken); - await database.SaveChangesAsync(cancellationToken); - return newProcess.Id; - } - - public async Task StartStepAsync( - Guid processExecutionId, - string stepName, - ProcessStepComponentType componentType, - int attempt, - CancellationToken cancellationToken) - { - var step = new ProcessStepExecution - { - ProcessExecutionId = processExecutionId, - StepName = stepName, - ComponentType = componentType, - Attempt = attempt, - Status = ProcessExecutionStatus.Running, - StartedDateTime = DateTime.UtcNow, - }; - - await database.ProcessStepExecutions.AddAsync(step, cancellationToken); - await database.SaveChangesAsync(cancellationToken); - return step.Id; - } - - public async Task CompleteStepAsync(Guid processExecutionId, Guid stepExecutionId, CancellationToken cancellationToken) - { - var step = await database.ProcessStepExecutions - .SingleAsync(currentStep => currentStep.Id == stepExecutionId, cancellationToken); - - step.Status = ProcessExecutionStatus.Succeeded; - step.FinishedDateTime = DateTime.UtcNow; - - var process = await database.ProcessExecutions - .SingleAsync(currentProcess => currentProcess.Id == processExecutionId, cancellationToken); - - var hasRunningSteps = await database.ProcessStepExecutions - .AnyAsync( - currentStep => currentStep.ProcessExecutionId == processExecutionId - && currentStep.Status == ProcessExecutionStatus.Running, - cancellationToken); - - var hasFailedSteps = await database.ProcessStepExecutions - .AnyAsync( - currentStep => currentStep.ProcessExecutionId == processExecutionId - && currentStep.Status == ProcessExecutionStatus.Failed, - cancellationToken); - - if (!hasRunningSteps && !hasFailedSteps) - { - process.Status = ProcessExecutionStatus.Succeeded; - process.FinishedDateTime = DateTime.UtcNow; - process.ErrorMessage = string.Empty; - } - - await database.SaveChangesAsync(cancellationToken); - } - - public async Task FailStepAsync(Guid processExecutionId, Guid stepExecutionId, string errorMessage, CancellationToken cancellationToken) - { - var step = await database.ProcessStepExecutions - .SingleAsync(currentStep => currentStep.Id == stepExecutionId, cancellationToken); - - step.Status = ProcessExecutionStatus.Failed; - step.FinishedDateTime = DateTime.UtcNow; - step.ErrorMessage = errorMessage; - - var process = await database.ProcessExecutions - .SingleAsync(currentProcess => currentProcess.Id == processExecutionId, cancellationToken); - - process.Status = ProcessExecutionStatus.Failed; - process.FinishedDateTime = DateTime.UtcNow; - process.ErrorMessage = errorMessage; - - await database.SaveChangesAsync(cancellationToken); - } - } -} From 8ad203c974e9168ffcd46789135e32f95a8a8135 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 01:00:44 +0200 Subject: [PATCH 131/172] Safe code and changes --- .../Handlers/CreatePlayListCommandHandler.cs | 21 +++ .../Handlers/DeletePlayListCommandHandler.cs | 76 ++++++-- .../GetPlayListsByUserIdQueryHandler.cs | 6 +- .../Handlers/UpdatePlayListCommandHandler.cs | 120 +++++++----- .../Queries/GetPlayListsByUserIdQuery.cs | 2 +- .../Handler/CreateTrackCommandHandler.cs | 66 ++++++- .../Handler/DeleteTrackCommandHandler.cs | 46 ++++- .../Handler/GetTracksByUserIdQueryHandler.cs | 6 +- .../Tracks/Handler/GetTracksQueryHandler.cs | 11 +- .../Tracks/Queries/GetTracksByUserIdQuery.cs | 2 +- .../Handlers/CreateUserCommandHandler.cs | 13 +- .../Users/Handlers/GetUsersQueryHandler.cs | 5 +- Application/Users/Queries/GetUsersQuery.cs | 2 +- .../Files/UploadFileToBucketActivity.cs | 1 - .../Pictures/ResizePictureActivity.cs | 1 - .../Pictures/UpdateTrackPictureActivity.cs | 1 - .../Consumers/RemoveFileConsumer.cs | 40 ++++ ...assTransitDependencyInjection.Endpoints.cs | 50 +++++ .../MassTransitDependencyInjection.Options.cs | 24 +++ ...TransitDependencyInjection.Registration.cs | 144 ++++++++++++++ .../MassTransitDependencyInjection.cs | 178 ++---------------- .../MassTransitEndpointNames.cs | 9 + .../MassTransit/Logs/UpdateTrackPictureLog.cs | 6 +- .../OpenTelemetryDependencyInjection.cs | 1 - .../20260328141805_FirstMigration.cs | 3 +- 25 files changed, 568 insertions(+), 266 deletions(-) create mode 100644 Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs create mode 100644 Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs create mode 100644 Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Options.cs create mode 100644 Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs create mode 100644 Infrastructure/MassTransit/DependencyInjection/MassTransitEndpointNames.cs diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 0bdb882..543cb4d 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -62,13 +62,31 @@ namespace Musify.Application.PlayLists.Handlers { var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken); if (!publishResult.IsSuccess) + { + await TryRollbackOnPublishFailureAsync(playList, cancellationToken); return publishResult; + } } logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId); return Result.Created(PlayListResponse.FromEntity(playList)); } + async Task TryRollbackOnPublishFailureAsync(PlayList playList, CancellationToken cancellationToken) + { + try + { + database.PlayLists.Remove(playList); + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback playlist creation after publish failure for playlist {PlayListId}", playList.Id); + } + + await RollbackAllPicturesAsync(playList, cancellationToken); + } + async Task UploadPictureAsync(PlayList playList, IFileData picture, CancellationToken cancellationToken) { var pictureName = Guid.NewGuid() + picture.FileType; @@ -123,6 +141,9 @@ namespace Musify.Application.PlayLists.Handlers if (string.IsNullOrEmpty(playList.OriginalPictureName)) return; + if (playList.OriginalPictureName == playListConfiguration.Routes.PresetOriginalPicture) + return; + var originalPicturePath = playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName); try diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index db0c188..900d44d 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -28,25 +28,73 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } - if (!string.IsNullOrWhiteSpace(playList.OriginalPictureName)) - await PublishRemoveFileEventAsync(playList.Id, - playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName), cancellationToken); - if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) - await PublishRemoveFileEventAsync(playList.Id, - playListConfiguration.Routes.BuildSmallPicturePath(playList.SmallPictureName), cancellationToken); - if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) - await PublishRemoveFileEventAsync(playList.Id, - playListConfiguration.Routes.BuildMediumPicturePath(playList.MediumPictureName), cancellationToken); - if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) - await PublishRemoveFileEventAsync(playList.Id, - playListConfiguration.Routes.BuildLargePicturePath(playList.LargePictureName), cancellationToken); + var originalPictureName = playList.OriginalPictureName; + var smallPictureName = playList.SmallPictureName; + var mediumPictureName = playList.MediumPictureName; + var largePictureName = playList.LargePictureName; - database.PlayLists.Remove(playList); - await database.SaveChangesAsync(cancellationToken); + var originalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName); + var smallPictureKey = playListConfiguration.Routes.BuildSmallPicturePath(playList.SmallPictureName); + var mediumPictureKey = playListConfiguration.Routes.BuildMediumPicturePath(playList.MediumPictureName); + var largePictureKey = playListConfiguration.Routes.BuildLargePicturePath(playList.LargePictureName); + + try + { + database.PlayLists.Remove(playList); + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to delete playlist {PlayListId}", request.PlayListId); + return Result.Error($"Failed to delete playlist {request.PlayListId}"); + } + + await PublishRemoveFileEventsBestEffortAsync( + playList.Id, + originalPictureName, + smallPictureName, + mediumPictureName, + largePictureName, + originalPictureKey, + smallPictureKey, + mediumPictureKey, + largePictureKey, + playListConfiguration.Routes, + cancellationToken); return Result.NoContent(); } + async Task PublishRemoveFileEventsBestEffortAsync( + Guid playListId, + string originalPictureName, + string smallPictureName, + string mediumPictureName, + string largePictureName, + string originalPictureKey, + string smallPictureKey, + string mediumPictureKey, + string largePictureKey, + PlayListRoutes playListRoutes, + CancellationToken cancellationToken) + { + if (originalPictureName != playListRoutes.PresetOriginalPicture) + await PublishRemoveFileEventIgnoringErrorsAsync(playListId, originalPictureKey, cancellationToken); + if (smallPictureName != playListRoutes.PresetSmallPicture) + await PublishRemoveFileEventIgnoringErrorsAsync(playListId, smallPictureKey, cancellationToken); + if (mediumPictureName != playListRoutes.PresetMediumPicture) + await PublishRemoveFileEventIgnoringErrorsAsync(playListId, mediumPictureKey, cancellationToken); + if (largePictureName != playListRoutes.PresetLargePicture) + await PublishRemoveFileEventIgnoringErrorsAsync(playListId, largePictureKey, cancellationToken); + } + + async Task PublishRemoveFileEventIgnoringErrorsAsync(Guid playListId, string key, CancellationToken cancellationToken) + { + var result = await PublishRemoveFileEventAsync(playListId, key, cancellationToken); + if (!result.IsSuccess) + logger.LogWarning("Remove file event publish failed for playlist {PlayListId} with key {Key}", playListId, key); + } + async Task PublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) { try diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index 3803418..6c7cedf 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -16,14 +16,16 @@ namespace Musify.Application.PlayLists.Handlers { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var normalizedName = request.Name?.Trim().ToUpperInvariant() ?? string.Empty; var playListsQuery = database.PlayLists .AsNoTracking() .Where(p => p.UserId == request.UserId); - if (!string.IsNullOrWhiteSpace(normalizedName)) + if (request.Name is not null) + { + var normalizedName = request.Name.Trim().ToUpperInvariant(); playListsQuery = playListsQuery.Where(p => p.NormalizedName.Contains(normalizedName)); + } var totalCount = await playListsQuery.CountAsync(cancellationToken); diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 2e643ec..2235d02 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -17,6 +17,9 @@ namespace Musify.Application.PlayLists.Handlers PlayListConfiguration playListConfiguration) : ICommandHandler> { + record UpdatePictureOutcome(Result Result, bool PictureUpdated, string? UploadedPictureKey); + record PreviousPlayListState(string Name, string NormalizedName, string Description, string OriginalPictureName); + public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); @@ -32,11 +35,14 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } + var previousState = CapturePreviousState(playListEntity); + UpdateName(playListEntity, request.NewName); UpdateDescription(playListEntity, request.NewDescription); - var updatePictureResult = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); - if (!updatePictureResult.IsSuccess) - return updatePictureResult; + + var pictureOutcome = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); + if (!pictureOutcome.Result.IsSuccess) + return pictureOutcome.Result; try { @@ -45,20 +51,24 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - await Rollback(playListEntity.OriginalPictureName, cancellationToken); + if (pictureOutcome.PictureUpdated && pictureOutcome.UploadedPictureKey is not null) + await Rollback(pictureOutcome.UploadedPictureKey, cancellationToken); logger.LogError(exception, "Failed to update playlist {PlayListId}", playListEntity.Id); return Result.Error($"Failed to update playlist {playListEntity.Id}"); } - try + if (pictureOutcome.PictureUpdated) { - await PublishPlayListPictureEvent(playListEntity, cancellationToken); - await RemoveOldPictures(playListEntity, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish playlist picture update event or remove old pictures for playlist {PlayListId}", playListEntity.Id); - return Result.Error($"Failed to update playlist {playListEntity.Id}"); + var publishResult = await PublishPlayListPictureEvent(playListEntity, cancellationToken); + if (!publishResult.IsSuccess) + { + await TryRollbackAfterPublishFailureAsync( + playListEntity, + previousState, + pictureOutcome.UploadedPictureKey, + cancellationToken); + return Result.Error($"Failed to update playlist {playListEntity.Id}"); + } } logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id); @@ -67,26 +77,34 @@ namespace Musify.Application.PlayLists.Handlers void UpdateName(PlayList playList, string? newName) { - if (!string.IsNullOrWhiteSpace(newName)) - { - playList.Name = newName; - playList.NormalizedName = newName.Trim().ToUpperInvariant(); - } + if (newName is null) + return; + + playList.Name = newName; + playList.NormalizedName = newName.Trim().ToUpperInvariant(); } void UpdateDescription(PlayList playList, string? newDescription) { - if (!string.IsNullOrWhiteSpace(newDescription)) - playList.Description = newDescription; + if (newDescription is null) + return; + + playList.Description = newDescription; } - async Task UpdatePicture(PlayList playList, IFileData? newPicture, CancellationToken cancellationToken) + static PreviousPlayListState CapturePreviousState(PlayList playList) + => new(playList.Name, playList.NormalizedName, playList.Description, playList.OriginalPictureName); + + async Task UpdatePicture( + PlayList playList, + IFileData? newPicture, + CancellationToken cancellationToken) { if (newPicture is null) - return Result.Success(); + return new UpdatePictureOutcome(Result.Success(), false, null); var pictureName = Guid.NewGuid() + newPicture.FileType; - var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); + var pictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(pictureName); try { await storageHandler.UploadFileAsync( @@ -98,11 +116,11 @@ namespace Musify.Application.PlayLists.Handlers } catch { - return Result.Error("Failed to upload picture to storage"); + return new UpdatePictureOutcome(Result.Error("Failed to upload picture to storage"), false, null); } playList.OriginalPictureName = pictureName; - return Result.Success(); + return new UpdatePictureOutcome(Result.Success(), true, pictureKey); } async Task PublishPlayListPictureEvent(PlayList playList, CancellationToken cancellationToken) @@ -112,7 +130,7 @@ namespace Musify.Application.PlayLists.Handlers await eventBus.PublishAsync(new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.Bucket, - Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), + playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, @@ -135,25 +153,6 @@ namespace Musify.Application.PlayLists.Handlers } } - async Task RemoveOldPictures(PlayList playList, CancellationToken cancellationToken) - { - if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) - await PublishRemoveFileEvent(Path.Combine( - playListConfiguration.Routes.SmallPicturesPath, - playList.SmallPictureName), - cancellationToken); - if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) - await PublishRemoveFileEvent(Path.Combine( - playListConfiguration.Routes.MediumPicturesPath, - playList.MediumPictureName), - cancellationToken); - if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) - await PublishRemoveFileEvent(Path.Combine( - playListConfiguration.Routes.LargePicturesPath, - playList.LargePictureName), - cancellationToken); - } - async Task Rollback(string pictureKey, CancellationToken cancellationToken) { await storageHandler.RemoveFileAsync( @@ -163,20 +162,37 @@ namespace Musify.Application.PlayLists.Handlers logger.LogInformation("Rolled back uploaded picture {PictureKey} from storage", pictureKey); } - async Task PublishRemoveFileEvent(string key, CancellationToken cancellationToken) + async Task TryRollbackAfterPublishFailureAsync( + PlayList playList, + PreviousPlayListState previousState, + string? uploadedPictureKey, + CancellationToken cancellationToken) { + playList.Name = previousState.Name; + playList.NormalizedName = previousState.NormalizedName; + playList.Description = previousState.Description; + playList.OriginalPictureName = previousState.OriginalPictureName; + try { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.Bucket, - key), cancellationToken); - return Result.NoContent(); + await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish remove file event for key {Key}", key); - return Result.Error($"Failed to publish remove file event for {key}"); + logger.LogError(exception, "Failed to rollback playlist state after publish failure for playlist {PlayListId}", playList.Id); + } + + if (uploadedPictureKey is null) + return; + + try + { + await Rollback(uploadedPictureKey, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback uploaded picture after publish failure for playlist {PlayListId}", playList.Id); } } } -} \ No newline at end of file +} diff --git a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index c745a09..8f5439e 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -7,7 +7,7 @@ namespace Musify.Application.PlayLists.Queries { public record GetPlayListsByUserIdQuery( Guid UserId, - string Name = "", + string? Name = null, int PageNumber = 1, int PageSize = 10) : IQuery> >; diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 1144f76..e07eaa5 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -34,13 +34,35 @@ namespace Musify.Application.Tracks.Handler var fullPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(trackEntity.OriginalPictureName); var fullAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(trackEntity.OriginalAudioName); - await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken); + var uploadResult = await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken); + if (!uploadResult.IsSuccess) + return uploadResult; - await PublishUpdateEvent(trackEntity, cancellationToken); - await PublishTranscodeEvent(trackEntity, cancellationToken); + try + { + await database.Tracks.AddAsync(trackEntity, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to save track {Title} after uploading files", request.Title); + await RollbackFilesAsync(fullPictureKey, fullAudioKey, cancellationToken); + return Result.Error($"Failed to create track for {request.Title}"); + } - await database.Tracks.AddAsync(trackEntity, cancellationToken); - await database.SaveChangesAsync(cancellationToken); + var publishPictureResult = await PublishUpdateEvent(trackEntity, cancellationToken); + if (!publishPictureResult.IsSuccess) + { + await RollbackTrackAfterPublishFailureAsync(trackEntity, fullPictureKey, fullAudioKey, cancellationToken); + return Result.Error($"Failed to create track for {request.Title}"); + } + + var publishTranscodeResult = await PublishTranscodeEvent(trackEntity, cancellationToken); + if (!publishTranscodeResult.IsSuccess) + { + await RollbackTrackAfterPublishFailureAsync(trackEntity, fullPictureKey, fullAudioKey, cancellationToken); + return Result.Error($"Failed to create track for {request.Title}"); + } return Result.Created(TrackResponse.FromEntity(trackEntity)); } @@ -91,7 +113,7 @@ namespace Musify.Application.Tracks.Handler logger.LogInformation("Rolled back uploaded files {PictureKey}, {AudioKey}", pictureKey, audioKey); } - async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken) + async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken) { try { @@ -111,14 +133,16 @@ namespace Musify.Application.Tracks.Handler trackConfiguration.Routes.LargePicturesPath, trackConfiguration.PicturesSizes.LargePictureWidth, trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); + return Result.Success(); } catch (Exception exception) { logger.LogError(exception, "Failed to publish track picture update event for track {TrackId}", track.Id); + return Result.Error($"Failed to publish track picture update event for track {track.Id}"); } } - async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken) + async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken) { var destinationFolderAudio = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString()); @@ -130,10 +154,38 @@ namespace Musify.Application.Tracks.Handler trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), storageConfiguration.Bucket, destinationFolderAudio), cancellationToken); + return Result.Success(); } catch (Exception exception) { logger.LogError(exception, "Failed to publish track audio update event for track {TrackId}", track.Id); + return Result.Error($"Failed to publish track audio update event for track {track.Id}"); + } + } + + async Task RollbackTrackAfterPublishFailureAsync( + Track track, + string pictureKey, + string audioKey, + CancellationToken cancellationToken) + { + try + { + database.Tracks.Remove(track); + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback track {TrackId} persistence after publish failure", track.Id); + } + + try + { + await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback uploaded files after publish failure for track {TrackId}", track.Id); } } } diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index fff04b9..8b41fd8 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -24,7 +24,10 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound(); } - if (track.Id != request.UserId) + var isOwner = await database.UserHasTracks + .AsNoTracking() + .AnyAsync(ut => ut.TrackId == request.TrackId && ut.UserId == request.UserId, cancellationToken); + if (!isOwner) { logger.LogWarning("User {UserId} unauthorized to delete track {TrackId}", request.UserId, request.TrackId); return Result.Unauthorized(); @@ -37,13 +40,46 @@ namespace Musify.Application.Tracks.Handler return Result.Conflict("Track is currently being processed and cannot be deleted"); } + var previousPicturesStatus = track.PicturesProcessingStatus; + var previousAudioStatus = track.AudioTranscodeProcessingStatus; + track.PicturesProcessingStatus = ProcessingStatus.Processing; track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing; - await database.SaveChangesAsync(cancellationToken); - await eventBus.PublishAsync( - new DeleteTrackEvent(track.Id, request.UserId), - cancellationToken); + try + { + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to mark track {TrackId} as processing before deletion", request.TrackId); + return Result.Error($"Failed to delete track {request.TrackId}"); + } + + try + { + await eventBus.PublishAsync( + new DeleteTrackEvent(track.Id, request.UserId), + cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish delete track event for track {TrackId}", request.TrackId); + + track.PicturesProcessingStatus = previousPicturesStatus; + track.AudioTranscodeProcessingStatus = previousAudioStatus; + + try + { + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception rollbackException) + { + logger.LogError(rollbackException, "Failed to rollback track {TrackId} processing flags after publish failure", request.TrackId); + } + + return Result.Error($"Failed to delete track {request.TrackId}"); + } return Result.NoContent(); } diff --git a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs index 3b107c6..c48426f 100644 --- a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs @@ -16,15 +16,17 @@ namespace Musify.Application.Tracks.Handler { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var normalizedName = request.Name?.Trim().ToUpperInvariant() ?? string.Empty; var tracksQuery = database.UserHasTracks .AsNoTracking() .Include(ut => ut.Track) .Where(p => p.UserId == request.UserId); - if (!string.IsNullOrWhiteSpace(normalizedName)) + if (request.Name is not null) + { + var normalizedName = request.Name.Trim().ToUpperInvariant(); tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName)); + } var totalCount = await tracksQuery.CountAsync(cancellationToken); diff --git a/Application/Tracks/Handler/GetTracksQueryHandler.cs b/Application/Tracks/Handler/GetTracksQueryHandler.cs index 8fc47f9..1f3528c 100644 --- a/Application/Tracks/Handler/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksQueryHandler.cs @@ -17,15 +17,16 @@ namespace Musify.Application.Tracks.Handler var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var totalCount = await database.PlayLists.CountAsync(cancellationToken); - var pagedPlayLists = await database.UserHasTracks + var tracksQuery = database.UserHasTracks .AsNoTracking() .OrderBy(t => t.Id) .Include(ut => ut.Track) - .Select(t => TrackResponse.FromEntity(t.Track)) - .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + .Select(t => TrackResponse.FromEntity(t.Track)); - return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); + var totalCount = await tracksQuery.CountAsync(cancellationToken); + var pagedTracks = await tracksQuery.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + + return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); } } } \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs index 240cbbf..f4eff24 100644 --- a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs +++ b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs @@ -7,7 +7,7 @@ namespace Musify.Application.Tracks.Queries { public record GetTracksByUserIdQuery( Guid UserId, - string Name = "", + string? Name = null, int PageNumber = 1, int PageSize = 10) : IQuery > >; diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index 9d9e097..2c6b11f 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -21,8 +21,17 @@ namespace Musify.Application.Users.Handlers } var newUser = CreateUserCommand.ToEntity(request); - await database.Users.AddAsync(newUser, cancellationToken); - await database.SaveChangesAsync(cancellationToken); + + try + { + await database.Users.AddAsync(newUser, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to create user {UserId}", request.Id); + return Result.Error($"Failed to create user {request.Id}"); + } logger.LogInformation("Created user {UserId}", newUser.Id); return Result.Created(UserResponse.FromEntity(newUser)); diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index 3f807d8..56c31cb 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -6,7 +6,6 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; using X.PagedList.EF; -using X.PagedList.Extensions; namespace Musify.Application.Users.Handlers { @@ -17,12 +16,12 @@ namespace Musify.Application.Users.Handlers { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var first = (pageNumber - 1) * pageSize; var user = database.Users .AsNoTracking() .AsQueryable(); - if (!string.IsNullOrEmpty(request.UsernameSearch)) + + if (request.UsernameSearch is not null) { var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant(); user = user.Where(u => u.NormalizedName.Contains(normalizedUsername)); diff --git a/Application/Users/Queries/GetUsersQuery.cs b/Application/Users/Queries/GetUsersQuery.cs index 62ba046..6f403aa 100644 --- a/Application/Users/Queries/GetUsersQuery.cs +++ b/Application/Users/Queries/GetUsersQuery.cs @@ -5,6 +5,6 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { - public record GetUsersQuery(int PageNumber, int PageSize, string UsernameSearch) + public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch = null) : IQuery >>; } diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 415d68c..0b675b2 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -1,6 +1,5 @@ using MassTransit; using Microsoft.Extensions.Logging; -using MimeMapping; using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index e066806..941132b 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; -using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 44392a1..3c807b8 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,7 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; -using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs new file mode 100644 index 0000000..38345ad --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs @@ -0,0 +1,40 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Events; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class RemoveFileConsumer( + IStorageService storageService, + ILogger logger) + : IConsumer + { + public const string QueueName = "remove-file"; + + public async Task Consume(ConsumeContext consumeContext) + { + try + { + await storageService.RemoveFileAsync( + consumeContext.Message.Bucket, + consumeContext.Message.Key, + consumeContext.CancellationToken); + + logger.LogInformation( + "Removed file {Key} from bucket {Bucket}", + consumeContext.Message.Key, + consumeContext.Message.Bucket); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to remove file {Key} from bucket {Bucket}", + consumeContext.Message.Key, + consumeContext.Message.Bucket); + throw; + } + } + } +} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs new file mode 100644 index 0000000..023ea42 --- /dev/null +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs @@ -0,0 +1,50 @@ +using MassTransit; +using Musify.Infrastructure.MassTransit.Consumers; + +namespace Musify.Infrastructure.MassTransit +{ + public static partial class MassTransitDependencyInjection + { + static void ConfigureConsumerEndpoint( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string queueName) + where TConsumer : class, IConsumer + { + busFactoryConfigurator.ReceiveEndpoint(queueName, endpointConfigurator => + endpointConfigurator.ConfigureConsumer(busRegistrationContext)); + } + + static void ConfigureExecuteActivityEndpoint( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string endpointName) + where TActivity : class, IExecuteActivity + where TArguments : class + { + busFactoryConfigurator.ReceiveEndpoint(MassTransitEndpointNames.ExecuteQueue(endpointName), endpointConfigurator => + { + endpointConfigurator.ExecuteActivityHost(busRegistrationContext); + }); + } + + static void ConfigureActivityEndpoint( + IRabbitMqBusFactoryConfigurator busFactoryConfigurator, + IBusRegistrationContext busRegistrationContext, + string endpointName) + where TActivity : class, IActivity + where TArguments : class + where TLog : class + { + busFactoryConfigurator.ReceiveEndpoint(MassTransitEndpointNames.ExecuteQueue(endpointName), endpointConfigurator => + endpointConfigurator.ExecuteActivityHost( + EndpointHelper.BuildCompensateActivityUri(endpointName), + busRegistrationContext) + ); + + busFactoryConfigurator.ReceiveEndpoint(MassTransitEndpointNames.CompensateQueue(endpointName), endpointConfigurator => + endpointConfigurator.CompensateActivityHost(busRegistrationContext) + ); + } + } +} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Options.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Options.cs new file mode 100644 index 0000000..3255d0a --- /dev/null +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Options.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Musify.Infrastructure.MassTransit +{ + public static partial class MassTransitDependencyInjection + { + static void RegisterValidatedOptions( + IServiceCollection services, + IConfiguration configuration, + string sectionName) + where TOptions : class + { + services + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(sectionName)) + .ValidateDataAnnotations(); + + services.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + } + } +} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs new file mode 100644 index 0000000..f1620cd --- /dev/null +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -0,0 +1,144 @@ +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Activities; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Audio; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.Tracks; +using Musify.Infrastructure.MassTransit.Arguments; +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 + { + static void RegisterRoutingSlipBuilders(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options) + { + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + options.AddActivity(); + } + + static void ConfigureRabbitMqWorkerEndpoints( + IBusRegistrationContext busRegistrationContext, + IRabbitMqBusFactoryConfigurator busFactoryConfigurator) + { + ConfigureRabbitMqHost( + busFactoryConfigurator, + busRegistrationContext.GetRequiredService()); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdatePlayListPictureConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackAudioConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackPictureConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + DeleteTrackConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + RemoveFileConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + RoutingSlipCleanUpConsumer.QueueName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + RemoveFileFromBucketActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + GenerateAudioWorkflowPathsActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + GeneratePictureWorkflowPathsActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + ResizePictureActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdatePlayListPictureActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackPictureActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + DownloadFileFromBucketActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + TranscodeDashAudioActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + TransferFilesToBucketActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdateTrackAudioActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UploadFileToBucketActivity.ExecuteEndpointName); + } + } +} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 499e8b0..75f3132 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -1,33 +1,19 @@ using MassTransit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.MassTransit.Activities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Audio; -using Musify.Infrastructure.MassTransit.Activities.Files; -using Musify.Infrastructure.MassTransit.Activities.Logs; -using Musify.Infrastructure.MassTransit.Arguments; -using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.MassTransit.Logs; -using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; -using Musify.Infrastructure.MassTransit.Activities.Tracks; namespace Musify.Infrastructure.MassTransit { - public static class MassTransitDependencyInjection + public static partial class MassTransitDependencyInjection { public static IServiceCollection AddMassTransitClient(this IServiceCollection serviceDescriptors, IConfiguration configuration) { - serviceDescriptors - .AddOptionsWithValidateOnStart() - .Bind(configuration.GetRequiredSection(MassTransitConfiguration.SectionName)) - .ValidateDataAnnotations(); - - serviceDescriptors.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService>().Value); + RegisterValidatedOptions( + serviceDescriptors, + configuration, + MassTransitConfiguration.SectionName); serviceDescriptors.AddScoped(); serviceDescriptors.AddMassTransit(options => @@ -43,124 +29,24 @@ namespace Musify.Infrastructure.MassTransit public static IServiceCollection AddMassTransitConsumers(this IServiceCollection serviceDescriptors, IConfiguration configuration) { - serviceDescriptors - .AddOptionsWithValidateOnStart() - .Bind(configuration.GetRequiredSection(MassTransitConfiguration.SectionName)) - .ValidateDataAnnotations(); + RegisterValidatedOptions( + serviceDescriptors, + configuration, + MassTransitConfiguration.SectionName); - serviceDescriptors.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService>().Value); + RegisterValidatedOptions( + serviceDescriptors, + configuration, + WorkerConfiguration.SectionName); - serviceDescriptors - .AddOptionsWithValidateOnStart() - .Bind(configuration.GetRequiredSection(WorkerConfiguration.SectionName)) - .ValidateDataAnnotations(); - - serviceDescriptors.AddSingleton(serviceProvider => - serviceProvider.GetRequiredService>().Value); - - serviceDescriptors.AddScoped(); - serviceDescriptors.AddScoped(); - serviceDescriptors.AddScoped(); + RegisterRoutingSlipBuilders(serviceDescriptors); serviceDescriptors.AddMassTransit(options => { - options.AddConsumer(); - options.AddConsumer(); - options.AddConsumer(); - options.AddConsumer(); - options.AddConsumer(); - - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - options.AddExecuteActivity(); - - options.AddActivity(); - options.AddActivity(); - options.AddActivity(); - options.AddActivity(); - options.AddActivity(); - options.AddActivity(); - options.AddActivity(); - options.AddActivity(); + RegisterConsumersAndActivities(options); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => - { - ConfigureRabbitMqHost( - busFactoryConfigurator, - busRegistrationContext.GetRequiredService()); - - busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - - busFactoryConfigurator.ReceiveEndpoint(UpdateTrackAudioConsumer.QueueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - - busFactoryConfigurator.ReceiveEndpoint(UpdateTrackPictureConsumer.QueueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - - busFactoryConfigurator.ReceiveEndpoint(DeleteTrackConsumer.QueueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - - busFactoryConfigurator.ReceiveEndpoint(RoutingSlipCleanUpConsumer.QueueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - RemoveFileFromBucketActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - GenerateAudioWorkflowPathsActivity.ExecuteEndpointName); - - ConfigureExecuteActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - GeneratePictureWorkflowPathsActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - ResizePictureActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - UpdatePlayListPictureActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - UpdateTrackPictureActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - DownloadFileFromBucketActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - TranscodeDashAudioActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - TransferFilesToBucketActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - UpdateTrackAudioActivity.ExecuteEndpointName); - - ConfigureActivityEndpoint( - busFactoryConfigurator, - busRegistrationContext, - UploadFileToBucketActivity.ExecuteEndpointName); - } ); + ConfigureRabbitMqWorkerEndpoints(busRegistrationContext, busFactoryConfigurator)); } ); return serviceDescriptors; @@ -176,37 +62,5 @@ namespace Musify.Infrastructure.MassTransit options.Password(massTransitConfiguration.Password); } ); } - - static void ConfigureExecuteActivityEndpoint( - IRabbitMqBusFactoryConfigurator busFactoryConfigurator, - IBusRegistrationContext busRegistrationContext, - string endpointName) - where TActivity : class, IExecuteActivity - where TArguments : class - { - busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => - { - endpointConfigurator.ExecuteActivityHost(busRegistrationContext); - } ); - } - - static void ConfigureActivityEndpoint( - IRabbitMqBusFactoryConfigurator busFactoryConfigurator, - IBusRegistrationContext busRegistrationContext, - string endpointName) - where TActivity : class, IActivity - where TArguments : class - where TLog : class - { - busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_execute", endpointConfigurator => - endpointConfigurator.ExecuteActivityHost( - EndpointHelper.BuildCompensateActivityUri(endpointName), - busRegistrationContext) - ); - - busFactoryConfigurator.ReceiveEndpoint($"{endpointName}_compensate", endpointConfigurator => - endpointConfigurator.CompensateActivityHost(busRegistrationContext) - ); - } } } \ No newline at end of file diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitEndpointNames.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitEndpointNames.cs new file mode 100644 index 0000000..cdcdffc --- /dev/null +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitEndpointNames.cs @@ -0,0 +1,9 @@ +namespace Musify.Infrastructure.MassTransit +{ + internal static class MassTransitEndpointNames + { + internal static string ExecuteQueue(string endpointName) => $"{endpointName}_execute"; + + internal static string CompensateQueue(string endpointName) => $"{endpointName}_compensate"; + } +} diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs index a760eb7..c6e7387 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs @@ -3,7 +3,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Logs public record UpdateTrackPictureLog( Guid TrackId, string PreviousOriginalPictureKey, - string PreviousSmallPictureKey, - string PreviousMediumPictureKey, - string PreviousLargePictureKey); + string? PreviousSmallPictureKey, + string? PreviousMediumPictureKey, + string? PreviousLargePictureKey); } diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs index 83c5c1f..0ac4f0c 100644 --- a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -6,7 +6,6 @@ using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -using SixLabors.ImageSharp; namespace Musify.Infrastructure.Observability { diff --git a/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs index d184dd8..3c04607 100644 --- a/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs +++ b/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable From 8cda85e5626832ca89840780e5186e38b52fdc97 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 01:05:07 +0200 Subject: [PATCH 132/172] Renames --- .../Application/IFileData.cs | 2 +- .../Application/PaginatedResponse.cs | 2 +- .../Infrastructure/IAudioTranscoderService.cs | 2 +- .../Infrastructure/IDatabase.cs | 2 +- .../Infrastructure/IEventBus.cs | 2 +- .../Infrastructure/IPictureService.cs | 2 +- .../Infrastructure/IStorageService.cs | 2 +- .../PlayLists/Commands/CreatePlayListCommand.cs | 4 ++-- .../PlayLists/Commands/UpdatePlayListCommand.cs | 4 ++-- .../Handlers/CreatePlayListCommandHandler.cs | 10 +++++----- .../Handlers/DeletePlayListCommandHandler.cs | 2 +- .../Handlers/GetPlayListByIdQueryHandler.cs | 10 +++++----- .../Handlers/GetPlayListsByUserIdQueryHandler.cs | 12 ++++++------ .../PlayLists/Handlers/GetPlayListsQueryHandler.cs | 12 ++++++------ .../Handlers/UpdatePlayListCommandHandler.cs | 10 +++++----- .../PlayLists/Queries/GetPlayListByIdQuery.cs | 2 +- .../PlayLists/Queries/GetPlayListsByUserIdQuery.cs | 4 ++-- Application/PlayLists/Queries/GetPlayListsQuery.cs | 4 ++-- ...istResponse.cs => PlayListApplicationResponse.cs} | 6 +++--- Application/Tracks/Commands/CreateTrackCommand.cs | 4 ++-- .../Tracks/Handler/CreateTrackCommandHandler.cs | 8 ++++---- .../Tracks/Handler/DeleteTrackCommandHandler.cs | 2 +- .../Tracks/Handler/GetTrackByIdQueryHandler.cs | 10 +++++----- .../Tracks/Handler/GetTracksByUserIdQueryHandler.cs | 12 ++++++------ Application/Tracks/Handler/GetTracksQueryHandler.cs | 12 ++++++------ Application/Tracks/Queries/GetTrackByIdQuery.cs | 2 +- Application/Tracks/Queries/GetTracksByUserIdQuery.cs | 4 ++-- Application/Tracks/Queries/GetTracksQuery.cs | 4 ++-- ...{TrackResponse.cs => TrackApplicationResponse.cs} | 6 +++--- Application/Users/Commands/CreateUserCommand.cs | 2 +- .../Users/Handlers/CreateUserCommandHandler.cs | 10 +++++----- .../Users/Handlers/GetUserByIdQueryHandler.cs | 10 +++++----- .../Users/Handlers/GetUserByKeycloakQueryHandler.cs | 10 +++++----- Application/Users/Handlers/GetUsersQueryHandler.cs | 12 ++++++------ Application/Users/Queries/GetUserByIdQuery.cs | 2 +- Application/Users/Queries/GetUserByKeycloakQuery.cs | 2 +- Application/Users/Queries/GetUsersQuery.cs | 4 ++-- Application/Users/Responses/KeycloakUserResponse.cs | 8 -------- .../{UserResponse.cs => UserApplicationResponse.cs} | 6 +++--- .../Audio/GenerateAudioWorkflowPathsActivity.cs | 2 +- .../Activities/Audio/TranscodeDashAudioActivity.cs | 2 +- .../Activities/Audio/UpdateTrackAudioActivity.cs | 2 +- .../Files/DownloadFileFromBucketActivity.cs | 2 +- .../Activities/Files/RemoveFileFromBucketActivity.cs | 2 +- .../Files/RemoveFolderFromBucketActivity.cs | 2 +- .../Files/TransferFilesToBucketActivity.cs | 2 +- .../Activities/Files/UploadFileToBucketActivity.cs | 2 +- .../Activities/Pictures/ResizePictureActivity.cs | 2 +- .../Pictures/UpdatePlayListPictureActivity.cs | 2 +- .../Pictures/UpdateTrackPictureActivity.cs | 2 +- .../Activities/Tracks/DeleteTrackActivity.cs | 2 +- .../MassTransit/Consumers/RemoveFileConsumer.cs | 2 +- .../MassTransitDependencyInjection.cs | 2 +- .../MassTransit/EventBus/MassTransitEventBus.cs | 2 +- Infrastructure/Persistence/Database.cs | 2 +- .../Persistence/DatabaseDependencyInjection.cs | 2 +- Infrastructure/Services/AudioTranscoderService.cs | 2 +- Infrastructure/Services/PictureService.cs | 2 +- .../Services/ServicesDependencyInjection.cs | 2 +- Infrastructure/Services/StorageService.cs | 2 +- 60 files changed, 129 insertions(+), 137 deletions(-) rename Application/{Contracts => Abstractions}/Application/IFileData.cs (76%) rename Application/{Contracts => Abstractions}/Application/PaginatedResponse.cs (92%) rename Application/{Contracts => Abstractions}/Infrastructure/IAudioTranscoderService.cs (82%) rename Application/{Contracts => Abstractions}/Infrastructure/IDatabase.cs (88%) rename Application/{Contracts => Abstractions}/Infrastructure/IEventBus.cs (67%) rename Application/{Contracts => Abstractions}/Infrastructure/IPictureService.cs (75%) rename Application/{Contracts => Abstractions}/Infrastructure/IStorageService.cs (94%) rename Application/PlayLists/Responses/{PlayListResponse.cs => PlayListApplicationResponse.cs} (78%) rename Application/Tracks/Responses/{TrackResponse.cs => TrackApplicationResponse.cs} (67%) delete mode 100644 Application/Users/Responses/KeycloakUserResponse.cs rename Application/Users/Responses/{UserResponse.cs => UserApplicationResponse.cs} (73%) diff --git a/Application/Contracts/Application/IFileData.cs b/Application/Abstractions/Application/IFileData.cs similarity index 76% rename from Application/Contracts/Application/IFileData.cs rename to Application/Abstractions/Application/IFileData.cs index 96a9347..8d92e55 100644 --- a/Application/Contracts/Application/IFileData.cs +++ b/Application/Abstractions/Application/IFileData.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Application +namespace Musify.Application.Abstractions.Application { public interface IFileData { diff --git a/Application/Contracts/Application/PaginatedResponse.cs b/Application/Abstractions/Application/PaginatedResponse.cs similarity index 92% rename from Application/Contracts/Application/PaginatedResponse.cs rename to Application/Abstractions/Application/PaginatedResponse.cs index 9f9b5ac..fe38d61 100644 --- a/Application/Contracts/Application/PaginatedResponse.cs +++ b/Application/Abstractions/Application/PaginatedResponse.cs @@ -1,6 +1,6 @@ using X.PagedList; -namespace Musify.Application.Contracts.Application +namespace Musify.Application.Abstractions.Application { public record PaginatedResponse( IReadOnlyCollection Items, diff --git a/Application/Contracts/Infrastructure/IAudioTranscoderService.cs b/Application/Abstractions/Infrastructure/IAudioTranscoderService.cs similarity index 82% rename from Application/Contracts/Infrastructure/IAudioTranscoderService.cs rename to Application/Abstractions/Infrastructure/IAudioTranscoderService.cs index 3c74cd5..69b86b3 100644 --- a/Application/Contracts/Infrastructure/IAudioTranscoderService.cs +++ b/Application/Abstractions/Infrastructure/IAudioTranscoderService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Abstractions.Infrastructure { public interface IAudioTranscoderService { diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Abstractions/Infrastructure/IDatabase.cs similarity index 88% rename from Application/Contracts/Infrastructure/IDatabase.cs rename to Application/Abstractions/Infrastructure/IDatabase.cs index 98ec9f4..ea81d4e 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Abstractions/Infrastructure/IDatabase.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Musify.Domain.Entities; -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Abstractions.Infrastructure { public interface IDatabase { diff --git a/Application/Contracts/Infrastructure/IEventBus.cs b/Application/Abstractions/Infrastructure/IEventBus.cs similarity index 67% rename from Application/Contracts/Infrastructure/IEventBus.cs rename to Application/Abstractions/Infrastructure/IEventBus.cs index 0091321..578a1b2 100644 --- a/Application/Contracts/Infrastructure/IEventBus.cs +++ b/Application/Abstractions/Infrastructure/IEventBus.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Abstractions.Infrastructure { public interface IEventBus { diff --git a/Application/Contracts/Infrastructure/IPictureService.cs b/Application/Abstractions/Infrastructure/IPictureService.cs similarity index 75% rename from Application/Contracts/Infrastructure/IPictureService.cs rename to Application/Abstractions/Infrastructure/IPictureService.cs index 66bc445..666f6e5 100644 --- a/Application/Contracts/Infrastructure/IPictureService.cs +++ b/Application/Abstractions/Infrastructure/IPictureService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Abstractions.Infrastructure { public interface IPictureService { diff --git a/Application/Contracts/Infrastructure/IStorageService.cs b/Application/Abstractions/Infrastructure/IStorageService.cs similarity index 94% rename from Application/Contracts/Infrastructure/IStorageService.cs rename to Application/Abstractions/Infrastructure/IStorageService.cs index 3127c35..a40b237 100644 --- a/Application/Contracts/Infrastructure/IStorageService.cs +++ b/Application/Abstractions/Infrastructure/IStorageService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Abstractions.Infrastructure { public interface IStorageService { diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index 5b6bdbb..6da1706 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -11,7 +11,7 @@ namespace Musify.Application.PlayLists.Commands string Name, string Description, IFileData? Picture) - : ICommand> + : ICommand> { public static PlayList ToEntity(CreatePlayListCommand command, string originalKey, string smallKey, string mediumKey, string largeKey) { diff --git a/Application/PlayLists/Commands/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayListCommand.cs index 3aac73c..748b8a9 100644 --- a/Application/PlayLists/Commands/UpdatePlayListCommand.cs +++ b/Application/PlayLists/Commands/UpdatePlayListCommand.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Commands @@ -11,5 +11,5 @@ namespace Musify.Application.PlayLists.Commands string? NewName, string? NewDescription, IFileData? NewPicture) - : ICommand>; + : ICommand>; } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 543cb4d..76fa134 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -3,8 +3,8 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; @@ -19,9 +19,9 @@ namespace Musify.Application.PlayLists.Handlers ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) - : ICommandHandler> + : ICommandHandler> { - public async ValueTask> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) { var userExists = await database.Users .AsNoTracking() @@ -69,7 +69,7 @@ namespace Musify.Application.PlayLists.Handlers } logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId); - return Result.Created(PlayListResponse.FromEntity(playList)); + return Result.Created(PlayListApplicationResponse.FromEntity(playList)); } async Task TryRollbackOnPublishFailureAsync(PlayList playList, CancellationToken cancellationToken) diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 900d44d..76709cb 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -3,7 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index 6b430c7..f54ae91 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using Mediator; @@ -8,16 +8,16 @@ using Mediator; namespace Musify.Application.PlayLists.Handlers { public class GetPlayListByIdQueryHandler(IDatabase database) - : IQueryHandler> + : IQueryHandler> { - public async ValueTask> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken) { var playList = await database.PlayLists .AsNoTracking() - .Select(p => PlayListResponse.FromEntity(p)) + .Select(p => PlayListApplicationResponse.FromEntity(p)) .SingleOrDefaultAsync(p => p.Id == request.Id, cancellationToken); - return playList ?? Result.NotFound(); + return playList ?? Result.NotFound(); } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index 6c7cedf..8cfd94d 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; @@ -10,9 +10,9 @@ using Mediator; namespace Musify.Application.PlayLists.Handlers { public class GetPlayListsByUserIdQueryHandler(IDatabase database) - : IQueryHandler > > + : IQueryHandler > > { - public async ValueTask >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -31,10 +31,10 @@ namespace Musify.Application.PlayLists.Handlers var pagedPlayLists = await playListsQuery .OrderBy(p => p.CreatedDate) - .Select(p => PlayListResponse.FromEntity(p)) + .Select(p => PlayListApplicationResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); + return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index 7eb435c..e05fbe2 100644 --- a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; @@ -10,9 +10,9 @@ using Mediator; namespace Musify.Application.PlayLists.Handlers { public class GetPlayListsQueryHandler(IDatabase database) - : IQueryHandler >> + : IQueryHandler >> { - public async ValueTask >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -22,10 +22,10 @@ namespace Musify.Application.PlayLists.Handlers var pagedPlayLists = await database.PlayLists .AsNoTracking() .OrderBy(p => p.Id) - .Select(p => PlayListResponse.FromEntity(p)) + .Select(p => PlayListApplicationResponse.FromEntity(p)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); + return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 2235d02..0ec4b4b 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -3,8 +3,8 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; @@ -15,12 +15,12 @@ namespace Musify.Application.PlayLists.Handlers public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) - : ICommandHandler> + : ICommandHandler> { record UpdatePictureOutcome(Result Result, bool PictureUpdated, string? UploadedPictureKey); record PreviousPlayListState(string Name, string NormalizedName, string Description, string OriginalPictureName); - public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); if (playListEntity is null) @@ -72,7 +72,7 @@ namespace Musify.Application.PlayLists.Handlers } logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id); - return Result.Success(PlayListResponse.FromEntity(playListEntity)); + return Result.Success(PlayListApplicationResponse.FromEntity(playListEntity)); } void UpdateName(PlayList playList, string? newName) diff --git a/Application/PlayLists/Queries/GetPlayListByIdQuery.cs b/Application/PlayLists/Queries/GetPlayListByIdQuery.cs index 27cc8c0..55d98c5 100644 --- a/Application/PlayLists/Queries/GetPlayListByIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListByIdQuery.cs @@ -5,5 +5,5 @@ using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries { public record GetPlayListByIdQuery(Guid Id) - : IQuery>; + : IQuery>; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index 8f5439e..788853c 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries @@ -10,5 +10,5 @@ namespace Musify.Application.PlayLists.Queries string? Name = null, int PageNumber = 1, int PageSize = 10) - : IQuery> >; + : IQuery> >; } \ No newline at end of file diff --git a/Application/PlayLists/Queries/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayListsQuery.cs index c8ae5de..f532d99 100644 --- a/Application/PlayLists/Queries/GetPlayListsQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsQuery.cs @@ -1,10 +1,10 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries { public record GetPlayListsQuery(int PageNumber = 1, int PageSize = 10) - : IQuery> >; + : IQuery> >; } \ No newline at end of file diff --git a/Application/PlayLists/Responses/PlayListResponse.cs b/Application/PlayLists/Responses/PlayListApplicationResponse.cs similarity index 78% rename from Application/PlayLists/Responses/PlayListResponse.cs rename to Application/PlayLists/Responses/PlayListApplicationResponse.cs index 6ebebaf..994996a 100644 --- a/Application/PlayLists/Responses/PlayListResponse.cs +++ b/Application/PlayLists/Responses/PlayListApplicationResponse.cs @@ -2,7 +2,7 @@ namespace Musify.Application.PlayLists.Responses { - public record PlayListResponse( + public record PlayListApplicationResponse( Guid Id, string Name, string Description, @@ -12,9 +12,9 @@ namespace Musify.Application.PlayLists.Responses DateTime CreatedAt, DateTime UpdatedAt) { - public static PlayListResponse FromEntity(PlayList playList) + public static PlayListApplicationResponse FromEntity(PlayList playList) { - return new PlayListResponse( + return new PlayListApplicationResponse( playList.Id, playList.Name, playList.Description, diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index cc65340..f99e324 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.Tracks.Responses; using Musify.Domain.Entities; @@ -11,7 +11,7 @@ namespace Musify.Application.Tracks.Commands string Title, IFileData Picture, IFileData Audio) - : ICommand > + : ICommand > { public static Track ToEntity(CreateTrackCommand command, string smallPictureName, string mediumPictureName, string largePictureName) { diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index e07eaa5..07625a3 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -3,7 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; @@ -13,9 +13,9 @@ namespace Musify.Application.Tracks.Handler { public class CreateTrackCommandHandler(IDatabase database, IEventBus eventBus, IStorageService storageHandler, ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) - : ICommandHandler > + : ICommandHandler > { - public async ValueTask> Handle(CreateTrackCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateTrackCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AsNoTracking() .AnyAsync(u => u.Id == request.UserId, cancellationToken); @@ -64,7 +64,7 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to create track for {request.Title}"); } - return Result.Created(TrackResponse.FromEntity(trackEntity)); + return Result.Created(TrackApplicationResponse.FromEntity(trackEntity)); } async Task UploadFilesAsync( diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 8b41fd8..8c78e75 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -2,7 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Tracks.Commands; using Musify.Domain.Entities; using Musify.Application.Events; diff --git a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs index f529bab..85607e3 100644 --- a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs @@ -1,22 +1,22 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Handler { public class GetTrackByIdQueryHandler(IDatabase database) - : IQueryHandler > + : IQueryHandler > { - public async ValueTask> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) { var track = await database.Tracks.AsNoTracking() - .Select(t => TrackResponse.FromEntity(t)) + .Select(t => TrackApplicationResponse.FromEntity(t)) .SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken); - return track ?? Result.NotFound(); + return track ?? Result.NotFound(); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs index c48426f..9710a2b 100644 --- a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; using X.PagedList.EF; @@ -10,9 +10,9 @@ using X.PagedList.EF; namespace Musify.Application.Tracks.Handler { public class GetTracksByUserIdQueryHandler(IDatabase database) - : IQueryHandler> > + : IQueryHandler> > { - public async ValueTask >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -32,10 +32,10 @@ namespace Musify.Application.Tracks.Handler var pagedTracks = await tracksQuery .OrderBy(t => t.Track.CreatedDate) - .Select(t => TrackResponse.FromEntity(t.Track)) + .Select(t => TrackApplicationResponse.FromEntity(t.Track)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); + return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/GetTracksQueryHandler.cs b/Application/Tracks/Handler/GetTracksQueryHandler.cs index 1f3528c..c1875d1 100644 --- a/Application/Tracks/Handler/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksQueryHandler.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; using X.PagedList.EF; @@ -10,9 +10,9 @@ using X.PagedList.EF; namespace Musify.Application.Tracks.Handler { public class GetTracksQueryHandler(IDatabase database) - : IQueryHandler> > + : IQueryHandler> > { - public async ValueTask >> Handle(GetTracksQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetTracksQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -21,12 +21,12 @@ namespace Musify.Application.Tracks.Handler .AsNoTracking() .OrderBy(t => t.Id) .Include(ut => ut.Track) - .Select(t => TrackResponse.FromEntity(t.Track)); + .Select(t => TrackApplicationResponse.FromEntity(t.Track)); var totalCount = await tracksQuery.CountAsync(cancellationToken); var pagedTracks = await tracksQuery.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); + return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); } } } \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTrackByIdQuery.cs b/Application/Tracks/Queries/GetTrackByIdQuery.cs index 97a6ee6..cdc3709 100644 --- a/Application/Tracks/Queries/GetTrackByIdQuery.cs +++ b/Application/Tracks/Queries/GetTrackByIdQuery.cs @@ -5,5 +5,5 @@ using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries { public record GetTrackByIdQuery(Guid TrackId) - : IQuery>; + : IQuery>; } \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs index f4eff24..21c378e 100644 --- a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs +++ b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries @@ -10,5 +10,5 @@ namespace Musify.Application.Tracks.Queries string? Name = null, int PageNumber = 1, int PageSize = 10) - : IQuery > >; + : IQuery > >; } \ No newline at end of file diff --git a/Application/Tracks/Queries/GetTracksQuery.cs b/Application/Tracks/Queries/GetTracksQuery.cs index d67698b..8b0dd64 100644 --- a/Application/Tracks/Queries/GetTracksQuery.cs +++ b/Application/Tracks/Queries/GetTracksQuery.cs @@ -1,10 +1,10 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries { public record GetTracksQuery(int PageNumber = 1, int PageSize = 10) - : IQuery >>; + : IQuery >>; } \ No newline at end of file diff --git a/Application/Tracks/Responses/TrackResponse.cs b/Application/Tracks/Responses/TrackApplicationResponse.cs similarity index 67% rename from Application/Tracks/Responses/TrackResponse.cs rename to Application/Tracks/Responses/TrackApplicationResponse.cs index 78143dd..33d8a83 100644 --- a/Application/Tracks/Responses/TrackResponse.cs +++ b/Application/Tracks/Responses/TrackApplicationResponse.cs @@ -2,15 +2,15 @@ namespace Musify.Application.Tracks.Responses { - public record TrackResponse( + public record TrackApplicationResponse( Guid Id, string Title, DateTime CreatedDate, DateTime UpdatedDate) { - public static TrackResponse FromEntity(Track track) + public static TrackApplicationResponse FromEntity(Track track) { - return new TrackResponse( + return new TrackApplicationResponse( track.Id, track.Title, track.CreatedDate, diff --git a/Application/Users/Commands/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs index 1b0224e..fef2150 100644 --- a/Application/Users/Commands/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -9,7 +9,7 @@ namespace Musify.Application.Users.Commands string Name, string? FirstName, string? SecondName) - : ICommand> + : ICommand> { public static User ToEntity(CreateUserCommand command) { diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index 2c6b11f..3fd02cf 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -2,22 +2,22 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Users.Commands; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { public class CreateUserCommandHandler(IDatabase database, ILogger logger) - : ICommandHandler > + : ICommandHandler > { - public async ValueTask> Handle(CreateUserCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AnyAsync(u => u.Id == request.Id, cancellationToken); if (userExists) { logger.LogInformation("User {UserId} already exists", request.Id); - return Result.Conflict($"User {request.Id} already exists"); + return Result.Conflict($"User {request.Id} already exists"); } var newUser = CreateUserCommand.ToEntity(request); @@ -34,7 +34,7 @@ namespace Musify.Application.Users.Handlers } logger.LogInformation("Created user {UserId}", newUser.Id); - return Result.Created(UserResponse.FromEntity(newUser)); + return Result.Created(UserApplicationResponse.FromEntity(newUser)); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index fe92751..6441884 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -1,23 +1,23 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { public class GetUserByIdQueryHandler(IDatabase database) - : IQueryHandler > + : IQueryHandler > { - public async ValueTask> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { var user = await database.Users .AsNoTracking() - .Select(u => UserResponse.FromEntity(u)) + .Select(u => UserApplicationResponse.FromEntity(u)) .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - return user ?? Result.NotFound(); + return user ?? Result.NotFound(); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index f46fb00..26c3c51 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -1,23 +1,23 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Handlers { public class GetUserByKeycloakQueryHandler(IDatabase database) - : IQueryHandler > + : IQueryHandler > { - public async ValueTask> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) { var user = await database.Users .AsNoTracking() - .Select(u => UserResponse.FromEntity(u)) + .Select(u => UserApplicationResponse.FromEntity(u)) .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - return user ?? Result.NotFound(); + return user ?? Result.NotFound(); } } } diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index 56c31cb..4d474c2 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; using X.PagedList.EF; @@ -10,9 +10,9 @@ using X.PagedList.EF; namespace Musify.Application.Users.Handlers { public class GetUsersQueryHandler(IDatabase database) - : IQueryHandler > > + : IQueryHandler > > { - public async ValueTask >> Handle(GetUsersQuery request, CancellationToken cancellationToken) + public async ValueTask >> Handle(GetUsersQuery request, CancellationToken cancellationToken) { var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; var pageSize = request.PageSize < 1 ? 10 : request.PageSize; @@ -29,10 +29,10 @@ namespace Musify.Application.Users.Handlers var totalCount = await user.CountAsync(cancellationToken); var pagedUser = await user - .Select(u => UserResponse.FromEntity(u)) + .Select(u => UserApplicationResponse.FromEntity(u)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); - return Result.Success(PaginatedResponse.FromPagedList(pagedUser)); + return Result.Success(PaginatedResponse.FromPagedList(pagedUser)); } } } \ No newline at end of file diff --git a/Application/Users/Queries/GetUserByIdQuery.cs b/Application/Users/Queries/GetUserByIdQuery.cs index b81c047..d23ead1 100644 --- a/Application/Users/Queries/GetUserByIdQuery.cs +++ b/Application/Users/Queries/GetUserByIdQuery.cs @@ -5,5 +5,5 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { public record GetUserByIdQuery(Guid Id) - : IQuery >; + : IQuery >; } \ No newline at end of file diff --git a/Application/Users/Queries/GetUserByKeycloakQuery.cs b/Application/Users/Queries/GetUserByKeycloakQuery.cs index 4f19a7d..f351590 100644 --- a/Application/Users/Queries/GetUserByKeycloakQuery.cs +++ b/Application/Users/Queries/GetUserByKeycloakQuery.cs @@ -5,5 +5,5 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { public record GetUserByKeycloakQuery(Guid Id) - : IQuery >; + : IQuery >; } diff --git a/Application/Users/Queries/GetUsersQuery.cs b/Application/Users/Queries/GetUsersQuery.cs index 6f403aa..5d78679 100644 --- a/Application/Users/Queries/GetUsersQuery.cs +++ b/Application/Users/Queries/GetUsersQuery.cs @@ -1,10 +1,10 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Contracts.Application; +using Musify.Application.Abstractions.Application; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch = null) - : IQuery >>; + : IQuery >>; } diff --git a/Application/Users/Responses/KeycloakUserResponse.cs b/Application/Users/Responses/KeycloakUserResponse.cs deleted file mode 100644 index 8cedf5d..0000000 --- a/Application/Users/Responses/KeycloakUserResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Musify.Application.Users.Responses -{ - public record KeycloakUserResponse( - Guid Id, - string Name, - string FirstName, - string SecondName); -} \ No newline at end of file diff --git a/Application/Users/Responses/UserResponse.cs b/Application/Users/Responses/UserApplicationResponse.cs similarity index 73% rename from Application/Users/Responses/UserResponse.cs rename to Application/Users/Responses/UserApplicationResponse.cs index 054af69..2c98ec2 100644 --- a/Application/Users/Responses/UserResponse.cs +++ b/Application/Users/Responses/UserApplicationResponse.cs @@ -2,7 +2,7 @@ namespace Musify.Application.Users.Responses { - public record UserResponse( + public record UserApplicationResponse( Guid Id, string Name, string? FirstName, @@ -10,9 +10,9 @@ namespace Musify.Application.Users.Responses DateTime CreatedAt, DateTime UpdatedAt) { - public static UserResponse FromEntity(User user) + public static UserApplicationResponse FromEntity(User user) { - return new UserResponse( + return new UserApplicationResponse( user.Id, user.Name, user.FirstName, diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 73f4dcc..606f66e 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 4766ae5..6da0ffb 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index cf32e81..7fc302d 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index f7d7db8..3612ffa 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 9f8b29f..45125d8 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; namespace Musify.Infrastructure.MassTransit.Activities.Files diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs index 98f1289..6170921 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Arguments; namespace Musify.Infrastructure.MassTransit.Activities.Files diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 5f6ceb8..70dd5b5 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 0b675b2..a10c283 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 941132b..90b9c3c 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index c6df282..2a7afb2 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 3c807b8..8f56c5c 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs index f58f263..6175262 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs @@ -2,7 +2,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; namespace Musify.Infrastructure.MassTransit.Activities.Tracks diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs index 38345ad..e56b117 100644 --- a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Consumers diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 75f3132..f6a8317 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.MassTransit diff --git a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs index 8c2bfef..7692f01 100644 --- a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs +++ b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs @@ -1,5 +1,5 @@ using MassTransit; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; namespace Musify.Infrastructure.MassTransit { diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 8519719..94874bb 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index 0a3f01c..c5db7ac 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Persistence diff --git a/Infrastructure/Services/AudioTranscoderService.cs b/Infrastructure/Services/AudioTranscoderService.cs index 1bec040..f7086ac 100644 --- a/Infrastructure/Services/AudioTranscoderService.cs +++ b/Infrastructure/Services/AudioTranscoderService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.Configuration; using System.Diagnostics; diff --git a/Infrastructure/Services/PictureService.cs b/Infrastructure/Services/PictureService.cs index f50f9ba..390a487 100644 --- a/Infrastructure/Services/PictureService.cs +++ b/Infrastructure/Services/PictureService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 6d76ccf..56e48e7 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Services diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index fc5dbd6..1f170ef 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -3,7 +3,7 @@ using Amazon.S3.Model; using Amazon.S3.Transfer; using Microsoft.Extensions.Logging; using MimeMapping; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; From 214537697e643139b681e36bdd1bc1a5aad7d555 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 01:06:30 +0200 Subject: [PATCH 133/172] Update migrations --- Infrastructure/Infrastructure.csproj | 4 + ...20260513230601_FirstMigration.Designer.cs} | 132 ++++-------------- ...on.cs => 20260513230601_FirstMigration.cs} | 79 ++--------- .../Migrations/DatabaseModelSnapshot.cs | 130 ++++------------- 4 files changed, 79 insertions(+), 266 deletions(-) rename Infrastructure/Persistence/Migrations/{20260328141805_FirstMigration.Designer.cs => 20260513230601_FirstMigration.Designer.cs} (76%) rename Infrastructure/Persistence/Migrations/{20260328141805_FirstMigration.cs => 20260513230601_FirstMigration.cs} (70%) diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 30d947b..9e42c50 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -42,4 +42,8 @@ + + + + diff --git a/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.Designer.cs b/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs similarity index 76% rename from Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.Designer.cs rename to Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs index f84d009..30ca380 100644 --- a/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Musify.Infrastructure.Persistence.Migrations { [DbContext(typeof(Database))] - [Migration("20260328141805_FirstMigration")] + [Migration("20260513230601_FirstMigration")] partial class FirstMigration { /// @@ -109,86 +109,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("PlayListHasTrack"); }); - modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConversationId") - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("ErrorMessage") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("FinishedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ProcessName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("StartedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CorrelationId", "ProcessName"); - - b.ToTable("ProcessExecution"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Attempt") - .HasColumnType("integer"); - - b.Property("ComponentType") - .HasColumnType("integer"); - - b.Property("ErrorMessage") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("FinishedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("ProcessExecutionId") - .HasColumnType("uuid"); - - b.Property("StartedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StepName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("ProcessExecutionId", "StepName"); - - b.ToTable("ProcessStepExecution"); - }); - modelBuilder.Entity("Musify.Domain.Entities.Track", b => { b.Property("Id") @@ -196,20 +116,23 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasColumnType("uuid"); b.Property("AudioFolderName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); b.Property("LargePictureName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + b.Property("MediumPictureName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -228,8 +151,13 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + b.Property("SmallPictureName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -284,6 +212,24 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -343,17 +289,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("Track"); }); - modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => - { - b.HasOne("Musify.Domain.Entities.ProcessExecution", "ProcessExecution") - .WithMany("Steps") - .HasForeignKey("ProcessExecutionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ProcessExecution"); - }); - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => { b.HasOne("Musify.Domain.Entities.Track", "Track") @@ -378,11 +313,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("PlayListTracks"); }); - modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => - { - b.Navigation("Steps"); - }); - modelBuilder.Entity("Musify.Domain.Entities.Track", b => { b.Navigation("PlayListTracks"); diff --git a/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.cs similarity index 70% rename from Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs rename to Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.cs index 3c04607..17e8882 100644 --- a/Infrastructure/Persistence/Migrations/20260328141805_FirstMigration.cs +++ b/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -10,25 +11,6 @@ namespace Musify.Infrastructure.Persistence.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.CreateTable( - name: "ProcessExecution", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CorrelationId = table.Column(type: "uuid", nullable: true), - ConversationId = table.Column(type: "uuid", nullable: true), - MessageId = table.Column(type: "uuid", nullable: true), - ProcessName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - Status = table.Column(type: "integer", nullable: false), - StartedDateTime = table.Column(type: "timestamp with time zone", nullable: false), - FinishedDateTime = table.Column(type: "timestamp with time zone", nullable: true), - ErrorMessage = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ProcessExecution", x => x.Id); - }); - migrationBuilder.CreateTable( name: "Tracks", columns: table => new @@ -37,11 +19,15 @@ namespace Musify.Infrastructure.Persistence.Migrations Title = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), NormalizedTitle = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), OriginalPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - SmallPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - MediumPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - LargePictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + SmallPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + MediumPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + LargePictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), OriginalAudioName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - AudioFolderName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + AudioFolderName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + PicturesProcessingStatus = table.Column(type: "integer", nullable: false), + AudioTranscodeProcessingStatus = table.Column(type: "integer", nullable: false), + RetryCount = table.Column(type: "integer", nullable: false), + LastRetryAt = table.Column(type: "timestamp with time zone", nullable: true), CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -71,6 +57,10 @@ namespace Musify.Infrastructure.Persistence.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(48)", maxLength: 48, nullable: false), + NormalizedName = table.Column(type: "character varying(48)", maxLength: 48, nullable: false), + FirstName = table.Column(type: "character varying(48)", maxLength: 48, nullable: true), + SecondName = table.Column(type: "character varying(48)", maxLength: 48, nullable: true), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -79,31 +69,6 @@ namespace Musify.Infrastructure.Persistence.Migrations table.PrimaryKey("PK_User", x => x.Id); }); - migrationBuilder.CreateTable( - name: "ProcessStepExecution", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ProcessExecutionId = table.Column(type: "uuid", nullable: false), - StepName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - ComponentType = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Attempt = table.Column(type: "integer", nullable: false), - StartedDateTime = table.Column(type: "timestamp with time zone", nullable: false), - FinishedDateTime = table.Column(type: "timestamp with time zone", nullable: true), - ErrorMessage = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ProcessStepExecution", x => x.Id); - table.ForeignKey( - name: "FK_ProcessStepExecution_ProcessExecution_ProcessExecutionId", - column: x => x.ProcessExecutionId, - principalTable: "ProcessExecution", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - migrationBuilder.CreateTable( name: "PlayLists", columns: table => new @@ -198,16 +163,6 @@ namespace Musify.Infrastructure.Persistence.Migrations table: "PlayLists", column: "UserId"); - migrationBuilder.CreateIndex( - name: "IX_ProcessExecution_CorrelationId_ProcessName", - table: "ProcessExecution", - columns: new[] { "CorrelationId", "ProcessName" }); - - migrationBuilder.CreateIndex( - name: "IX_ProcessStepExecution_ProcessExecutionId_StepName", - table: "ProcessStepExecution", - columns: new[] { "ProcessExecutionId", "StepName" }); - migrationBuilder.CreateIndex( name: "IX_UserHasTrack_TrackId", table: "UserHasTrack", @@ -225,9 +180,6 @@ namespace Musify.Infrastructure.Persistence.Migrations migrationBuilder.DropTable( name: "PlayListHasTrack"); - migrationBuilder.DropTable( - name: "ProcessStepExecution"); - migrationBuilder.DropTable( name: "Upload"); @@ -237,9 +189,6 @@ namespace Musify.Infrastructure.Persistence.Migrations migrationBuilder.DropTable( name: "PlayLists"); - migrationBuilder.DropTable( - name: "ProcessExecution"); - migrationBuilder.DropTable( name: "Tracks"); diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 85d9421..0f04a84 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -106,86 +106,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("PlayListHasTrack"); }); - modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConversationId") - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("ErrorMessage") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("FinishedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ProcessName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("StartedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CorrelationId", "ProcessName"); - - b.ToTable("ProcessExecution"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Attempt") - .HasColumnType("integer"); - - b.Property("ComponentType") - .HasColumnType("integer"); - - b.Property("ErrorMessage") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("FinishedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("ProcessExecutionId") - .HasColumnType("uuid"); - - b.Property("StartedDateTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("StepName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("ProcessExecutionId", "StepName"); - - b.ToTable("ProcessStepExecution"); - }); - modelBuilder.Entity("Musify.Domain.Entities.Track", b => { b.Property("Id") @@ -193,20 +113,23 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasColumnType("uuid"); b.Property("AudioFolderName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + b.Property("CreatedDate") .HasColumnType("timestamp with time zone"); b.Property("LargePictureName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + b.Property("MediumPictureName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -225,8 +148,13 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + b.Property("SmallPictureName") - .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -281,6 +209,24 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -340,17 +286,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("Track"); }); - modelBuilder.Entity("Musify.Domain.Entities.ProcessStepExecution", b => - { - b.HasOne("Musify.Domain.Entities.ProcessExecution", "ProcessExecution") - .WithMany("Steps") - .HasForeignKey("ProcessExecutionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ProcessExecution"); - }); - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => { b.HasOne("Musify.Domain.Entities.Track", "Track") @@ -375,11 +310,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("PlayListTracks"); }); - modelBuilder.Entity("Musify.Domain.Entities.ProcessExecution", b => - { - b.Navigation("Steps"); - }); - modelBuilder.Entity("Musify.Domain.Entities.Track", b => { b.Navigation("PlayListTracks"); From d2b83524cfc7ea08401eaef12d02ac8f85ab5300 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 01:32:29 +0200 Subject: [PATCH 134/172] Bump version --- Application/Application.csproj | 2 +- Domain/Domain.csproj | 2 +- Infrastructure/Infrastructure.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index ac3e192..a6c1f02 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -6,7 +6,7 @@ enable Musify.Application Musify.Application - 0.1.1 + 0.1.2 NotS1mp https://github.com/XShine555/MusifyBackend diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index b2ee78d..879a55c 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -6,7 +6,7 @@ enable Musify.Domain Musify.Domain - 0.1.1 + 0.1.2 NotS1mp https://github.com/XShine555/MusifyBackend diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 9e42c50..fde65d6 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -8,7 +8,7 @@ 07d419fc-9ade-471a-bc0e-aebece785b30 Musify.Infrastructure Musify.Infrastructure - 0.1.0 + 0.1.1 NotS1mp https://github.com/XShine555/MusifyBackend From 7215eebb5a576d8b213ecb773051455b7756fe65 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 14:32:19 +0200 Subject: [PATCH 135/172] A lot of changes --- .agents/skills/csharp-async/SKILL.md | 49 ++ .agents/skills/csharp-docs/SKILL.md | 62 +++ .agents/skills/csharp-mstest/SKILL.md | 478 ++++++++++++++++ .agents/skills/csharp-nunit/SKILL.md | 71 +++ .agents/skills/csharp-tunit/SKILL.md | 100 ++++ .agents/skills/csharp-xunit/SKILL.md | 68 +++ .agents/skills/dotnet-best-practices/SKILL.md | 85 +++ .../dotnet-design-pattern-review/SKILL.md | 42 ++ .agents/skills/dotnet-upgrade/SKILL.md | 116 ++++ .../Abstractions/Infrastructure/IEventBus.cs | 2 +- .../Infrastructure/IStorageService.cs | 2 + .../Configuration/PlayListConfiguration.cs | 34 +- .../Configuration/TrackConfiguration.cs | 44 +- .../Commands/CreatePlayListCommand.cs | 22 +- .../RequestPlayListPictureUploadCommand.cs | 12 + .../Commands/UpdatePlayListCommand.cs | 3 +- .../Handlers/CreatePlayListCommandHandler.cs | 98 +--- .../Handlers/DeletePlayListCommandHandler.cs | 62 ++- ...uestPlayListPictureUploadCommandHandler.cs | 88 +++ .../Handlers/UpdatePlayListCommandHandler.cs | 151 +----- .../PlayListPictureUploadResponse.cs | 10 + .../Tracks/Commands/CreateTrackCommand.cs | 23 +- .../Commands/RequestTrackUploadUrlsCommand.cs | 14 + .../Handler/CreateTrackCommandHandler.cs | 137 ++--- .../Handler/DeleteTrackCommandHandler.cs | 40 +- .../RequestTrackUploadUrlsCommandHandler.cs | 110 ++++ .../Responses/TrackUploadUrlsResponse.cs | 14 + Infrastructure/Infrastructure.csproj | 1 + .../Files/UploadFileToBucketActivity.cs | 7 +- .../Activities/Tracks/DeleteTrackActivity.cs | 12 +- ...assTransitDependencyInjection.Endpoints.cs | 51 +- .../MassTransitDependencyInjection.cs | 19 + .../EventBus/MassTransitEventBus.cs | 2 +- .../PictureWorkflowRoutingSlipBuilder.cs | 2 +- Infrastructure/Persistence/Database.cs | 10 + ...514095113_AddMassTransitOutbox.Designer.cs | 512 ++++++++++++++++++ .../20260514095113_AddMassTransitOutbox.cs | 142 +++++ .../Migrations/DatabaseModelSnapshot.cs | 180 ++++++ Infrastructure/Services/StorageService.cs | 25 +- docs/changes-summary.md | 165 ++++++ docs/delete-workflows-plan.md | 175 ++++++ docs/presigned-upload-hardening.md | 95 ++++ skills-lock.json | 50 ++ 43 files changed, 2941 insertions(+), 444 deletions(-) create mode 100644 .agents/skills/csharp-async/SKILL.md create mode 100644 .agents/skills/csharp-docs/SKILL.md create mode 100644 .agents/skills/csharp-mstest/SKILL.md create mode 100644 .agents/skills/csharp-nunit/SKILL.md create mode 100644 .agents/skills/csharp-tunit/SKILL.md create mode 100644 .agents/skills/csharp-xunit/SKILL.md create mode 100644 .agents/skills/dotnet-best-practices/SKILL.md create mode 100644 .agents/skills/dotnet-design-pattern-review/SKILL.md create mode 100644 .agents/skills/dotnet-upgrade/SKILL.md create mode 100644 Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs create mode 100644 Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs create mode 100644 Application/PlayLists/Responses/PlayListPictureUploadResponse.cs create mode 100644 Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs create mode 100644 Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs create mode 100644 Application/Tracks/Responses/TrackUploadUrlsResponse.cs create mode 100644 Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs create mode 100644 docs/changes-summary.md create mode 100644 docs/delete-workflows-plan.md create mode 100644 docs/presigned-upload-hardening.md create mode 100644 skills-lock.json diff --git a/.agents/skills/csharp-async/SKILL.md b/.agents/skills/csharp-async/SKILL.md new file mode 100644 index 0000000..4dbe78b --- /dev/null +++ b/.agents/skills/csharp-async/SKILL.md @@ -0,0 +1,49 @@ +--- +name: csharp-async +description: 'Get best practices for C# async programming' +--- + +# C# Async Programming Best Practices + +Your goal is to help me follow best practices for asynchronous programming in C#. + +## Naming Conventions + +- Use the 'Async' suffix for all async methods +- Match method names with their synchronous counterparts when applicable (e.g., `GetDataAsync()` for `GetData()`) + +## Return Types + +- Return `Task` when the method returns a value +- Return `Task` when the method doesn't return a value +- Consider `ValueTask` for high-performance scenarios to reduce allocations +- Avoid returning `void` for async methods except for event handlers + +## Exception Handling + +- Use try/catch blocks around await expressions +- Avoid swallowing exceptions in async methods +- Use `ConfigureAwait(false)` when appropriate to prevent deadlocks in library code +- Propagate exceptions with `Task.FromException()` instead of throwing in async Task returning methods + +## Performance + +- Use `Task.WhenAll()` for parallel execution of multiple tasks +- Use `Task.WhenAny()` for implementing timeouts or taking the first completed task +- Avoid unnecessary async/await when simply passing through task results +- Consider cancellation tokens for long-running operations + +## Common Pitfalls + +- Never use `.Wait()`, `.Result`, or `.GetAwaiter().GetResult()` in async code +- Avoid mixing blocking and async code +- Don't create async void methods (except for event handlers) +- Always await Task-returning methods + +## Implementation Patterns + +- Implement the async command pattern for long-running operations +- Use async streams (IAsyncEnumerable) for processing sequences asynchronously +- Consider the task-based asynchronous pattern (TAP) for public APIs + +When reviewing my C# code, identify these issues and suggest improvements that follow these best practices. diff --git a/.agents/skills/csharp-docs/SKILL.md b/.agents/skills/csharp-docs/SKILL.md new file mode 100644 index 0000000..6c67306 --- /dev/null +++ b/.agents/skills/csharp-docs/SKILL.md @@ -0,0 +1,62 @@ +--- +name: csharp-docs +description: 'Ensure that C# types are documented with XML comments and follow best practices for documentation.' +--- + +# C# Documentation Best Practices + +- Public members should be documented with XML comments. +- It is encouraged to document internal members as well, especially if they are complex or not self-explanatory. + +## Guidance for all APIs + +- Use `` to provide a brief, one sentence, description of what the type or member does. Start the summary with a present-tense, third-person verb. +- Use `` for additional information, which can include implementation details, usage notes, or any other relevant context. +- Use `` for language-specific keywords like `null`, `true`, `false`, `int`, `bool`, etc. +- Use `` for inline code snippets. +- Use `` for usage examples on how to use the member. + - Use `` for code blocks. `` tags should be placed within an `` tag. Add the language of the code example using the `language` attribute, for example, ``. +- Use `` to reference other types or members inline (in a sentence). +- Use `` for standalone (not in a sentence) references to other types or members in the "See also" section of the online docs. +- Use `` to inherit documentation from base classes or interfaces. + - Unless there is major behavior change, in which case you should document the differences. + +## Methods + +- Use `` to describe method parameters. + - The description should be a noun phrase that doesn't specify the data type. + - Begin with an introductory article. + - If the parameter is a flag enum, start the description with "A bitwise combination of the enumeration values that specifies...". + - If the parameter is a non-flag enum, start the description with "One of the enumeration values that specifies...". + - If the parameter is a Boolean, the wording should be of the form "`` to ...; otherwise, ``.". + - If the parameter is an "out" parameter, the wording should be of the form "When this method returns, contains .... This parameter is treated as uninitialized.". +- Use `` to reference parameter names in documentation. +- Use `` to describe type parameters in generic types or methods. +- Use `` to reference type parameters in documentation. +- Use `` to describe what the method returns. + - The description should be a noun phrase that doesn't specify the data type. + - Begin with an introductory article. + - If the return type is Boolean, the wording should be of the form "`` if ...; otherwise, ``.". + +## Constructors + +- The summary wording should be "Initializes a new instance of the class [or struct].". + +## Properties + +- The `` should start with: + - "Gets or sets..." for a read-write property. + - "Gets..." for a read-only property. + - "Gets [or sets] a value that indicates whether..." for properties that return a Boolean value. +- Use `` to describe the value of the property. + - The description should be a noun phrase that doesn't specify the data type. + - If the property has a default value, add it in a separate sentence, for example, "The default is ``". + - If the value type is Boolean, the wording should be of the form "`` if ...; otherwise, ``. The default is ...". + +## Exceptions + +- Use `` to document exceptions thrown by constructors, properties, indexers, methods, operators, and events. +- Document all exceptions thrown directly by the member. +- For exceptions thrown by nested members, document only the exceptions users are most likely to encounter. +- The description of the exception describes the condition under which it's thrown. + - Omit "Thrown if ..." or "If ..." at the beginning of the sentence. Just state the condition directly, for example "An error occurred when accessing a Message Queuing API." diff --git a/.agents/skills/csharp-mstest/SKILL.md b/.agents/skills/csharp-mstest/SKILL.md new file mode 100644 index 0000000..e68bc31 --- /dev/null +++ b/.agents/skills/csharp-mstest/SKILL.md @@ -0,0 +1,478 @@ +--- +name: csharp-mstest +description: 'Get best practices for MSTest 3.x/4.x unit testing, including modern assertion APIs and data-driven tests' +--- + +# MSTest Best Practices (MSTest 3.x/4.x) + +Your goal is to help me write effective unit tests with modern MSTest, using current APIs and best practices. + +## Project Setup + +- Use a separate test project with naming convention `[ProjectName].Tests` +- Reference MSTest 3.x+ NuGet packages (includes analyzers) +- Consider using MSTest.Sdk for simplified project setup +- Run tests with `dotnet test` + +## Test Class Structure + +- Use `[TestClass]` attribute for test classes +- **Seal test classes by default** for performance and design clarity +- Use `[TestMethod]` for test methods (prefer over `[DataTestMethod]`) +- Follow Arrange-Act-Assert (AAA) pattern +- Name tests using pattern `MethodName_Scenario_ExpectedBehavior` + +```csharp +[TestClass] +public sealed class CalculatorTests +{ + [TestMethod] + public void Add_TwoPositiveNumbers_ReturnsSum() + { + // Arrange + var calculator = new Calculator(); + + // Act + var result = calculator.Add(2, 3); + + // Assert + Assert.AreEqual(5, result); + } +} +``` + +## Test Lifecycle + +- **Prefer constructors over `[TestInitialize]`** - enables `readonly` fields and follows standard C# patterns +- Use `[TestCleanup]` for cleanup that must run even if test fails +- Combine constructor with async `[TestInitialize]` when async setup is needed + +```csharp +[TestClass] +public sealed class ServiceTests +{ + private readonly MyService _service; // readonly enabled by constructor + + public ServiceTests() + { + _service = new MyService(); + } + + [TestInitialize] + public async Task InitAsync() + { + // Use for async initialization only + await _service.WarmupAsync(); + } + + [TestCleanup] + public void Cleanup() => _service.Reset(); +} +``` + +### Execution Order + +1. **Assembly Initialization** - `[AssemblyInitialize]` (once per test assembly) +2. **Class Initialization** - `[ClassInitialize]` (once per test class) +3. **Test Initialization** (for every test method): + 1. Constructor + 2. Set `TestContext` property + 3. `[TestInitialize]` +4. **Test Execution** - test method runs +5. **Test Cleanup** (for every test method): + 1. `[TestCleanup]` + 2. `DisposeAsync` (if implemented) + 3. `Dispose` (if implemented) +6. **Class Cleanup** - `[ClassCleanup]` (once per test class) +7. **Assembly Cleanup** - `[AssemblyCleanup]` (once per test assembly) + +## Modern Assertion APIs + +MSTest provides three assertion classes: `Assert`, `StringAssert`, and `CollectionAssert`. + +### Assert Class - Core Assertions + +```csharp +// Equality +Assert.AreEqual(expected, actual); +Assert.AreNotEqual(notExpected, actual); +Assert.AreSame(expectedObject, actualObject); // Reference equality +Assert.AreNotSame(notExpectedObject, actualObject); + +// Null checks +Assert.IsNull(value); +Assert.IsNotNull(value); + +// Boolean +Assert.IsTrue(condition); +Assert.IsFalse(condition); + +// Fail/Inconclusive +Assert.Fail("Test failed due to..."); +Assert.Inconclusive("Test cannot be completed because..."); +``` + +### Exception Testing (Prefer over `[ExpectedException]`) + +```csharp +// Assert.Throws - matches TException or derived types +var ex = Assert.Throws(() => Method(null)); +Assert.AreEqual("Value cannot be null.", ex.Message); + +// Assert.ThrowsExactly - matches exact type only +var ex = Assert.ThrowsExactly(() => Method()); + +// Async versions +var ex = await Assert.ThrowsAsync(async () => await client.GetAsync(url)); +var ex = await Assert.ThrowsExactlyAsync(async () => await Method()); +``` + +### Collection Assertions (Assert class) + +```csharp +Assert.Contains(expectedItem, collection); +Assert.DoesNotContain(unexpectedItem, collection); +Assert.ContainsSingle(collection); // exactly one element +Assert.HasCount(5, collection); +Assert.IsEmpty(collection); +Assert.IsNotEmpty(collection); +``` + +### String Assertions (Assert class) + +```csharp +Assert.Contains("expected", actualString); +Assert.StartsWith("prefix", actualString); +Assert.EndsWith("suffix", actualString); +Assert.DoesNotStartWith("prefix", actualString); +Assert.DoesNotEndWith("suffix", actualString); +Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber); +Assert.DoesNotMatchRegex(@"\d+", textOnly); +``` + +### Comparison Assertions + +```csharp +Assert.IsGreaterThan(lowerBound, actual); +Assert.IsGreaterThanOrEqualTo(lowerBound, actual); +Assert.IsLessThan(upperBound, actual); +Assert.IsLessThanOrEqualTo(upperBound, actual); +Assert.IsInRange(actual, low, high); +Assert.IsPositive(number); +Assert.IsNegative(number); +``` + +### Type Assertions + +```csharp +// MSTest 3.x - uses out parameter +Assert.IsInstanceOfType(obj, out var typed); +typed.DoSomething(); + +// MSTest 4.x - returns typed result directly +var typed = Assert.IsInstanceOfType(obj); +typed.DoSomething(); + +Assert.IsNotInstanceOfType(obj); +``` + +### Assert.That (MSTest 4.0+) + +```csharp +Assert.That(result.Count > 0); // Auto-captures expression in failure message +``` + +### StringAssert Class + +> **Note:** Prefer `Assert` class equivalents when available (e.g., `Assert.Contains("expected", actual)` over `StringAssert.Contains(actual, "expected")`). + +```csharp +StringAssert.Contains(actualString, "expected"); +StringAssert.StartsWith(actualString, "prefix"); +StringAssert.EndsWith(actualString, "suffix"); +StringAssert.Matches(actualString, new Regex(@"\d{3}-\d{4}")); +StringAssert.DoesNotMatch(actualString, new Regex(@"\d+")); +``` + +### CollectionAssert Class + +> **Note:** Prefer `Assert` class equivalents when available (e.g., `Assert.Contains`). + +```csharp +// Containment +CollectionAssert.Contains(collection, expectedItem); +CollectionAssert.DoesNotContain(collection, unexpectedItem); + +// Equality (same elements, same order) +CollectionAssert.AreEqual(expectedCollection, actualCollection); +CollectionAssert.AreNotEqual(unexpectedCollection, actualCollection); + +// Equivalence (same elements, any order) +CollectionAssert.AreEquivalent(expectedCollection, actualCollection); +CollectionAssert.AreNotEquivalent(unexpectedCollection, actualCollection); + +// Subset checks +CollectionAssert.IsSubsetOf(subset, superset); +CollectionAssert.IsNotSubsetOf(notSubset, collection); + +// Element validation +CollectionAssert.AllItemsAreInstancesOfType(collection, typeof(MyClass)); +CollectionAssert.AllItemsAreNotNull(collection); +CollectionAssert.AllItemsAreUnique(collection); +``` + +## Data-Driven Tests + +### DataRow + +```csharp +[TestMethod] +[DataRow(1, 2, 3)] +[DataRow(0, 0, 0, DisplayName = "Zeros")] +[DataRow(-1, 1, 0, IgnoreMessage = "Known issue #123")] // MSTest 3.8+ +public void Add_ReturnsSum(int a, int b, int expected) +{ + Assert.AreEqual(expected, Calculator.Add(a, b)); +} +``` + +### DynamicData + +The data source can return any of the following types: + +- `IEnumerable<(T1, T2, ...)>` (ValueTuple) - **preferred**, provides type safety (MSTest 3.7+) +- `IEnumerable>` - provides type safety +- `IEnumerable` - provides type safety plus control over test metadata (display name, categories) +- `IEnumerable` - **least preferred**, no type safety + +> **Note:** When creating new test data methods, prefer `ValueTuple` or `TestDataRow` over `IEnumerable`. The `object[]` approach provides no compile-time type checking and can lead to runtime errors from type mismatches. + +```csharp +[TestMethod] +[DynamicData(nameof(TestData))] +public void DynamicTest(int a, int b, int expected) +{ + Assert.AreEqual(expected, Calculator.Add(a, b)); +} + +// ValueTuple - preferred (MSTest 3.7+) +public static IEnumerable<(int a, int b, int expected)> TestData => +[ + (1, 2, 3), + (0, 0, 0), +]; + +// TestDataRow - when you need custom display names or metadata +public static IEnumerable> TestDataWithMetadata => +[ + new((1, 2, 3)) { DisplayName = "Positive numbers" }, + new((0, 0, 0)) { DisplayName = "Zeros" }, + new((-1, 1, 0)) { DisplayName = "Mixed signs", IgnoreMessage = "Known issue #123" }, +]; + +// IEnumerable - avoid for new code (no type safety) +public static IEnumerable LegacyTestData => +[ + [1, 2, 3], + [0, 0, 0], +]; +``` + +## TestContext + +The `TestContext` class provides test run information, cancellation support, and output methods. +See [TestContext documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-writing-tests-testcontext) for complete reference. + +### Accessing TestContext + +```csharp +// Property (MSTest suppresses CS8618 - don't use nullable or = null!) +public TestContext TestContext { get; set; } + +// Constructor injection (MSTest 3.6+) - preferred for immutability +[TestClass] +public sealed class MyTests +{ + private readonly TestContext _testContext; + + public MyTests(TestContext testContext) + { + _testContext = testContext; + } +} + +// Static methods receive it as parameter +[ClassInitialize] +public static void ClassInit(TestContext context) { } + +// Optional for cleanup methods (MSTest 3.6+) +[ClassCleanup] +public static void ClassCleanup(TestContext context) { } + +[AssemblyCleanup] +public static void AssemblyCleanup(TestContext context) { } +``` + +### Cancellation Token + +Always use `TestContext.CancellationToken` for cooperative cancellation with `[Timeout]`: + +```csharp +[TestMethod] +[Timeout(5000)] +public async Task LongRunningTest() +{ + await _httpClient.GetAsync(url, TestContext.CancellationToken); +} +``` + +### Test Run Properties + +```csharp +TestContext.TestName // Current test method name +TestContext.TestDisplayName // Display name (3.7+) +TestContext.CurrentTestOutcome // Pass/Fail/InProgress +TestContext.TestData // Parameterized test data (3.7+, in TestInitialize/Cleanup) +TestContext.TestException // Exception if test failed (3.7+, in TestCleanup) +TestContext.DeploymentDirectory // Directory with deployment items +``` + +### Output and Result Files + +```csharp +// Write to test output (useful for debugging) +TestContext.WriteLine("Processing item {0}", itemId); + +// Attach files to test results (logs, screenshots) +TestContext.AddResultFile(screenshotPath); + +// Store/retrieve data across test methods +TestContext.Properties["SharedKey"] = computedValue; +``` + +## Advanced Features + +### Retry for Flaky Tests (MSTest 3.9+) + +```csharp +[TestMethod] +[Retry(3)] +public void FlakyTest() { } +``` + +### Conditional Execution (MSTest 3.10+) + +Skip or run tests based on OS or CI environment: + +```csharp +// OS-specific tests +[TestMethod] +[OSCondition(OperatingSystems.Windows)] +public void WindowsOnlyTest() { } + +[TestMethod] +[OSCondition(OperatingSystems.Linux | OperatingSystems.MacOS)] +public void UnixOnlyTest() { } + +[TestMethod] +[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] +public void SkipOnWindowsTest() { } + +// CI environment tests +[TestMethod] +[CICondition] // Runs only in CI (default: ConditionMode.Include) +public void CIOnlyTest() { } + +[TestMethod] +[CICondition(ConditionMode.Exclude)] // Skips in CI, runs locally +public void LocalOnlyTest() { } +``` + +### Parallelization + +```csharp +// Assembly level +[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)] + +// Disable for specific class +[TestClass] +[DoNotParallelize] +public sealed class SequentialTests { } +``` + +### Work Item Traceability (MSTest 3.8+) + +Link tests to work items for traceability in test reports: + +```csharp +// Azure DevOps work items +[TestMethod] +[WorkItem(12345)] // Links to work item #12345 +public void Feature_Scenario_ExpectedBehavior() { } + +// Multiple work items +[TestMethod] +[WorkItem(12345)] +[WorkItem(67890)] +public void Feature_CoversMultipleRequirements() { } + +// GitHub issues (MSTest 3.8+) +[TestMethod] +[GitHubWorkItem("https://github.com/owner/repo/issues/42")] +public void BugFix_Issue42_IsResolved() { } +``` + +Work item associations appear in test results and can be used for: +- Tracing test coverage to requirements +- Linking bug fixes to regression tests +- Generating traceability reports in CI/CD pipelines + +## Common Mistakes to Avoid + +```csharp +// ❌ Wrong argument order +Assert.AreEqual(actual, expected); +// ✅ Correct +Assert.AreEqual(expected, actual); + +// ❌ Using ExpectedException (obsolete) +[ExpectedException(typeof(ArgumentException))] +// ✅ Use Assert.Throws +Assert.Throws(() => Method()); + +// ❌ Using LINQ Single() - unclear exception +var item = items.Single(); +// ✅ Use ContainsSingle - better failure message +var item = Assert.ContainsSingle(items); + +// ❌ Hard cast - unclear exception +var handler = (MyHandler)result; +// ✅ Type assertion - shows actual type on failure +var handler = Assert.IsInstanceOfType(result); + +// ❌ Ignoring cancellation token +await client.GetAsync(url, CancellationToken.None); +// ✅ Flow test cancellation +await client.GetAsync(url, TestContext.CancellationToken); + +// ❌ Making TestContext nullable - leads to unnecessary null checks +public TestContext? TestContext { get; set; } +// ❌ Using null! - MSTest already suppresses CS8618 for this property +public TestContext TestContext { get; set; } = null!; +// ✅ Declare without nullable or initializer - MSTest handles the warning +public TestContext TestContext { get; set; } +``` + +## Test Organization + +- Group tests by feature or component +- Use `[TestCategory("Category")]` for filtering +- Use `[TestProperty("Name", "Value")]` for custom metadata (e.g., `[TestProperty("Bug", "12345")]`) +- Use `[Priority(1)]` for critical tests +- Enable relevant MSTest analyzers (MSTEST0020 for constructor preference) + +## Mocking and Isolation + +- Use Moq or NSubstitute for mocking dependencies +- Use interfaces to facilitate mocking +- Mock dependencies to isolate units under test diff --git a/.agents/skills/csharp-nunit/SKILL.md b/.agents/skills/csharp-nunit/SKILL.md new file mode 100644 index 0000000..7890775 --- /dev/null +++ b/.agents/skills/csharp-nunit/SKILL.md @@ -0,0 +1,71 @@ +--- +name: csharp-nunit +description: 'Get best practices for NUnit unit testing, including data-driven tests' +--- + +# NUnit Best Practices + +Your goal is to help me write effective unit tests with NUnit, covering both standard and data-driven testing approaches. + +## Project Setup + +- Use a separate test project with naming convention `[ProjectName].Tests` +- Reference Microsoft.NET.Test.Sdk, NUnit, and NUnit3TestAdapter packages +- Create test classes that match the classes being tested (e.g., `CalculatorTests` for `Calculator`) +- Use .NET SDK test commands: `dotnet test` for running tests + +## Test Structure + +- Apply `[TestFixture]` attribute to test classes +- Use `[Test]` attribute for test methods +- Follow the Arrange-Act-Assert (AAA) pattern +- Name tests using the pattern `MethodName_Scenario_ExpectedBehavior` +- Use `[SetUp]` and `[TearDown]` for per-test setup and teardown +- Use `[OneTimeSetUp]` and `[OneTimeTearDown]` for per-class setup and teardown +- Use `[SetUpFixture]` for assembly-level setup and teardown + +## Standard Tests + +- Keep tests focused on a single behavior +- Avoid testing multiple behaviors in one test method +- Use clear assertions that express intent +- Include only the assertions needed to verify the test case +- Make tests independent and idempotent (can run in any order) +- Avoid test interdependencies + +## Data-Driven Tests + +- Use `[TestCase]` for inline test data +- Use `[TestCaseSource]` for programmatically generated test data +- Use `[Values]` for simple parameter combinations +- Use `[ValueSource]` for property or method-based data sources +- Use `[Random]` for random numeric test values +- Use `[Range]` for sequential numeric test values +- Use `[Combinatorial]` or `[Pairwise]` for combining multiple parameters + +## Assertions + +- Use `Assert.That` with constraint model (preferred NUnit style) +- Use constraints like `Is.EqualTo`, `Is.SameAs`, `Contains.Item` +- Use `Assert.AreEqual` for simple value equality (classic style) +- Use `CollectionAssert` for collection comparisons +- Use `StringAssert` for string-specific assertions +- Use `Assert.Throws` or `Assert.ThrowsAsync` to test exceptions +- Use descriptive messages in assertions for clarity on failure + +## Mocking and Isolation + +- Consider using Moq or NSubstitute alongside NUnit +- Mock dependencies to isolate units under test +- Use interfaces to facilitate mocking +- Consider using a DI container for complex test setups + +## Test Organization + +- Group tests by feature or component +- Use categories with `[Category("CategoryName")]` +- Use `[Order]` to control test execution order when necessary +- Use `[Author("DeveloperName")]` to indicate ownership +- Use `[Description]` to provide additional test information +- Consider `[Explicit]` for tests that shouldn't run automatically +- Use `[Ignore("Reason")]` to temporarily skip tests diff --git a/.agents/skills/csharp-tunit/SKILL.md b/.agents/skills/csharp-tunit/SKILL.md new file mode 100644 index 0000000..c972ebe --- /dev/null +++ b/.agents/skills/csharp-tunit/SKILL.md @@ -0,0 +1,100 @@ +--- +name: csharp-tunit +description: 'Get best practices for TUnit unit testing, including data-driven tests' +--- + +# TUnit Best Practices + +Your goal is to help me write effective unit tests with TUnit, covering both standard and data-driven testing approaches. + +## Project Setup + +- Use a separate test project with naming convention `[ProjectName].Tests` +- Reference TUnit package and TUnit.Assertions for fluent assertions +- Create test classes that match the classes being tested (e.g., `CalculatorTests` for `Calculator`) +- Use .NET SDK test commands: `dotnet test` for running tests +- TUnit requires .NET 8.0 or higher + +## Test Structure + +- No test class attributes required (like xUnit/NUnit) +- Use `[Test]` attribute for test methods (not `[Fact]` like xUnit) +- Follow the Arrange-Act-Assert (AAA) pattern +- Name tests using the pattern `MethodName_Scenario_ExpectedBehavior` +- Use lifecycle hooks: `[Before(Test)]` for setup and `[After(Test)]` for teardown +- Use `[Before(Class)]` and `[After(Class)]` for shared context between tests in a class +- Use `[Before(Assembly)]` and `[After(Assembly)]` for shared context across test classes +- TUnit supports advanced lifecycle hooks like `[Before(TestSession)]` and `[After(TestSession)]` + +## Standard Tests + +- Keep tests focused on a single behavior +- Avoid testing multiple behaviors in one test method +- Use TUnit's fluent assertion syntax with `await Assert.That()` +- Include only the assertions needed to verify the test case +- Make tests independent and idempotent (can run in any order) +- Avoid test interdependencies (use `[DependsOn]` attribute if needed) + +## Data-Driven Tests + +- Use `[Arguments]` attribute for inline test data (equivalent to xUnit's `[InlineData]`) +- Use `[MethodData]` for method-based test data (equivalent to xUnit's `[MemberData]`) +- Use `[ClassData]` for class-based test data +- Create custom data sources by implementing `ITestDataSource` +- Use meaningful parameter names in data-driven tests +- Multiple `[Arguments]` attributes can be applied to the same test method + +## Assertions + +- Use `await Assert.That(value).IsEqualTo(expected)` for value equality +- Use `await Assert.That(value).IsSameReferenceAs(expected)` for reference equality +- Use `await Assert.That(value).IsTrue()` or `await Assert.That(value).IsFalse()` for boolean conditions +- Use `await Assert.That(collection).Contains(item)` or `await Assert.That(collection).DoesNotContain(item)` for collections +- Use `await Assert.That(value).Matches(pattern)` for regex pattern matching +- Use `await Assert.That(action).Throws()` or `await Assert.That(asyncAction).ThrowsAsync()` to test exceptions +- Chain assertions with `.And` operator: `await Assert.That(value).IsNotNull().And.IsEqualTo(expected)` +- Use `.Or` operator for alternative conditions: `await Assert.That(value).IsEqualTo(1).Or.IsEqualTo(2)` +- Use `.Within(tolerance)` for DateTime and numeric comparisons with tolerance +- All assertions are asynchronous and must be awaited + +## Advanced Features + +- Use `[Repeat(n)]` to repeat tests multiple times +- Use `[Retry(n)]` for automatic retry on failure +- Use `[ParallelLimit]` to control parallel execution limits +- Use `[Skip("reason")]` to skip tests conditionally +- Use `[DependsOn(nameof(OtherTest))]` to create test dependencies +- Use `[Timeout(milliseconds)]` to set test timeouts +- Create custom attributes by extending TUnit's base attributes + +## Test Organization + +- Group tests by feature or component +- Use `[Category("CategoryName")]` for test categorization +- Use `[DisplayName("Custom Test Name")]` for custom test names +- Consider using `TestContext` for test diagnostics and information +- Use conditional attributes like custom `[WindowsOnly]` for platform-specific tests + +## Performance and Parallel Execution + +- TUnit runs tests in parallel by default (unlike xUnit which requires explicit configuration) +- Use `[NotInParallel]` to disable parallel execution for specific tests +- Use `[ParallelLimit]` with custom limit classes to control concurrency +- Tests within the same class run sequentially by default +- Use `[Repeat(n)]` with `[ParallelLimit]` for load testing scenarios + +## Migration from xUnit + +- Replace `[Fact]` with `[Test]` +- Replace `[Theory]` with `[Test]` and use `[Arguments]` for data +- Replace `[InlineData]` with `[Arguments]` +- Replace `[MemberData]` with `[MethodData]` +- Replace `Assert.Equal` with `await Assert.That(actual).IsEqualTo(expected)` +- Replace `Assert.True` with `await Assert.That(condition).IsTrue()` +- Replace `Assert.Throws` with `await Assert.That(action).Throws()` +- Replace constructor/IDisposable with `[Before(Test)]`/`[After(Test)]` +- Replace `IClassFixture` with `[Before(Class)]`/`[After(Class)]` + +**Why TUnit over xUnit?** + +TUnit offers a modern, fast, and flexible testing experience with advanced features not present in xUnit, such as asynchronous assertions, more refined lifecycle hooks, and improved data-driven testing capabilities. TUnit's fluent assertions provide clearer and more expressive test validation, making it especially suitable for complex .NET projects. diff --git a/.agents/skills/csharp-xunit/SKILL.md b/.agents/skills/csharp-xunit/SKILL.md new file mode 100644 index 0000000..4347c5a --- /dev/null +++ b/.agents/skills/csharp-xunit/SKILL.md @@ -0,0 +1,68 @@ +--- +name: csharp-xunit +description: 'Get best practices for XUnit unit testing, including data-driven tests' +--- + +# XUnit Best Practices + +Your goal is to help me write effective unit tests with XUnit, covering both standard and data-driven testing approaches. + +## Project Setup + +- Use a separate test project with naming convention `[ProjectName].Tests` +- Reference Microsoft.NET.Test.Sdk, xunit, and xunit.runner.visualstudio packages +- Create test classes that match the classes being tested (e.g., `CalculatorTests` for `Calculator`) +- Use .NET SDK test commands: `dotnet test` for running tests + +## Test Structure + +- No test class attributes required (unlike MSTest/NUnit) +- Use fact-based tests with `[Fact]` attribute for simple tests +- Follow the Arrange-Act-Assert (AAA) pattern +- Name tests using the pattern `MethodName_Scenario_ExpectedBehavior` +- Use constructor for setup and `IDisposable.Dispose()` for teardown +- Use `IClassFixture` for shared context between tests in a class +- Use `ICollectionFixture` for shared context between multiple test classes + +## Standard Tests + +- Keep tests focused on a single behavior +- Avoid testing multiple behaviors in one test method +- Use clear assertions that express intent +- Include only the assertions needed to verify the test case +- Make tests independent and idempotent (can run in any order) +- Avoid test interdependencies + +## Data-Driven Tests + +- Use `[Theory]` combined with data source attributes +- Use `[InlineData]` for inline test data +- Use `[MemberData]` for method-based test data +- Use `[ClassData]` for class-based test data +- Create custom data attributes by implementing `DataAttribute` +- Use meaningful parameter names in data-driven tests + +## Assertions + +- Use `Assert.Equal` for value equality +- Use `Assert.Same` for reference equality +- Use `Assert.True`/`Assert.False` for boolean conditions +- Use `Assert.Contains`/`Assert.DoesNotContain` for collections +- Use `Assert.Matches`/`Assert.DoesNotMatch` for regex pattern matching +- Use `Assert.Throws` or `await Assert.ThrowsAsync` to test exceptions +- Use fluent assertions library for more readable assertions + +## Mocking and Isolation + +- Consider using Moq or NSubstitute alongside XUnit +- Mock dependencies to isolate units under test +- Use interfaces to facilitate mocking +- Consider using a DI container for complex test setups + +## Test Organization + +- Group tests by feature or component +- Use `[Trait("Category", "CategoryName")]` for categorization +- Use collection fixtures to group tests with shared dependencies +- Consider output helpers (`ITestOutputHelper`) for test diagnostics +- Skip tests conditionally with `Skip = "reason"` in fact/theory attributes diff --git a/.agents/skills/dotnet-best-practices/SKILL.md b/.agents/skills/dotnet-best-practices/SKILL.md new file mode 100644 index 0000000..183d3be --- /dev/null +++ b/.agents/skills/dotnet-best-practices/SKILL.md @@ -0,0 +1,85 @@ +--- +name: dotnet-best-practices +description: 'Ensure .NET/C# code meets best practices for the solution/project.' +--- + +# .NET/C# Best Practices + +Your task is to ensure .NET/C# code in ${selection} meets the best practices specific to this solution/project. This includes: + +## Documentation & Structure + +- Create comprehensive XML documentation comments for all public classes, interfaces, methods, and properties +- Include parameter descriptions and return value descriptions in XML comments +- Follow the established namespace structure: {Core|Console|App|Service}.{Feature} + +## Design Patterns & Architecture + +- Use primary constructor syntax for dependency injection (e.g., `public class MyClass(IDependency dependency)`) +- Implement the Command Handler pattern with generic base classes (e.g., `CommandHandler`) +- Use interface segregation with clear naming conventions (prefix interfaces with 'I') +- Follow the Factory pattern for complex object creation. + +## Dependency Injection & Services + +- Use constructor dependency injection with null checks via ArgumentNullException +- Register services with appropriate lifetimes (Singleton, Scoped, Transient) +- Use Microsoft.Extensions.DependencyInjection patterns +- Implement service interfaces for testability + +## Resource Management & Localization + +- Use ResourceManager for localized messages and error strings +- Separate LogMessages and ErrorMessages resource files +- Access resources via `_resourceManager.GetString("MessageKey")` + +## Async/Await Patterns + +- Use async/await for all I/O operations and long-running tasks +- Return Task or Task from async methods +- Use ConfigureAwait(false) where appropriate +- Handle async exceptions properly + +## Testing Standards + +- Use MSTest framework with FluentAssertions for assertions +- Follow AAA pattern (Arrange, Act, Assert) +- Use Moq for mocking dependencies +- Test both success and failure scenarios +- Include null parameter validation tests + +## Configuration & Settings + +- Use strongly-typed configuration classes with data annotations +- Implement validation attributes (Required, NotEmptyOrWhitespace) +- Use IConfiguration binding for settings +- Support appsettings.json configuration files + +## Semantic Kernel & AI Integration + +- Use Microsoft.SemanticKernel for AI operations +- Implement proper kernel configuration and service registration +- Handle AI model settings (ChatCompletion, Embedding, etc.) +- Use structured output patterns for reliable AI responses + +## Error Handling & Logging + +- Use structured logging with Microsoft.Extensions.Logging +- Include scoped logging with meaningful context +- Throw specific exceptions with descriptive messages +- Use try-catch blocks for expected failure scenarios + +## Performance & Security + +- Use C# 12+ features and .NET 8 optimizations where applicable +- Implement proper input validation and sanitization +- Use parameterized queries for database operations +- Follow secure coding practices for AI/ML operations + +## Code Quality + +- Ensure SOLID principles compliance +- Avoid code duplication through base classes and utilities +- Use meaningful names that reflect domain concepts +- Keep methods focused and cohesive +- Implement proper disposal patterns for resources diff --git a/.agents/skills/dotnet-design-pattern-review/SKILL.md b/.agents/skills/dotnet-design-pattern-review/SKILL.md new file mode 100644 index 0000000..5d9ded0 --- /dev/null +++ b/.agents/skills/dotnet-design-pattern-review/SKILL.md @@ -0,0 +1,42 @@ +--- +name: dotnet-design-pattern-review +description: 'Review the C#/.NET code for design pattern implementation and suggest improvements.' +--- + +# .NET/C# Design Pattern Review + +Review the C#/.NET code in ${selection} for design pattern implementation and suggest improvements for the solution/project. Do not make any changes to the code, just provide a review. + +## Required Design Patterns + +- **Command Pattern**: Generic base classes (`CommandHandler`), `ICommandHandler` interface, `CommandHandlerOptions` inheritance, static `SetupCommand(IHost host)` methods +- **Factory Pattern**: Complex object creation service provider integration +- **Dependency Injection**: Primary constructor syntax, `ArgumentNullException` null checks, interface abstractions, proper service lifetimes +- **Repository Pattern**: Async data access interfaces provider abstractions for connections +- **Provider Pattern**: External service abstractions (database, AI), clear contracts, configuration handling +- **Resource Pattern**: ResourceManager for localized messages, separate .resx files (LogMessages, ErrorMessages) + +## Review Checklist + +- **Design Patterns**: Identify patterns used. Are Command Handler, Factory, Provider, and Repository patterns correctly implemented? Missing beneficial patterns? +- **Architecture**: Follow namespace conventions (`{Core|Console|App|Service}.{Feature}`)? Proper separation between Core/Console projects? Modular and readable? +- **.NET Best Practices**: Primary constructors, async/await with Task returns, ResourceManager usage, structured logging, strongly-typed configuration? +- **GoF Patterns**: Command, Factory, Template Method, Strategy patterns correctly implemented? +- **SOLID Principles**: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion violations? +- **Performance**: Proper async/await, resource disposal, ConfigureAwait(false), parallel processing opportunities? +- **Maintainability**: Clear separation of concerns, consistent error handling, proper configuration usage? +- **Testability**: Dependencies abstracted via interfaces, mockable components, async testability, AAA pattern compatibility? +- **Security**: Input validation, secure credential handling, parameterized queries, safe exception handling? +- **Documentation**: XML docs for public APIs, parameter/return descriptions, resource file organization? +- **Code Clarity**: Meaningful names reflecting domain concepts, clear intent through patterns, self-explanatory structure? +- **Clean Code**: Consistent style, appropriate method/class size, minimal complexity, eliminated duplication? + +## Improvement Focus Areas + +- **Command Handlers**: Validation in base class, consistent error handling, proper resource management +- **Factories**: Dependency configuration, service provider integration, disposal patterns +- **Providers**: Connection management, async patterns, exception handling and logging +- **Configuration**: Data annotations, validation attributes, secure sensitive value handling +- **AI/ML Integration**: Semantic Kernel patterns, structured output handling, model configuration + +Provide specific, actionable recommendations for improvements aligned with the project's architecture and .NET best practices. diff --git a/.agents/skills/dotnet-upgrade/SKILL.md b/.agents/skills/dotnet-upgrade/SKILL.md new file mode 100644 index 0000000..93ca760 --- /dev/null +++ b/.agents/skills/dotnet-upgrade/SKILL.md @@ -0,0 +1,116 @@ +--- +name: dotnet-upgrade +description: 'Ready-to-use prompts for comprehensive .NET framework upgrade analysis and execution' +--- + +# Project Discovery & Assessment + - name: "Project Classification Analysis" + prompt: "Identify all projects in the solution and classify them by type (`.NET Framework`, `.NET Core`, `.NET Standard`). Analyze each `.csproj` for its current `TargetFramework` and SDK usage." + + - name: "Dependency Compatibility Review" + prompt: "Review external and internal dependencies for framework compatibility. Determine the upgrade complexity based on dependency graph depth." + + - name: "Legacy Package Detection" + prompt: "Identify legacy `packages.config` projects needing migration to `PackageReference` format." + + # Upgrade Strategy & Sequencing + - name: "Project Upgrade Ordering" + prompt: "Recommend a project upgrade order from least to most dependent components. Suggest how to isolate class library upgrades before API or Azure Function migrations." + + - name: "Incremental Strategy Planning" + prompt: "Propose an incremental upgrade strategy with rollback checkpoints. Evaluate the use of **Upgrade Assistant** or **manual upgrades** based on project structure." + + - name: "Progress Tracking Setup" + prompt: "Generate an upgrade checklist for tracking build, test, and deployment readiness across all projects." + + # Framework Targeting & Code Adjustments + - name: "Target Framework Selection" + prompt: "Suggest the correct `TargetFramework` for each project (e.g., `net8.0`). Review and update deprecated SDK or build configurations." + + - name: "Code Modernization Analysis" + prompt: "Identify code patterns needing modernization (e.g., `WebHostBuilder` → `HostBuilder`). Suggest replacements for deprecated .NET APIs and third-party libraries." + + - name: "Async Pattern Conversion" + prompt: "Recommend conversion of synchronous calls to async where appropriate for improved performance and scalability." + + # NuGet & Dependency Management + - name: "Package Compatibility Analysis" + prompt: "Analyze outdated or incompatible NuGet packages and suggest compatible versions. Identify third-party libraries that lack .NET 8 support and provide migration paths." + + - name: "Shared Dependency Strategy" + prompt: "Recommend strategies for handling shared dependency upgrades across projects. Evaluate usage of legacy packages and suggest alternatives in Microsoft-supported namespaces." + + - name: "Transitive Dependency Review" + prompt: "Review transitive dependencies and potential version conflicts after upgrade. Suggest resolution strategies for dependency conflicts." + + # CI/CD & Build Pipeline Updates + - name: "Pipeline Configuration Analysis" + prompt: "Analyze YAML build definitions for SDK version pinning and recommend updates. Suggest modifications for `UseDotNet@2` and `NuGetToolInstaller` tasks." + + - name: "Build Pipeline Modernization" + prompt: "Generate updated build pipeline snippets for .NET 8 migration. Recommend validation builds on feature branches before merging to main." + + - name: "CI Automation Enhancement" + prompt: "Identify opportunities to automate test and build verification in CI pipelines. Suggest strategies for continuous integration validation." + + # Testing & Validation + - name: "Build Validation Strategy" + prompt: "Propose validation checks to ensure the upgraded solution builds and runs successfully. Recommend automated test execution for unit and integration suites post-upgrade." + + - name: "Service Integration Verification" + prompt: "Generate validation steps to verify logging, telemetry, and service connectivity. Suggest strategies for verifying backward compatibility and runtime behavior." + + - name: "Deployment Readiness Check" + prompt: "Recommend UAT deployment verification steps before production rollout. Create comprehensive testing scenarios for upgraded components." + + # Breaking Change Analysis + - name: "API Deprecation Detection" + prompt: "Identify deprecated APIs or removed namespaces between target versions. Suggest automated scanning using `.NET Upgrade Assistant` and API Analyzer." + + - name: "API Replacement Strategy" + prompt: "Recommend replacement APIs or libraries for known breaking areas. Review configuration changes such as `Startup.cs` → `Program.cs` refactoring." + + - name: "Regression Testing Focus" + prompt: "Suggest regression testing scenarios focused on upgraded API endpoints or services. Create test plans for critical functionality validation." + + # Version Control & Commit Strategy + - name: "Branching Strategy Planning" + prompt: "Recommend branching strategy for safe upgrade with rollback capability. Generate commit templates for partial and complete project upgrades." + + - name: "PR Structure Optimization" + prompt: "Suggest best practices for creating structured PRs (`Upgrade to .NET [Version]`). Identify tagging strategies for PRs involving breaking changes." + + - name: "Code Review Guidelines" + prompt: "Recommend peer review focus areas (build, test, and dependency validation). Create checklists for effective upgrade reviews." + + # Documentation & Communication + - name: "Upgrade Documentation Strategy" + prompt: "Suggest how to document each project's framework change in the PR. Propose automated release note generation summarizing upgrades and test results." + + - name: "Stakeholder Communication" + prompt: "Recommend communicating version upgrades and migration timelines to consumers. Generate documentation templates for dependency updates and validation results." + + - name: "Progress Tracking Systems" + prompt: "Suggest maintaining an upgrade summary dashboard or markdown checklist. Create templates for tracking upgrade progress across multiple projects." + + # Tools & Automation + - name: "Upgrade Tool Selection" + prompt: "Recommend when and how to use: `.NET Upgrade Assistant`, `dotnet list package --outdated`, `dotnet migrate`, and `graph.json` dependency visualization." + + - name: "Analysis Script Generation" + prompt: "Generate scripts or prompts for analyzing dependency graphs before upgrading. Propose AI-assisted prompts for Copilot to identify upgrade issues automatically." + + - name: "Multi-Repository Validation" + prompt: "Suggest how to validate automation output across multiple repositories. Create standardized validation workflows for enterprise-scale upgrades." + + # Final Validation & Delivery + - name: "Final Solution Validation" + prompt: "Generate validation steps to confirm the final upgraded solution passes all validation checks. Suggest production deployment verification steps post-upgrade." + + - name: "Deployment Readiness Confirmation" + prompt: "Recommend generating final test results and build artifacts. Create a checklist summarizing completion across projects (builds/tests/deployment)." + + - name: "Release Documentation" + prompt: "Generate a release note summarizing framework changes and CI/CD updates. Create comprehensive upgrade summary documentation." + +--- diff --git a/Application/Abstractions/Infrastructure/IEventBus.cs b/Application/Abstractions/Infrastructure/IEventBus.cs index 578a1b2..18925d0 100644 --- a/Application/Abstractions/Infrastructure/IEventBus.cs +++ b/Application/Abstractions/Infrastructure/IEventBus.cs @@ -2,6 +2,6 @@ { public interface IEventBus { - Task PublishAsync(T message, CancellationToken cancellationToken); + Task PublishAsync(T message, CancellationToken cancellationToken) where T : class; } } \ No newline at end of file diff --git a/Application/Abstractions/Infrastructure/IStorageService.cs b/Application/Abstractions/Infrastructure/IStorageService.cs index a40b237..26af204 100644 --- a/Application/Abstractions/Infrastructure/IStorageService.cs +++ b/Application/Abstractions/Infrastructure/IStorageService.cs @@ -6,6 +6,8 @@ Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); + Task GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, CancellationToken cancellationToken); + Task UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken); Task UploadFileAsync(Stream sourceStream, string contentType, string bucket, string key, CancellationToken cancellationToken); diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index dd1964d..5bd7a37 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -15,6 +15,9 @@ namespace Musify.Application.Configuration public class PlayListRoutes { + [Required] + public string UploadsFolder { get; set; } = "uploads"; + [Required] public string ParentFolders { get; set; } = "PlayLists"; @@ -42,27 +45,36 @@ namespace Musify.Application.Configuration [Required] public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; - public string SmallPicturesPath => Path.Combine(ParentFolders, SmallPicturesFolder); + public string SmallPicturesPath => CombineKey(ParentFolders, SmallPicturesFolder); - public string MediumPicturesPath => Path.Combine(ParentFolders, MediumPicturesFolder); + public string MediumPicturesPath => CombineKey(ParentFolders, MediumPicturesFolder); - public string LargePicturesPath => Path.Combine(ParentFolders, LargePicturesFolder); + public string LargePicturesPath => CombineKey(ParentFolders, LargePicturesFolder); - public string PresetSmallPicturePath => Path.Combine(ParentFolders, PresetSmallPicture); + public string PresetSmallPicturePath => CombineKey(ParentFolders, PresetSmallPicture); - public string PresetMediumPicturePath => Path.Combine(ParentFolders, PresetMediumPicture); + public string PresetMediumPicturePath => CombineKey(ParentFolders, PresetMediumPicture); - public string PresetLargePicturePath => Path.Combine(ParentFolders, PresetLargePicture); + public string PresetLargePicturePath => CombineKey(ParentFolders, PresetLargePicture); - public string OriginalPicturesPath => Path.Combine(ParentFolders, OriginalPicturesFolder); + public string OriginalPicturesPath => CombineKey(ParentFolders, OriginalPicturesFolder); - public string BuildOriginalPicturePath(string pictureName) => Path.Combine(ParentFolders, OriginalPicturesFolder, pictureName); + public string BuildOriginalPicturePath(string pictureName) => CombineKey(ParentFolders, OriginalPicturesFolder, pictureName); - public string BuildSmallPicturePath(string pictureName) => Path.Combine(ParentFolders, SmallPicturesFolder, pictureName); + public string BuildOriginalPicturePath(Guid userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName); - public string BuildMediumPicturePath(string pictureName) => Path.Combine(ParentFolders, MediumPicturesFolder, pictureName); + public string BuildSmallPicturePath(string pictureName) => CombineKey(ParentFolders, SmallPicturesFolder, pictureName); - public string BuildLargePicturePath(string pictureName) => Path.Combine(ParentFolders, LargePicturesFolder, pictureName); + public string BuildMediumPicturePath(string pictureName) => CombineKey(ParentFolders, MediumPicturesFolder, pictureName); + + public string BuildLargePicturePath(string pictureName) => CombineKey(ParentFolders, LargePicturesFolder, pictureName); + + static string CombineKey(params string[] segments) + { + return string.Join('/', segments + .Where(static s => !string.IsNullOrWhiteSpace(s)) + .Select(static s => s.Trim().Trim('/', '\\'))); + } } public class PlayListPicturesSizes diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index c9658ef..f7b5e76 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -15,6 +15,9 @@ namespace Musify.Application.Configuration public class TrackRoutes { + [Required] + public string UploadsFolder { get; set; } = "uploads"; + [Required] public string ParentFolder { get; set; } = "Tracks"; @@ -48,35 +51,46 @@ namespace Musify.Application.Configuration [Required] public string ProcessedAudioFolder { get; set; } = "ProcessedAudios"; - public string SmallPicturesPath => Path.Combine(ParentFolder, SmallPicturesFolder); + public string SmallPicturesPath => CombineKey(ParentFolder, SmallPicturesFolder); - public string MediumPicturesPath => Path.Combine(ParentFolder, MediumPicturesFolder); + public string MediumPicturesPath => CombineKey(ParentFolder, MediumPicturesFolder); - public string LargePicturesPath => Path.Combine(ParentFolder, LargePicturesFolder); + public string LargePicturesPath => CombineKey(ParentFolder, LargePicturesFolder); - public string PresetSmallPicturePath => Path.Combine(ParentFolder, PresetSmallPicture); + public string PresetSmallPicturePath => CombineKey(ParentFolder, PresetSmallPicture); - public string PresetMediumPicturePath => Path.Combine(ParentFolder, PresetMediumPicture); + public string PresetMediumPicturePath => CombineKey(ParentFolder, PresetMediumPicture); - public string PresetLargePicturePath => Path.Combine(ParentFolder, PresetLargePicture); + public string PresetLargePicturePath => CombineKey(ParentFolder, PresetLargePicture); - public string OriginalPicturesPath => Path.Combine(ParentFolder, OriginalPicturesFolder); + public string OriginalPicturesPath => CombineKey(ParentFolder, OriginalPicturesFolder); - public string OriginalAudiosPath => Path.Combine(ParentFolder, OriginalAudiosFolder); + public string OriginalAudiosPath => CombineKey(ParentFolder, OriginalAudiosFolder); - public string ProcessedAudiosPath => Path.Combine(ParentFolder, ProcessedAudioFolder); + public string ProcessedAudiosPath => CombineKey(ParentFolder, ProcessedAudioFolder); - public string BuildOriginalPicturePath(string pictureName) => Path.Combine(OriginalPicturesPath, pictureName); + public string BuildOriginalPicturePath(string pictureName) => CombineKey(OriginalPicturesPath, pictureName); - public string BuildSmallPicturePath(string pictureName) => Path.Combine(SmallPicturesPath, pictureName); + public string BuildOriginalPicturePath(Guid userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), OriginalPicturesPath, pictureName); - public string BuildMediumPicturePath(string pictureName) => Path.Combine(MediumPicturesPath, pictureName); + public string BuildSmallPicturePath(string pictureName) => CombineKey(SmallPicturesPath, pictureName); - public string BuildLargePicturePath(string pictureName) => Path.Combine(LargePicturesPath, pictureName); + public string BuildMediumPicturePath(string pictureName) => CombineKey(MediumPicturesPath, pictureName); - public string BuildOriginalAudioPath(string audioName) => Path.Combine(OriginalAudiosPath, audioName); + public string BuildLargePicturePath(string pictureName) => CombineKey(LargePicturesPath, pictureName); - public string BuildProcessedAudioPath(string audioName) => Path.Combine(ProcessedAudiosPath, audioName); + public string BuildOriginalAudioPath(string audioName) => CombineKey(OriginalAudiosPath, audioName); + + public string BuildOriginalAudioPath(Guid userId, string audioName) => CombineKey(UploadsFolder, userId.ToString(), OriginalAudiosPath, audioName); + + public string BuildProcessedAudioPath(string audioName) => CombineKey(ProcessedAudiosPath, audioName); + + static string CombineKey(params string[] segments) + { + return string.Join('/', segments + .Where(static s => !string.IsNullOrWhiteSpace(s)) + .Select(static s => s.Trim().Trim('/', '\\'))); + } } public class TrackPicturesSizes diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index 6da1706..29ee783 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -1,8 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; using Musify.Application.PlayLists.Responses; -using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands { @@ -10,22 +8,6 @@ namespace Musify.Application.PlayLists.Commands Guid UserId, string Name, string Description, - IFileData? Picture) - : ICommand> - { - public static PlayList ToEntity(CreatePlayListCommand command, string originalKey, string smallKey, string mediumKey, string largeKey) - { - return new PlayList - { - UserId = command.UserId, - Name = command.Name, - NormalizedName = command.Name.Trim().ToUpperInvariant(), - Description = command.Description, - OriginalPictureName = originalKey, - SmallPictureName = smallKey, - MediumPictureName = mediumKey, - LargePictureName = largeKey, - }; - } - } + string? OriginalPictureName) + : ICommand>; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs b/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs new file mode 100644 index 0000000..33e01f6 --- /dev/null +++ b/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs @@ -0,0 +1,12 @@ +using Ardalis.Result; +using Mediator; +using Musify.Application.PlayLists.Responses; + +namespace Musify.Application.PlayLists.Commands +{ + public record RequestPlayListPictureUploadCommand( + Guid UserId, + string FileType, + string ContentType) + : ICommand>; +} diff --git a/Application/PlayLists/Commands/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayListCommand.cs index 748b8a9..5671782 100644 --- a/Application/PlayLists/Commands/UpdatePlayListCommand.cs +++ b/Application/PlayLists/Commands/UpdatePlayListCommand.cs @@ -1,6 +1,5 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Commands @@ -10,6 +9,6 @@ namespace Musify.Application.PlayLists.Commands Guid PlayListId, string? NewName, string? NewDescription, - IFileData? NewPicture) + string? NewOriginalPictureName) : ICommand>; } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 76fa134..5fb98a0 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -3,7 +3,6 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Abstractions.Application; using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; @@ -15,7 +14,6 @@ namespace Musify.Application.PlayLists.Handlers public class CreatePlayListCommandHandler( IEventBus eventBus, IDatabase database, - IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) @@ -32,79 +30,43 @@ namespace Musify.Application.PlayLists.Handlers return Result.NotFound($"User {request.UserId} not found"); } - var playList = CreatePlayListCommand.ToEntity( - request, - playListConfiguration.Routes.PresetOriginalPicture, - playListConfiguration.Routes.PresetSmallPicture, - playListConfiguration.Routes.PresetMediumPicture, - playListConfiguration.Routes.PresetLargePicture); + var originalPictureName = request.OriginalPictureName ?? playListConfiguration.Routes.PresetOriginalPicture; - if (request.Picture is not null) + var playList = new PlayList { - var uploadResult = await UploadPictureAsync(playList, request.Picture, cancellationToken); - if (!uploadResult.IsSuccess) - return uploadResult; + UserId = request.UserId, + Name = request.Name, + NormalizedName = request.Name.Trim().ToUpperInvariant(), + Description = request.Description, + OriginalPictureName = originalPictureName, + SmallPictureName = playListConfiguration.Routes.PresetSmallPicture, + MediumPictureName = playListConfiguration.Routes.PresetMediumPicture, + LargePictureName = playListConfiguration.Routes.PresetLargePicture + }; + + await database.PlayLists.AddAsync(playList, cancellationToken); + + if (request.OriginalPictureName is not null) + { + var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken); + if (!publishResult.IsSuccess) + return publishResult; } try { - await database.PlayLists.AddAsync(playList, cancellationToken); await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { logger.LogError(exception, "Failed to save playlist for user {UserId}", request.UserId); - await RollbackAllPicturesAsync(playList, cancellationToken); return Result.Error($"Failed to create playlist for user {request.UserId}"); } - if (request.Picture is not null) - { - var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken); - if (!publishResult.IsSuccess) - { - await TryRollbackOnPublishFailureAsync(playList, cancellationToken); - return publishResult; - } - } - logger.LogInformation("Created playlist {PlayListId} for user {UserId}", playList.Id, request.UserId); return Result.Created(PlayListApplicationResponse.FromEntity(playList)); } - async Task TryRollbackOnPublishFailureAsync(PlayList playList, CancellationToken cancellationToken) - { - try - { - database.PlayLists.Remove(playList); - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback playlist creation after publish failure for playlist {PlayListId}", playList.Id); - } - - await RollbackAllPicturesAsync(playList, cancellationToken); - } - - async Task UploadPictureAsync(PlayList playList, IFileData picture, CancellationToken cancellationToken) - { - var pictureName = Guid.NewGuid() + picture.FileType; - var pictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(pictureName); - - try - { - await storageService.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.Bucket, pictureKey, cancellationToken); - playList.OriginalPictureName = pictureName; - return Result.Success(); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to upload picture for playlist {PlayListId}", playList.Id); - return Result.Error($"Failed to upload picture for playlist {playList.Id}"); - } - } - async Task PublishPictureUpdateEventAsync(PlayList playList, CancellationToken cancellationToken) { try @@ -112,7 +74,7 @@ namespace Musify.Application.PlayLists.Handlers var updateEvent = new UpdatePlayListPictureEvent( playList.Id, storageConfiguration.Bucket, - playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName), + playListConfiguration.Routes.BuildOriginalPicturePath(playList.UserId, playList.OriginalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, @@ -135,25 +97,5 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error($"Failed to publish picture update event for playlist {playList.Id}"); } } - - async Task RollbackAllPicturesAsync(PlayList playList, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(playList.OriginalPictureName)) - return; - - if (playList.OriginalPictureName == playListConfiguration.Routes.PresetOriginalPicture) - return; - - var originalPicturePath = playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName); - - try - { - await storageService.RemoveFileAsync(storageConfiguration.Bucket, originalPicturePath, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback picture upload for playlist {PlayListId}", playList.Id); - } - } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 76709cb..08d3774 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -33,23 +33,12 @@ namespace Musify.Application.PlayLists.Handlers var mediumPictureName = playList.MediumPictureName; var largePictureName = playList.LargePictureName; - var originalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName); + var originalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(playList.UserId, playList.OriginalPictureName); var smallPictureKey = playListConfiguration.Routes.BuildSmallPicturePath(playList.SmallPictureName); var mediumPictureKey = playListConfiguration.Routes.BuildMediumPicturePath(playList.MediumPictureName); var largePictureKey = playListConfiguration.Routes.BuildLargePicturePath(playList.LargePictureName); - try - { - database.PlayLists.Remove(playList); - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to delete playlist {PlayListId}", request.PlayListId); - return Result.Error($"Failed to delete playlist {request.PlayListId}"); - } - - await PublishRemoveFileEventsBestEffortAsync( + var publishResult = await PublishRemoveFileEventsAsync( playList.Id, originalPictureName, smallPictureName, @@ -62,10 +51,24 @@ namespace Musify.Application.PlayLists.Handlers playListConfiguration.Routes, cancellationToken); + if (!publishResult.IsSuccess) + return publishResult; + + try + { + database.PlayLists.Remove(playList); + await database.SaveChangesAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to delete playlist {PlayListId}", request.PlayListId); + return Result.Error($"Failed to delete playlist {request.PlayListId}"); + } + return Result.NoContent(); } - async Task PublishRemoveFileEventsBestEffortAsync( + async Task PublishRemoveFileEventsAsync( Guid playListId, string originalPictureName, string smallPictureName, @@ -79,20 +82,31 @@ namespace Musify.Application.PlayLists.Handlers CancellationToken cancellationToken) { if (originalPictureName != playListRoutes.PresetOriginalPicture) - await PublishRemoveFileEventIgnoringErrorsAsync(playListId, originalPictureKey, cancellationToken); + { + var result = await PublishRemoveFileEventAsync(playListId, originalPictureKey, cancellationToken); + if (!result.IsSuccess) + return result; + } if (smallPictureName != playListRoutes.PresetSmallPicture) - await PublishRemoveFileEventIgnoringErrorsAsync(playListId, smallPictureKey, cancellationToken); + { + var result = await PublishRemoveFileEventAsync(playListId, smallPictureKey, cancellationToken); + if (!result.IsSuccess) + return result; + } if (mediumPictureName != playListRoutes.PresetMediumPicture) - await PublishRemoveFileEventIgnoringErrorsAsync(playListId, mediumPictureKey, cancellationToken); + { + var result = await PublishRemoveFileEventAsync(playListId, mediumPictureKey, cancellationToken); + if (!result.IsSuccess) + return result; + } if (largePictureName != playListRoutes.PresetLargePicture) - await PublishRemoveFileEventIgnoringErrorsAsync(playListId, largePictureKey, cancellationToken); - } + { + var result = await PublishRemoveFileEventAsync(playListId, largePictureKey, cancellationToken); + if (!result.IsSuccess) + return result; + } - async Task PublishRemoveFileEventIgnoringErrorsAsync(Guid playListId, string key, CancellationToken cancellationToken) - { - var result = await PublishRemoveFileEventAsync(playListId, key, cancellationToken); - if (!result.IsSuccess) - logger.LogWarning("Remove file event publish failed for playlist {PlayListId} with key {Key}", playListId, key); + return Result.Success(); } async Task PublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs new file mode 100644 index 0000000..3f2c589 --- /dev/null +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -0,0 +1,88 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Responses; + +namespace Musify.Application.PlayLists.Handlers +{ + public class RequestPlayListPictureUploadCommandHandler( + IDatabase database, + IStorageService storageService, + ILogger logger, + ApplicationStorageConfiguration storageConfiguration, + PlayListConfiguration playListConfiguration) + : ICommandHandler> + { + static readonly string[] AllowedExtensions = [".webp", ".png", ".jpg", ".jpeg"]; + const int ExpiresInSeconds = 600; + + public async ValueTask> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) + { + var userExists = await database.Users + .AsNoTracking() + .AnyAsync(u => u.Id == request.UserId, cancellationToken); + if (!userExists) + { + logger.LogWarning("User {UserId} not found", request.UserId); + return Result.NotFound($"User {request.UserId} not found"); + } + + var extensionResult = ValidateExtension(request.FileType); + if (!extensionResult.IsSuccess) + return Result.Invalid(extensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); + + if (string.IsNullOrWhiteSpace(request.ContentType)) + return Result.Invalid(new ValidationError("ContentType is required")); + + var pictureName = Guid.NewGuid() + extensionResult.Value; + var key = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureName); + + try + { + var uploadUrl = await storageService.GetUploadUrlAsync( + storageConfiguration.Bucket, + key, + request.ContentType, + TimeSpan.FromSeconds(ExpiresInSeconds), + cancellationToken); + + return Result.Success(new PlayListPictureUploadResponse( + storageConfiguration.Bucket, + key, + pictureName, + request.ContentType, + ExpiresInSeconds, + uploadUrl)); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to generate playlist picture upload URL for user {UserId}", request.UserId); + return Result.Error("Failed to generate upload URL"); + } + } + + static Result ValidateExtension(string fileType) + { + if (string.IsNullOrWhiteSpace(fileType)) + return Result.Error("FileType is required"); + + var extension = fileType.Trim(); + if (!extension.StartsWith('.')) + extension = "." + extension; + + extension = extension.ToLowerInvariant(); + + if (extension.Contains('/') || extension.Contains('\\')) + return Result.Error("Invalid FileType"); + + if (!AllowedExtensions.Contains(extension)) + return Result.Error($"Unsupported FileType '{extension}'"); + + return Result.Success(extension); + } + } +} diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 0ec4b4b..05a4501 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -3,23 +3,21 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Abstractions.Application; using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; -using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { - public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler, - ILogger logger, ApplicationStorageConfiguration storageConfiguration, + public class UpdatePlayListCommandHandler( + IEventBus eventBus, + IDatabase database, + ILogger logger, + ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : ICommandHandler> { - record UpdatePictureOutcome(Result Result, bool PictureUpdated, string? UploadedPictureKey); - record PreviousPlayListState(string Name, string NormalizedName, string Description, string OriginalPictureName); - public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); @@ -35,14 +33,25 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } - var previousState = CapturePreviousState(playListEntity); + if (request.NewName is not null) + { + playListEntity.Name = request.NewName; + playListEntity.NormalizedName = request.NewName.Trim().ToUpperInvariant(); + } - UpdateName(playListEntity, request.NewName); - UpdateDescription(playListEntity, request.NewDescription); + if (request.NewDescription is not null) + { + playListEntity.Description = request.NewDescription; + } - var pictureOutcome = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); - if (!pictureOutcome.Result.IsSuccess) - return pictureOutcome.Result; + if (request.NewOriginalPictureName is not null) + { + playListEntity.OriginalPictureName = request.NewOriginalPictureName; + + var publishResult = await PublishPlayListPictureEvent(playListEntity.UserId, playListEntity.Id, playListEntity.OriginalPictureName, cancellationToken); + if (!publishResult.IsSuccess) + return publishResult; + } try { @@ -51,86 +60,22 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - if (pictureOutcome.PictureUpdated && pictureOutcome.UploadedPictureKey is not null) - await Rollback(pictureOutcome.UploadedPictureKey, cancellationToken); logger.LogError(exception, "Failed to update playlist {PlayListId}", playListEntity.Id); return Result.Error($"Failed to update playlist {playListEntity.Id}"); } - if (pictureOutcome.PictureUpdated) - { - var publishResult = await PublishPlayListPictureEvent(playListEntity, cancellationToken); - if (!publishResult.IsSuccess) - { - await TryRollbackAfterPublishFailureAsync( - playListEntity, - previousState, - pictureOutcome.UploadedPictureKey, - cancellationToken); - return Result.Error($"Failed to update playlist {playListEntity.Id}"); - } - } - logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id); return Result.Success(PlayListApplicationResponse.FromEntity(playListEntity)); } - void UpdateName(PlayList playList, string? newName) - { - if (newName is null) - return; - - playList.Name = newName; - playList.NormalizedName = newName.Trim().ToUpperInvariant(); - } - - void UpdateDescription(PlayList playList, string? newDescription) - { - if (newDescription is null) - return; - - playList.Description = newDescription; - } - - static PreviousPlayListState CapturePreviousState(PlayList playList) - => new(playList.Name, playList.NormalizedName, playList.Description, playList.OriginalPictureName); - - async Task UpdatePicture( - PlayList playList, - IFileData? newPicture, - CancellationToken cancellationToken) - { - if (newPicture is null) - return new UpdatePictureOutcome(Result.Success(), false, null); - - var pictureName = Guid.NewGuid() + newPicture.FileType; - var pictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(pictureName); - try - { - await storageHandler.UploadFileAsync( - newPicture.FileStream, - newPicture.ContentType, - storageConfiguration.Bucket, - pictureKey, - cancellationToken); - } - catch - { - return new UpdatePictureOutcome(Result.Error("Failed to upload picture to storage"), false, null); - } - - playList.OriginalPictureName = pictureName; - return new UpdatePictureOutcome(Result.Success(), true, pictureKey); - } - - async Task PublishPlayListPictureEvent(PlayList playList, CancellationToken cancellationToken) + async Task PublishPlayListPictureEvent(Guid userId, Guid playListId, string originalPictureName, CancellationToken cancellationToken) { try { await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, + playListId, storageConfiguration.Bucket, - playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName), + playListConfiguration.Routes.BuildOriginalPicturePath(userId, originalPictureName), new ImageSize( playListConfiguration.Routes.SmallPicturesPath, playListConfiguration.PicturesSizes.SmallPictureWidth, @@ -148,50 +93,8 @@ namespace Musify.Application.PlayLists.Handlers } catch (Exception exception) { - logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playList.Id); - return Result.Error($"Failed to publish playlist {playList.Id} picture update event"); - } - } - - async Task Rollback(string pictureKey, CancellationToken cancellationToken) - { - await storageHandler.RemoveFileAsync( - storageConfiguration.Bucket, - pictureKey, - cancellationToken); - logger.LogInformation("Rolled back uploaded picture {PictureKey} from storage", pictureKey); - } - - async Task TryRollbackAfterPublishFailureAsync( - PlayList playList, - PreviousPlayListState previousState, - string? uploadedPictureKey, - CancellationToken cancellationToken) - { - playList.Name = previousState.Name; - playList.NormalizedName = previousState.NormalizedName; - playList.Description = previousState.Description; - playList.OriginalPictureName = previousState.OriginalPictureName; - - try - { - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback playlist state after publish failure for playlist {PlayListId}", playList.Id); - } - - if (uploadedPictureKey is null) - return; - - try - { - await Rollback(uploadedPictureKey, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback uploaded picture after publish failure for playlist {PlayListId}", playList.Id); + logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playListId); + return Result.Error($"Failed to publish playlist {playListId} picture update event"); } } } diff --git a/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs b/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs new file mode 100644 index 0000000..1a8ba92 --- /dev/null +++ b/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs @@ -0,0 +1,10 @@ +namespace Musify.Application.PlayLists.Responses +{ + public record PlayListPictureUploadResponse( + string Bucket, + string Key, + string PictureName, + string ContentType, + int ExpiresInSeconds, + string UploadUrl); +} diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index f99e324..652015a 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -1,30 +1,13 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; using Musify.Application.Tracks.Responses; -using Musify.Domain.Entities; namespace Musify.Application.Tracks.Commands { public record CreateTrackCommand( Guid UserId, string Title, - IFileData Picture, - IFileData Audio) - : ICommand > - { - public static Track ToEntity(CreateTrackCommand command, string smallPictureName, string mediumPictureName, string largePictureName) - { - return new Track - { - Title = command.Title, - NormalizedTitle = command.Title.ToUpperInvariant(), - OriginalPictureName = Guid.NewGuid() + command.Picture.FileType, - OriginalAudioName = Guid.NewGuid() + command.Audio.FileType, - SmallPictureName = smallPictureName, - MediumPictureName = mediumPictureName, - LargePictureName = largePictureName - }; - } - } + string OriginalPictureName, + string OriginalAudioName) + : ICommand>; } \ No newline at end of file diff --git a/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs b/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs new file mode 100644 index 0000000..022a0e1 --- /dev/null +++ b/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs @@ -0,0 +1,14 @@ +using Ardalis.Result; +using Mediator; +using Musify.Application.Tracks.Responses; + +namespace Musify.Application.Tracks.Commands +{ + public record RequestTrackUploadUrlsCommand( + Guid UserId, + string PictureFileType, + string PictureContentType, + string AudioFileType, + string AudioContentType) + : ICommand>; +} diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 07625a3..c014064 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -11,9 +11,13 @@ using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler { - public class CreateTrackCommandHandler(IDatabase database, IEventBus eventBus, IStorageService storageHandler, - ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration) - : ICommandHandler > + public class CreateTrackCommandHandler( + IDatabase database, + IEventBus eventBus, + ILogger logger, + ApplicationStorageConfiguration storageConfiguration, + TrackConfiguration trackConfiguration) + : ICommandHandler> { public async ValueTask> Handle(CreateTrackCommand request, CancellationToken cancellationToken) { @@ -25,102 +29,50 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } - var trackEntity = CreateTrackCommand.ToEntity( - request, - trackConfiguration.Routes.PresetSmallPicture, - trackConfiguration.Routes.PresetMediumPicture, - trackConfiguration.Routes.PresetLargePicture); + var trackEntity = new Track + { + Title = request.Title, + NormalizedTitle = request.Title.ToUpperInvariant(), + OriginalPictureName = request.OriginalPictureName, + OriginalAudioName = request.OriginalAudioName, + SmallPictureName = trackConfiguration.Routes.PresetSmallPicture, + MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, + LargePictureName = trackConfiguration.Routes.PresetLargePicture, + PicturesProcessingStatus = ProcessingStatus.Pending, + AudioTranscodeProcessingStatus = ProcessingStatus.Pending + }; - var fullPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(trackEntity.OriginalPictureName); - var fullAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(trackEntity.OriginalAudioName); + await database.Tracks.AddAsync(trackEntity, cancellationToken); - var uploadResult = await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken); - if (!uploadResult.IsSuccess) - return uploadResult; + var publishPictureResult = await PublishUpdateEvent(request.UserId, trackEntity, cancellationToken); + if (!publishPictureResult.IsSuccess) + return Result.Error($"Failed to create track for {request.Title}"); + + var publishTranscodeResult = await PublishTranscodeEvent(request.UserId, trackEntity, cancellationToken); + if (!publishTranscodeResult.IsSuccess) + return Result.Error($"Failed to create track for {request.Title}"); try { - await database.Tracks.AddAsync(trackEntity, cancellationToken); await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to save track {Title} after uploading files", request.Title); - await RollbackFilesAsync(fullPictureKey, fullAudioKey, cancellationToken); - return Result.Error($"Failed to create track for {request.Title}"); - } - - var publishPictureResult = await PublishUpdateEvent(trackEntity, cancellationToken); - if (!publishPictureResult.IsSuccess) - { - await RollbackTrackAfterPublishFailureAsync(trackEntity, fullPictureKey, fullAudioKey, cancellationToken); - return Result.Error($"Failed to create track for {request.Title}"); - } - - var publishTranscodeResult = await PublishTranscodeEvent(trackEntity, cancellationToken); - if (!publishTranscodeResult.IsSuccess) - { - await RollbackTrackAfterPublishFailureAsync(trackEntity, fullPictureKey, fullAudioKey, cancellationToken); + logger.LogError(exception, "Failed to save track {Title}", request.Title); return Result.Error($"Failed to create track for {request.Title}"); } return Result.Created(TrackApplicationResponse.FromEntity(trackEntity)); } - async Task UploadFilesAsync( - CreateTrackCommand request, - string pictureKey, - string audioKey, - CancellationToken cancellationToken) - { - try - { - await storageHandler.UploadFileAsync( - request.Picture.FileStream, - request.Picture.ContentType, - storageConfiguration.Bucket, - pictureKey, - cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to upload picture for track {Title}", request.Title); - return Result.Error($"Failed to upload picture for {request.Title}"); - } - - try - { - await storageHandler.UploadFileAsync( - request.Audio.FileStream, - request.Audio.ContentType, - storageConfiguration.Bucket, - audioKey, - cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to upload audio for track {Title}", request.Title); - await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); - return Result.Error($"Failed to upload audio for {request.Title}"); - } - return Result.Success(); - } - - async Task RollbackFilesAsync(string pictureKey, string audioKey, CancellationToken cancellationToken) - { - await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, pictureKey, cancellationToken); - await storageHandler.RemoveFileAsync(storageConfiguration.Bucket, audioKey, cancellationToken); - logger.LogInformation("Rolled back uploaded files {PictureKey}, {AudioKey}", pictureKey, audioKey); - } - - async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken) + async Task PublishUpdateEvent(Guid userId, Track track, CancellationToken cancellationToken) { try { await eventBus.PublishAsync(new UpdateTrackPictureEvent( track.Id, storageConfiguration.Bucket, - trackConfiguration.Routes.BuildOriginalPicturePath(track.OriginalPictureName), + trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName), new ImageSize( trackConfiguration.Routes.SmallPicturesPath, trackConfiguration.PicturesSizes.SmallPictureWidth, @@ -142,7 +94,7 @@ namespace Musify.Application.Tracks.Handler } } - async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken) + async Task PublishTranscodeEvent(Guid userId, Track track, CancellationToken cancellationToken) { var destinationFolderAudio = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString()); @@ -151,7 +103,7 @@ namespace Musify.Application.Tracks.Handler await eventBus.PublishAsync(new UpdateTrackAudioEvent( track.Id, storageConfiguration.Bucket, - trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), + trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName), storageConfiguration.Bucket, destinationFolderAudio), cancellationToken); return Result.Success(); @@ -163,30 +115,5 @@ namespace Musify.Application.Tracks.Handler } } - async Task RollbackTrackAfterPublishFailureAsync( - Track track, - string pictureKey, - string audioKey, - CancellationToken cancellationToken) - { - try - { - database.Tracks.Remove(track); - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback track {TrackId} persistence after publish failure", track.Id); - } - - try - { - await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to rollback uploaded files after publish failure for track {TrackId}", track.Id); - } - } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 8c78e75..bd07a1d 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -40,12 +40,21 @@ namespace Musify.Application.Tracks.Handler return Result.Conflict("Track is currently being processed and cannot be deleted"); } - var previousPicturesStatus = track.PicturesProcessingStatus; - var previousAudioStatus = track.AudioTranscodeProcessingStatus; - track.PicturesProcessingStatus = ProcessingStatus.Processing; track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing; + try + { + await eventBus.PublishAsync( + new DeleteTrackEvent(track.Id, request.UserId), + cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish delete track event for track {TrackId}", request.TrackId); + return Result.Error($"Failed to delete track {request.TrackId}"); + } + try { await database.SaveChangesAsync(cancellationToken); @@ -56,31 +65,6 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to delete track {request.TrackId}"); } - try - { - await eventBus.PublishAsync( - new DeleteTrackEvent(track.Id, request.UserId), - cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish delete track event for track {TrackId}", request.TrackId); - - track.PicturesProcessingStatus = previousPicturesStatus; - track.AudioTranscodeProcessingStatus = previousAudioStatus; - - try - { - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception rollbackException) - { - logger.LogError(rollbackException, "Failed to rollback track {TrackId} processing flags after publish failure", request.TrackId); - } - - return Result.Error($"Failed to delete track {request.TrackId}"); - } - return Result.NoContent(); } } diff --git a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs new file mode 100644 index 0000000..d4c4d68 --- /dev/null +++ b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs @@ -0,0 +1,110 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; +using Musify.Application.Tracks.Commands; +using Musify.Application.Tracks.Responses; + +namespace Musify.Application.Tracks.Handler +{ + public class RequestTrackUploadUrlsCommandHandler( + IDatabase database, + IStorageService storageService, + ILogger logger, + ApplicationStorageConfiguration storageConfiguration, + TrackConfiguration trackConfiguration) + : ICommandHandler> + { + static readonly string[] AllowedPictureExtensions = [".webp", ".png", ".jpg", ".jpeg"]; + static readonly string[] AllowedAudioExtensions = [".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".opus"]; + const int ExpiresInSeconds = 600; + + public async ValueTask> Handle(RequestTrackUploadUrlsCommand request, CancellationToken cancellationToken) + { + var userExists = await database.Users + .AsNoTracking() + .AnyAsync(u => u.Id == request.UserId, cancellationToken); + if (!userExists) + { + logger.LogWarning("User {UserId} not found", request.UserId); + return Result.NotFound($"User {request.UserId} not found"); + } + + var pictureExtensionResult = ValidateExtension(request.PictureFileType, AllowedPictureExtensions); + if (!pictureExtensionResult.IsSuccess) + return Result.Invalid(pictureExtensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); + + var audioExtensionResult = ValidateExtension(request.AudioFileType, AllowedAudioExtensions); + if (!audioExtensionResult.IsSuccess) + return Result.Invalid(audioExtensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); + + if (string.IsNullOrWhiteSpace(request.PictureContentType)) + return Result.Invalid(new ValidationError("PictureContentType is required")); + + if (string.IsNullOrWhiteSpace(request.AudioContentType)) + return Result.Invalid(new ValidationError("AudioContentType is required")); + + var pictureName = Guid.NewGuid() + pictureExtensionResult.Value; + var audioName = Guid.NewGuid() + audioExtensionResult.Value; + + var pictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureName); + var audioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioName); + + try + { + var pictureUploadUrl = await storageService.GetUploadUrlAsync( + storageConfiguration.Bucket, + pictureKey, + request.PictureContentType, + TimeSpan.FromSeconds(ExpiresInSeconds), + cancellationToken); + + var audioUploadUrl = await storageService.GetUploadUrlAsync( + storageConfiguration.Bucket, + audioKey, + request.AudioContentType, + TimeSpan.FromSeconds(ExpiresInSeconds), + cancellationToken); + + return Result.Success(new TrackUploadUrlsResponse( + storageConfiguration.Bucket, + pictureKey, + pictureName, + request.PictureContentType, + pictureUploadUrl, + audioKey, + audioName, + request.AudioContentType, + audioUploadUrl, + ExpiresInSeconds)); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to generate track upload URLs for user {UserId}", request.UserId); + return Result.Error("Failed to generate upload URLs"); + } + } + + static Result ValidateExtension(string fileType, string[] allowedExtensions) + { + if (string.IsNullOrWhiteSpace(fileType)) + return Result.Error("FileType is required"); + + var extension = fileType.Trim(); + if (!extension.StartsWith('.')) + extension = "." + extension; + + extension = extension.ToLowerInvariant(); + + if (extension.Contains('/') || extension.Contains('\\')) + return Result.Error("Invalid FileType"); + + if (!allowedExtensions.Contains(extension)) + return Result.Error($"Unsupported FileType '{extension}'"); + + return Result.Success(extension); + } + } +} diff --git a/Application/Tracks/Responses/TrackUploadUrlsResponse.cs b/Application/Tracks/Responses/TrackUploadUrlsResponse.cs new file mode 100644 index 0000000..cc5eb04 --- /dev/null +++ b/Application/Tracks/Responses/TrackUploadUrlsResponse.cs @@ -0,0 +1,14 @@ +namespace Musify.Application.Tracks.Responses +{ + public record TrackUploadUrlsResponse( + string Bucket, + string PictureKey, + string PictureName, + string PictureContentType, + string PictureUploadUrl, + string AudioKey, + string AudioName, + string AudioContentType, + string AudioUploadUrl, + int ExpiresInSeconds); +} diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index fde65d6..8599ea5 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -16,6 +16,7 @@ + diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index a10c283..286feff 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -17,17 +17,20 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.FilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(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('/', '\\'))); try { await storageHandler.UploadFileAsync( sourceFilePath, executeContext.Arguments.DestinationBucket, - Path.Combine(executeContext.Arguments.DestinationRoute, fileName), + destinationKey, executeContext.CancellationToken); return executeContext.CompletedWithVariables(new UploadFileToBucketLog( executeContext.Arguments.DestinationBucket, - executeContext.Arguments.DestinationRoute)); + destinationKey)); } catch (Exception exception) { diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs index 6175262..2e4bf08 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs @@ -33,11 +33,11 @@ namespace Musify.Infrastructure.MassTransit.Activities.Tracks try { - await RemovePicturesAsync(track, executeContext.CancellationToken); + await RemovePicturesAsync(track, executeContext.Arguments.UserId, executeContext.CancellationToken); track.PicturesProcessingStatus = ProcessingStatus.Completed; await database.SaveChangesAsync(executeContext.CancellationToken); - await RemoveAudiosAsync(track, executeContext.CancellationToken); + await RemoveAudiosAsync(track, executeContext.Arguments.UserId, executeContext.CancellationToken); track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; await database.SaveChangesAsync(executeContext.CancellationToken); @@ -66,9 +66,9 @@ namespace Musify.Infrastructure.MassTransit.Activities.Tracks } } - async Task RemovePicturesAsync(Track track, CancellationToken cancellationToken) + async Task RemovePicturesAsync(Track track, Guid userId, CancellationToken cancellationToken) { - await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalPicturePath(track.OriginalPictureName), cancellationToken); + await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName), cancellationToken); if (!track.IsPicturesProcessed) { @@ -83,9 +83,9 @@ namespace Musify.Infrastructure.MassTransit.Activities.Tracks await RemoveFileAsync(trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName), cancellationToken); } - async Task RemoveAudiosAsync(Track track, CancellationToken cancellationToken) + async Task RemoveAudiosAsync(Track track, Guid userId, CancellationToken cancellationToken) { - await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName), cancellationToken); + await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName), cancellationToken); if (!track.IsAudioProcessed) { diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs index 023ea42..82e8d35 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs @@ -1,5 +1,6 @@ using MassTransit; using Musify.Infrastructure.MassTransit.Consumers; +using Musify.Infrastructure.Persistence; namespace Musify.Infrastructure.MassTransit { @@ -12,7 +13,18 @@ namespace Musify.Infrastructure.MassTransit where TConsumer : class, IConsumer { busFactoryConfigurator.ReceiveEndpoint(queueName, endpointConfigurator => - endpointConfigurator.ConfigureConsumer(busRegistrationContext)); + { + endpointConfigurator.UseMessageRetry(retryConfigurator => + retryConfigurator.Exponential( + retryLimit: 5, + minInterval: TimeSpan.FromSeconds(1), + maxInterval: TimeSpan.FromSeconds(30), + intervalDelta: TimeSpan.FromSeconds(2))); + + endpointConfigurator.UseEntityFrameworkOutbox(busRegistrationContext); + + endpointConfigurator.ConfigureConsumer(busRegistrationContext); + }); } static void ConfigureExecuteActivityEndpoint( @@ -24,6 +36,15 @@ namespace Musify.Infrastructure.MassTransit { busFactoryConfigurator.ReceiveEndpoint(MassTransitEndpointNames.ExecuteQueue(endpointName), endpointConfigurator => { + endpointConfigurator.UseMessageRetry(retryConfigurator => + retryConfigurator.Exponential( + retryLimit: 5, + minInterval: TimeSpan.FromSeconds(1), + maxInterval: TimeSpan.FromSeconds(30), + intervalDelta: TimeSpan.FromSeconds(2))); + + endpointConfigurator.UseEntityFrameworkOutbox(busRegistrationContext); + endpointConfigurator.ExecuteActivityHost(busRegistrationContext); }); } @@ -37,14 +58,34 @@ namespace Musify.Infrastructure.MassTransit where TLog : class { busFactoryConfigurator.ReceiveEndpoint(MassTransitEndpointNames.ExecuteQueue(endpointName), endpointConfigurator => + { + endpointConfigurator.UseMessageRetry(retryConfigurator => + retryConfigurator.Exponential( + retryLimit: 5, + minInterval: TimeSpan.FromSeconds(1), + maxInterval: TimeSpan.FromSeconds(30), + intervalDelta: TimeSpan.FromSeconds(2))); + + endpointConfigurator.UseEntityFrameworkOutbox(busRegistrationContext); + endpointConfigurator.ExecuteActivityHost( EndpointHelper.BuildCompensateActivityUri(endpointName), - busRegistrationContext) - ); + busRegistrationContext); + }); busFactoryConfigurator.ReceiveEndpoint(MassTransitEndpointNames.CompensateQueue(endpointName), endpointConfigurator => - endpointConfigurator.CompensateActivityHost(busRegistrationContext) - ); + { + endpointConfigurator.UseMessageRetry(retryConfigurator => + retryConfigurator.Exponential( + retryLimit: 5, + minInterval: TimeSpan.FromSeconds(1), + maxInterval: TimeSpan.FromSeconds(30), + intervalDelta: TimeSpan.FromSeconds(2))); + + endpointConfigurator.UseEntityFrameworkOutbox(busRegistrationContext); + + endpointConfigurator.CompensateActivityHost(busRegistrationContext); + }); } } } diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index f6a8317..1bc01e6 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Persistence; namespace Musify.Infrastructure.MassTransit { @@ -18,6 +19,15 @@ namespace Musify.Infrastructure.MassTransit serviceDescriptors.AddScoped(); serviceDescriptors.AddMassTransit(options => { + options.AddEntityFrameworkOutbox(outbox => + { + outbox.UsePostgres(); + outbox.UseBusOutbox(); + + outbox.QueryDelay = TimeSpan.FromSeconds(1); + outbox.DuplicateDetectionWindow = TimeSpan.FromMinutes(30); + }); + options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => ConfigureRabbitMqHost( busFactoryConfigurator, @@ -43,6 +53,15 @@ namespace Musify.Infrastructure.MassTransit serviceDescriptors.AddMassTransit(options => { + options.AddEntityFrameworkOutbox(outbox => + { + outbox.UsePostgres(); + outbox.UseBusOutbox(); + + outbox.QueryDelay = TimeSpan.FromSeconds(1); + outbox.DuplicateDetectionWindow = TimeSpan.FromMinutes(30); + }); + RegisterConsumersAndActivities(options); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => diff --git a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs index 7692f01..50b398c 100644 --- a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs +++ b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs @@ -6,7 +6,7 @@ namespace Musify.Infrastructure.MassTransit public class MassTransitEventBus(IPublishEndpoint publishEndpoint) : IEventBus { - public async Task PublishAsync(T message, CancellationToken cancellationToken) + public async Task PublishAsync(T message, CancellationToken cancellationToken) where T : class { ArgumentNullException.ThrowIfNull(message, nameof(message)); diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index ef80864..fec9322 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -99,7 +99,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders AddResizeActivity( routingSlipBuilder, ActivityNames.ResizeMedium, - ActivityNames.ResizeMedium, + ActivityNames.UploadMedium, bucket, RoutingSlipVariableNames.Picture.MediumResizedFilePath, medium); diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 94874bb..778fc2d 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using MassTransit; using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; @@ -13,6 +14,15 @@ namespace Musify.Infrastructure.Persistence optionsBuilder.UseNpgsql(configuration.ConnectionString); } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.AddInboxStateEntity(); + modelBuilder.AddOutboxMessageEntity(); + modelBuilder.AddOutboxStateEntity(); + } + public DbSet Users => Set(); public DbSet Tracks => Set(); diff --git a/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs b/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs new file mode 100644 index 0000000..ce2d9a3 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs @@ -0,0 +1,512 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260514095113_AddMassTransitOutbox")] + partial class AddMassTransitOutbox + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs b/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs new file mode 100644 index 0000000..c4074bd --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddMassTransitOutbox : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "InboxState", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MessageId = table.Column(type: "uuid", nullable: false), + ConsumerId = table.Column(type: "uuid", nullable: false), + LockId = table.Column(type: "uuid", nullable: false), + RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), + Received = table.Column(type: "timestamp with time zone", nullable: false), + ReceiveCount = table.Column(type: "integer", nullable: false), + ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true), + Consumed = table.Column(type: "timestamp with time zone", nullable: true), + Delivered = table.Column(type: "timestamp with time zone", nullable: true), + LastSequenceNumber = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InboxState", x => x.Id); + table.UniqueConstraint("AK_InboxState_MessageId_ConsumerId", x => new { x.MessageId, x.ConsumerId }); + }); + + migrationBuilder.CreateTable( + name: "OutboxState", + columns: table => new + { + OutboxId = table.Column(type: "uuid", nullable: false), + LockId = table.Column(type: "uuid", nullable: false), + RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), + Created = table.Column(type: "timestamp with time zone", nullable: false), + Delivered = table.Column(type: "timestamp with time zone", nullable: true), + LastSequenceNumber = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxState", x => x.OutboxId); + }); + + migrationBuilder.CreateTable( + name: "OutboxMessage", + columns: table => new + { + SequenceNumber = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EnqueueTime = table.Column(type: "timestamp with time zone", nullable: true), + SentTime = table.Column(type: "timestamp with time zone", nullable: false), + Headers = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + InboxMessageId = table.Column(type: "uuid", nullable: true), + InboxConsumerId = table.Column(type: "uuid", nullable: true), + OutboxId = table.Column(type: "uuid", nullable: true), + MessageId = table.Column(type: "uuid", nullable: false), + ContentType = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + MessageType = table.Column(type: "text", nullable: false), + Body = table.Column(type: "text", nullable: false), + ConversationId = table.Column(type: "uuid", nullable: true), + CorrelationId = table.Column(type: "uuid", nullable: true), + InitiatorId = table.Column(type: "uuid", nullable: true), + RequestId = table.Column(type: "uuid", nullable: true), + SourceAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + DestinationAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ResponseAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + FaultAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessage", x => x.SequenceNumber); + table.ForeignKey( + name: "FK_OutboxMessage_InboxState_InboxMessageId_InboxConsumerId", + columns: x => new { x.InboxMessageId, x.InboxConsumerId }, + principalTable: "InboxState", + principalColumns: new[] { "MessageId", "ConsumerId" }); + table.ForeignKey( + name: "FK_OutboxMessage_OutboxState_OutboxId", + column: x => x.OutboxId, + principalTable: "OutboxState", + principalColumn: "OutboxId"); + }); + + migrationBuilder.CreateIndex( + name: "IX_InboxState_Delivered", + table: "InboxState", + column: "Delivered"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_EnqueueTime", + table: "OutboxMessage", + column: "EnqueueTime"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_ExpirationTime", + table: "OutboxMessage", + column: "ExpirationTime"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_InboxMessageId_InboxConsumerId_SequenceNumber", + table: "OutboxMessage", + columns: new[] { "InboxMessageId", "InboxConsumerId", "SequenceNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_OutboxId_SequenceNumber", + table: "OutboxMessage", + columns: new[] { "OutboxId", "SequenceNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OutboxState_Created", + table: "OutboxState", + column: "Created"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OutboxMessage"); + + migrationBuilder.DropTable( + name: "InboxState"); + + migrationBuilder.DropTable( + name: "OutboxState"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 0f04a84..12b1182 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -22,6 +22,174 @@ namespace Musify.Infrastructure.Persistence.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => { b.Property("Id") @@ -256,6 +424,18 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("UserHasTrack"); }); + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => { b.HasOne("Musify.Domain.Entities.User", "User") diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 1f170ef..806a271 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -48,6 +48,27 @@ namespace Musify.Infrastructure.Services return await amazonS3.GetPreSignedURLAsync(request); } + public async Task GetUploadUrlAsync( + string bucket, + string key, + string contentType, + TimeSpan expirationTime, + CancellationToken cancellationToken) + { + var request = new GetPreSignedUrlRequest + { + BucketName = bucket, + Key = key, + Expires = DateTime.UtcNow + expirationTime, + Protocol = storageClientConfiguration.UseHttp ? Protocol.HTTP : Protocol.HTTPS, + Verb = HttpVerb.PUT, + ContentType = contentType + }; + + logger.LogDebug("Generating pre-signed upload URL for {Bucket}/{Key}", bucket, key); + return await amazonS3.GetPreSignedURLAsync(request); + } + public async Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken) { var request = new DeleteObjectRequest @@ -74,7 +95,9 @@ namespace Musify.Infrastructure.Services string fileName = Path.GetFileName(file); string filePath = Path.Combine(sourceDirectory, file); - var key = Path.Combine(route, fileName); + var key = string.Join('/', new[] { route, fileName } + .Where(static s => !string.IsNullOrWhiteSpace(s)) + .Select(static s => s.Trim().Trim('/', '\\'))); var upload = new Upload { Id = Guid.NewGuid(), diff --git a/docs/changes-summary.md b/docs/changes-summary.md new file mode 100644 index 0000000..c6c57bc --- /dev/null +++ b/docs/changes-summary.md @@ -0,0 +1,165 @@ +# MusifyBackend — Resumen de cambios (2026-05-14) + +Este documento resume **todos los cambios aplicados hasta ahora** en la rama `master` durante esta iteración. + +> Nota: El proyecto compila en `Release` al cierre de estos cambios. + +--- + +## 1) Mensajería robusta: MassTransit + EF Outbox + +**Objetivo**: hacer la publicación de eventos confiable y transaccional (DB + mensajes) y tolerar redelivery en consumidores. + +### Cambios principales +- Se integró **MassTransit EntityFramework Outbox** con PostgreSQL (`UsePostgres` + `UseBusOutbox`). +- Se añadieron entidades/tablas de **Inbox/Outbox** en el `DbContext`. +- Se generó y añadió una **migración** para las tablas de outbox/inbox. +- Se ajustó la configuración de endpoints para aplicar: + - **Retry** (exponential backoff) + - **Idempotencia** en consumo usando `UseEntityFrameworkOutbox()` +- Se ajustó el orden de operaciones para el patrón outbox: **publicar evento y luego `SaveChanges`**. + +### Archivos involucrados (principalmente) +- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` +- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs` +- `Infrastructure/Persistence/Database.cs` +- `Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs` +- `Application/Abstractions/Infrastructure/IEventBus.cs` +- `Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs` + +--- + +## 2) Workflows: Courier RoutingSlip para imágenes y audio + +**Objetivo**: mantener el pipeline asíncrono (descarga, resize/transcode, upload, update de entidades) con reintentos e idempotencia. + +### Fix importante en workflow de imágenes +- Se corrigió el nombre de actividad del paso de **Upload del tamaño Medium** (antes duplicaba el nombre de `ResizeMedium`). + +Archivos: +- `Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs` +- `Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs` + +### Fix importante en compensación de uploads +- `UploadFileToBucketActivity` ahora registra correctamente el **object key completo** (ruta + filename) para que la compensación borre el objeto correcto. + +Archivo: +- `Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs` + +--- + +## 3) Uploads (Opción B): Presigned PUT URL + Create/Update con nombres + +**Objetivo**: separar la subida de binarios (S3) de la operación de dominio (Create/Update), evitando rollbacks complejos y haciendo el flujo más robusto. + +### Endpoint/Commands añadidos (request presigned URL) +- Playlist picture: + - `Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs` + - `Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs` + - `Application/PlayLists/Responses/PlayListPictureUploadResponse.cs` +- Track picture + audio: + - `Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs` + - `Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs` + - `Application/Tracks/Responses/TrackUploadUrlsResponse.cs` + +### Storage: presigned PUT +- Se añadió soporte para generar **presigned PUT URLs**: + - `Application/Abstractions/Infrastructure/IStorageService.cs` + - `Infrastructure/Services/StorageService.cs` (`GetUploadUrlAsync`) + +--- + +## 4) Limpieza: eliminar V1 y normalizar nombres (V2 → “normal”) + +**Objetivo**: eliminar comandos/handlers “V1” (subida directa por stream/multipart) y dejar un único set de comandos “normales” con el contrato final. + +### Resultado +- `CreatePlayListCommand`, `UpdatePlayListCommand` y `CreateTrackCommand` quedaron con el contrato nuevo (nombres/metadata, no streams). +- Se eliminaron archivos `*V2*` ya innecesarios. + +Archivos clave: +- `Application/PlayLists/Commands/CreatePlayListCommand.cs` +- `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` +- `Application/PlayLists/Commands/UpdatePlayListCommand.cs` +- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` +- `Application/Tracks/Commands/CreateTrackCommand.cs` +- `Application/Tracks/Handler/CreateTrackCommandHandler.cs` + +--- + +## 5) Prefijo de seguridad: `uploads/{userId}/...` para originales + +**Objetivo**: reducir superficie (y colisiones) haciendo que los objetos originales queden naturalmente “namespaced” por usuario. + +### Cambios +- Las **keys de originales** (playlist original picture, track original picture, track original audio) ahora se generan bajo: + - `uploads/{userId}/...` +- Se añadieron overloads de rutas para construir keys con `userId`. +- Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`. +- Los eventos de workflows (sourceKey) se publican con la key prefijada. +- El borrado de playlist borra la key original prefijada. +- El borrado de track (`DeleteTrackActivity`) borra originales usando el `userId` presente en `DeleteTrackArguments`. + +Archivos: +- `Application/Configuration/PlayListConfiguration.cs` +- `Application/Configuration/TrackConfiguration.cs` +- `Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs` +- `Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs` +- `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` +- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` +- `Application/Tracks/Handler/CreateTrackCommandHandler.cs` +- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` +- `Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs` + +--- + +## 6) Normalización de separadores en keys S3 (evitar `\\` en Windows) + +**Objetivo**: evitar que `Path.Combine` introduzca separadores `\\` en keys S3 (lo cual es incorrecto/inesperado en S3, y rompe interoperabilidad). + +### Cambios +- Se reemplazaron combinaciones de keys que usaban `Path.Combine` por joins con `/` en puntos críticos. + +Archivos: +- `Application/Configuration/PlayListConfiguration.cs` +- `Application/Configuration/TrackConfiguration.cs` +- `Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs` +- `Infrastructure/Services/StorageService.cs` + +--- + +## 7) Validaciones en Application eliminadas (delegadas a API) + +**Objetivo**: evitar duplicar validaciones de formato (extensiones/strings) si la capa API ya se encarga. + +### Cambios +- Se eliminó el helper de validación creado inicialmente y los checks en handlers. + +Archivos: +- Eliminado: `Application/Validation/UploadObjectNameValidator.cs` +- Ajustados: + - `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` + - `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` + - `Application/Tracks/Handler/CreateTrackCommandHandler.cs` + +--- + +## 8) Documento de mitigaciones (presigned uploads) + +Se añadió documentación de hardening/mitigaciones contra abuso/spam en endpoints presigned. + +Archivo: +- `docs/presigned-upload-hardening.md` + +--- + +## 9) Notas de compatibilidad / impacto + +- Este cambio **rompe compatibilidad** con la API anterior si existían endpoints que aceptaban multipart/streams para Create/Update (ahora se espera el flujo presigned y luego Create/Update con nombres). +- Los objetos originales ahora viven bajo `uploads/{userId}/...`. Objetos antiguos fuera de ese prefijo pueden quedar huérfanos si ya existían de antes. + +--- + +## 10) Estado final + +- `dotnet build MusifyBackend.slnx -c Release` ✅ diff --git a/docs/delete-workflows-plan.md b/docs/delete-workflows-plan.md new file mode 100644 index 0000000..5b92b0c --- /dev/null +++ b/docs/delete-workflows-plan.md @@ -0,0 +1,175 @@ +# Plan — Unificar Deletes con Workflows (RoutingSlip) + +## Contexto + +Actualmente hay dos problemas de diseño en los deletes: + +**PlayLists:** el borrado dispara múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego elimina la entidad en DB. Sin workflow, sin trazabilidad. + +**Tracks:** ya existe un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hace **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales. + +La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes. + +--- + +## Objetivos + +- **Un único camino** de borrado por aggregate (Track / PlayList), basado en **RoutingSlip con actividades granulares**. +- El handler de comando hace lo mínimo: validar ownership/reglas, cambiar estado a `Removing`, y publicar el evento de borrado (outbox). +- El workflow ejecuta pasos **ordenados, reutilizables y reiniciables** individualmente. +- **Reutilizar** `RemoveFileFromBucketActivity` (ya existente) para cada archivo a borrar, en lugar de lógica ad-hoc dentro de una activity monolítica. +- Eliminar `RemoveFileEvent`, `RemoveFileConsumer` y su registration si no tienen otros usos. + +No objetivos (por ahora): +- No introducir observabilidad extra (tracing/telemetría) fuera de lo ya existente. + +--- + +## Problemas del enfoque actual + +### PlayLists +- El handler implementa orquestación (decide qué archivos borrar y cómo) → acoplamiento alto. +- Publica múltiples `RemoveFileEvent` sueltos, sin agruparlos en un proceso único. +- No hay log de RoutingSlip que permita seguir el progreso/estado. + +### Tracks ← **nuevo problema identificado** +- `DeleteTrackActivity` es monolítica: carga entidad, borra pictures, borra audios, elimina de DB, todo en un solo `Execute`. +- No reutiliza `RemoveFileFromBucketActivity` ya existente. +- Si falla a mitad, no hay granularidad de reintento (no se sabe qué paso falló). +- El RoutingSlip tiene un único nodo → no es realmente un "slip" con pasos. + +--- + +## Diseño propuesto + +### Actividades compartidas / reutilizables + +Ya existe (y se mantiene): +- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip. + +Se añaden actividades de DB compartibles por aggregate: +- `MarkAsRemovingActivity` — cambia estado de la entidad a `Removing` y persiste. (O una por aggregate si la lógica difiere.) +- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones). +- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones). + +### Estructura del RoutingSlip para Track + +``` +DeleteTrackEvent + └─► DeleteTrackConsumer + └─► DeleteTrackRoutingSlipBuilder + ├─ MarkTrackAsRemovingActivity (cambia estado → Removing, persiste) + ├─ RemoveFileFromBucketActivity (original picture) + ├─ RemoveFileFromBucketActivity (small picture) ← solo si IsPicturesProcessed + ├─ RemoveFileFromBucketActivity (medium picture) ← solo si IsPicturesProcessed + ├─ RemoveFileFromBucketActivity (large picture) ← solo si IsPicturesProcessed + ├─ RemoveFileFromBucketActivity (original audio) + ├─ RemoveFileFromBucketActivity (processed audio) ← solo si IsAudioProcessed + └─ DeleteTrackFromDbActivity (Remove() + SaveChanges) +``` + +> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue). + +### Estructura del RoutingSlip para PlayList + +``` +DeletePlayListEvent + └─► DeletePlayListConsumer + └─► DeletePlayListRoutingSlipBuilder + ├─ MarkPlayListAsRemovingActivity (cambia estado → Removing, persiste) + ├─ RemoveFileFromBucketActivity (original — uploads/{userId}/...) ← solo si no es preset + ├─ RemoveFileFromBucketActivity (small) ← solo si no es preset + ├─ RemoveFileFromBucketActivity (medium) ← solo si no es preset + ├─ RemoveFileFromBucketActivity (large) ← solo si no es preset + └─ DeletePlayListFromDbActivity (Remove() + SaveChanges, cascade elimina PlayListHasTrack) +``` + +### Handler de comando (ambos aggregates) + +```csharp +// DeletePlayListCommandHandler — ANTES +await PublishRemoveFileEventsAsync(...); // 4 eventos sueltos +database.PlayLists.Remove(playlist); +await database.SaveChangesAsync(); + +// DESPUÉS +await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId)); +// Nada más. El workflow se encarga de todo. +``` + +```csharp +// DeleteTrackCommandHandler — ANTES +// (ya publicaba DeleteTrackEvent, se mantiene igual) +await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId)); + +// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica. +``` + +--- + +## Reglas de negocio a preservar + +- Ownership se valida en el handler del comando (igual que ahora), antes de publicar el evento. +- PlayList: solo borrar original/sizes si no son presets. +- PlayList: el original está bajo `uploads/{userId}/...` (cambio reciente). +- Track: respetar estados de procesamiento — si audio está `Pending/Processing`, el handler ya bloquea el delete antes de llegar al workflow. +- EF cascade elimina `PlayListHasTrack` al borrar la PlayList → no hace falta activity explícita para ello. + +--- + +## Idempotencia / reintentos + +- RoutingSlip + reintentos de endpoint ya configurados en MassTransit. +- Operaciones S3: idempotentes por diseño. Si el key no existe → log `Information` y `Completed()` (no throw). +- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora). +- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip. + +--- + +## Plan de implementación (paso a paso) + +### 1. Inventario +- Confirmar todos los usos de `RemoveFileEvent` / `RemoveFileConsumer` en la solución. +- Si no tienen otros usos fuera de playlist delete → marcarlos para eliminación en el paso 7. + +### 2. Refactorizar Track delete ← **nuevo** +- Crear `MarkTrackAsRemovingActivity` (execute-activity): carga Track, setea estado `Removing`, persiste. +- Crear `DeleteTrackFromDbActivity` (execute-activity): carga Track, llama `Remove()`, persiste. +- Actualizar `DeleteTrackRoutingSlipBuilder`: + - Carga el Track para calcular el plan (keys condicionales según `IsPicturesProcessed`, `IsAudioProcessed`). + - Encadena: `MarkTrackAsRemoving` → N × `RemoveFileFromBucket` → `DeleteTrackFromDb`. +- Eliminar `DeleteTrackActivity` monolítica (ya no se usa). +- Ajustar registration DI de MassTransit (quitar activity vieja, registrar las nuevas). + +### 3. Crear PlayList delete: evento + consumer + builder +- Añadir `DeletePlayListEvent(PlayListId, UserId)` en `Application/Events`. +- Crear `DeletePlayListConsumer` en Infrastructure: recibe el evento y ejecuta el builder. +- Crear `DeletePlayListRoutingSlipBuilder`: + - Carga la PlayList para calcular el plan (keys condicionales por presets, ruta de original). + - Encadena: `MarkPlayListAsRemoving` → N × `RemoveFileFromBucket` → `DeletePlayListFromDb`. + +### 4. Crear actividades de PlayList +- `MarkPlayListAsRemovingActivity` (execute-activity): carga PlayList, setea `Removing`, persiste. +- `DeletePlayListFromDbActivity` (execute-activity): carga PlayList, llama `Remove()`, persiste (cascade). + +### 5. Actualizar `RemoveFileFromBucketActivity` para NotFound +- Si el SDK lanza excepción por key inexistente → capturar, log `Information("Key {Key} not found, skipping")`, retornar `Completed()`. +- Si es otro error → log `Error` + throw (reintento normal). + +### 6. Cambiar `DeletePlayListCommandHandler` +- Reemplazar `PublishRemoveFileEventsAsync` por `eventBus.PublishAsync(new DeletePlayListEvent(...))`. +- No publicar más `RemoveFileEvent` para playlist delete. + +### 7. Ajustar DI / registration MassTransit +- Registrar: `DeletePlayListConsumer`, `MarkPlayListAsRemovingActivity`, `DeletePlayListFromDbActivity`. +- Registrar: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`. +- Desregistrar: `DeleteTrackActivity` (monolítica), y si procede `RemoveFileConsumer`. + +### 8. Eliminar código muerto +- `DeleteTrackActivity` monolítica. +- `RemoveFileEvent` y `RemoveFileConsumer` (si no tienen otros usos). +- Endpoints y registrations asociados. + +### 9. Build y verificación +- `dotnet build -c Release` sin warnings de referencias muertas. +- Smoke test manual: borrar un Track y una PlayList, verificar logs de cada activity en orden. \ No newline at end of file diff --git a/docs/presigned-upload-hardening.md b/docs/presigned-upload-hardening.md new file mode 100644 index 0000000..a31b247 --- /dev/null +++ b/docs/presigned-upload-hardening.md @@ -0,0 +1,95 @@ +# Hardening: abuso de pre-signed uploads (Opción B) + +## Contexto +Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando `pictureName/audioName`. + +Esto mejora latencia y descarga la API, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”). + +## Qué pasa hoy si el usuario spamea +- Se generan muchas URLs pre-firmadas (costo bajo pero no cero). +- El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB. +- El impacto principal es: + - coste/uso de S3 (storage + requests) + - posible saturación del endpoint de generación de URLs + +## Objetivo +Reducir abuso y coste, manteniendo: +- UX simple +- backend sin streams +- workflows asíncronos + MassTransit Outbox + +## Mitigaciones recomendadas (pueden coexistir) + +### 1) Rate limiting por usuario/IP (primera línea de defensa) +Aplicar limitación en el host/API (no en `Application`). Ejemplos: +- `N` requests/min por `UserId` +- opcional: `M` requests/min por IP + +Ventajas: +- Corta abuso sin tocar DB/S3. +- No cambia dominio. + +Nota: requiere acceso al proyecto host (controllers/minimal API) para aplicarlo. + +### 2) “Upload intents” en DB (control y conciliación) +Persistir una intención de upload cuando se genera un pre-signed URL. + +Modelo sugerido (mínimo): +- `UploadIntent` + - `Id` + - `UserId` + - `Bucket` + - `Key` + - `ContentType` + - `ExpiresAt` + - `Status`: `Issued | Consumed | Expired` + - `CreatedAt` + +Reglas: +- Al pedir URL (Request*Upload*): + - insertar intent `Issued` + - imponer límite por usuario: p.ej. `MaxActiveIntentsPerUser = 5` + - si supera el límite: devolver `429 Too Many Requests` (o `Conflict`) según convención del API +- Al crear/actualizar entidad (Create*/Update* V2): + - validar que el `pictureName/audioName` recibido corresponde a un intent `Issued` del mismo `UserId` y no expirado + - marcar intent como `Consumed` + +Ventajas: +- Previene spam con reglas duras y auditables. +- Permite cleanup de huérfanos con certeza. + +Coste: +- entidad + migración + lógica de validación. + +### 3) Limitar el scope del pre-signed URL (reducción de daño) +Aplicar restricciones conservadoras: +- Reducir TTL: + - `ExpiresInSeconds` más bajo (p.ej. 60–120s en vez de 600s) +- Validar/limitar: + - `ContentType` permitido + - extensiones permitidas (`FileType`) +- Usar keys temporales por usuario: + - `PlayLists/TempUploads/{userId}/{guid}.{ext}` + - `Tracks/TempUploads/{userId}/{guid}.{ext}` + +Luego, en el workflow/worker: +- mover/copiar de `TempUploads/...` a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`) +- borrar el objeto temporal + +Ventajas: +- Cleanup masivo sencillo (por prefijo `TempUploads/{userId}`) +- Reduce el impacto de huérfanos. + +## Orden de implementación sugerido +1) Rate limiting en API (rápido + mayor impacto) +2) Upload intents en DB (límite de activos + validación al crear/actualizar) +3) Reducir TTL y mover a rutas temporales (`TempUploads/...`) + cleanup + +## Parámetros recomendados (config) +- `UploadUrlExpiresInSeconds` (default: 120) +- `MaxActiveUploadIntentsPerUser` (default: 5) +- `AllowedPictureExtensions` / `AllowedAudioExtensions` +- `AllowedContentTypes` (lista blanca) + +## Notas +- Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos. diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..1ab5b91 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,50 @@ +{ + "version": 1, + "skills": { + "csharp-async": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "213d4b0921836fdf1ea3d218bc3625f48d149463f9bc765aac0eb343f31cfe37" + }, + "csharp-docs": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "03ba5f2f605625202803d1cc79353e10b4f3ff720b93992aeecc8d88a1e44720" + }, + "csharp-mstest": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "abb51cb248cb42f92716dc6568da8c33a9ea23af9a34155ef3ba156a61d9693d" + }, + "csharp-nunit": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "0493f7b95a519b13685335a32c9ff86e4eaca53e44a4f6795d11f72f899c683d" + }, + "csharp-tunit": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "5c6139f8cf75687df89a7c8c8fe8834676ffb496b632433973c84e128b52c2f6" + }, + "csharp-xunit": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "808c2d86afd16bccaba5f6d499361eb1da4d3e687a3305aa376ad4d40382e27d" + }, + "dotnet-best-practices": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "98bd60f0529ffbb6b6319e61e952ce11ae9deb99106212d1070007bc8ae6708f" + }, + "dotnet-design-pattern-review": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "60e201c6f3bd2832ed3834a8bb75143b2e6647e6026c5cbdad62f7706700c496" + }, + "dotnet-upgrade": { + "source": "github/awesome-copilot", + "sourceType": "autoskills-registry", + "computedHash": "77925fe0898ceeab04c574390815e7500a2426a26036a357325ce751f23cce71" + } + } +} From a4128d0953abcd43382a9668106d1c253fee3987 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 14 May 2026 18:24:20 +0200 Subject: [PATCH 136/172] Update delete workflows --- Application/Events/DeletePlayListEvent.cs | 6 + Application/Events/RemoveFileEvent.cs | 6 - .../Handlers/DeletePlayListCommandHandler.cs | 104 +--- .../Handler/DeleteTrackCommandHandler.cs | 3 +- Domain/Entities/LifeCycleStatus.cs | 8 + Domain/Entities/PlayList.cs | 3 + Domain/Entities/Track.cs | 3 + .../Files/RemoveFileFromBucketActivity.cs | 12 + .../PlayLists/DeletePlayListFromDbActivity.cs | 35 ++ .../DeletePlayListFromDbArguments.cs | 5 + .../MarkPlayListAsRemovingActivity.cs | 36 ++ .../MarkPlayListAsRemovingArguments.cs | 5 + .../Activities/Tracks/DeleteTrackActivity.cs | 115 ---- .../Activities/Tracks/DeleteTrackArguments.cs | 6 - .../Tracks/DeleteTrackFromDbActivity.cs | 35 ++ .../Tracks/DeleteTrackFromDbArguments.cs | 5 + .../Tracks/MarkTrackAsRemovingActivity.cs | 36 ++ .../Tracks/MarkTrackAsRemovingArguments.cs | 5 + .../Consumers/DeletePlayListConsumer.cs | 27 + .../Consumers/DeleteTrackConsumer.cs | 4 +- .../Consumers/RemoveFileConsumer.cs | 40 -- ...TransitDependencyInjection.Registration.cs | 33 +- .../MassTransit/RoutingSlip/ActivityNames.cs | 4 + .../DeletePlayListRoutingSlipBuilder.cs | 94 ++++ .../Builders/DeleteTrackRoutingSlipBuilder.cs | 84 ++- ...60514161518_AddLifeCycleStatus.Designer.cs | 518 ++++++++++++++++++ .../20260514161518_AddLifeCycleStatus.cs | 40 ++ .../Migrations/DatabaseModelSnapshot.cs | 6 + docs/changes-summary.md | 35 +- docs/delete-workflows-plan.md | 78 ++- 30 files changed, 1098 insertions(+), 293 deletions(-) create mode 100644 Application/Events/DeletePlayListEvent.cs delete mode 100644 Application/Events/RemoveFileEvent.cs create mode 100644 Domain/Entities/LifeCycleStatus.cs create mode 100644 Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbArguments.cs create mode 100644 Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingArguments.cs delete mode 100644 Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs delete mode 100644 Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbArguments.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingArguments.cs create mode 100644 Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs delete mode 100644 Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs create mode 100644 Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs diff --git a/Application/Events/DeletePlayListEvent.cs b/Application/Events/DeletePlayListEvent.cs new file mode 100644 index 0000000..0c068df --- /dev/null +++ b/Application/Events/DeletePlayListEvent.cs @@ -0,0 +1,6 @@ +namespace Musify.Application.Events +{ + public record DeletePlayListEvent( + Guid PlayListId, + Guid UserId); +} diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs deleted file mode 100644 index 4378be5..0000000 --- a/Application/Events/RemoveFileEvent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Musify.Application.Events -{ - public record RemoveFileEvent( - string Bucket, - string Key); -} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 08d3774..e7be170 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -2,15 +2,17 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Configuration; using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; +using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { - public class DeletePlayListCommandHandler(IDatabase database, IEventBus eventBus, - ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger logger) + public class DeletePlayListCommandHandler( + IDatabase database, + IEventBus eventBus, + ILogger logger) : ICommandHandler { public async ValueTask Handle(DeletePlayListCommand request, CancellationToken cancellationToken) @@ -28,101 +30,31 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } - var originalPictureName = playList.OriginalPictureName; - var smallPictureName = playList.SmallPictureName; - var mediumPictureName = playList.MediumPictureName; - var largePictureName = playList.LargePictureName; + playList.LifeCycleStatus = LifeCycleStatus.Removing; - var originalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(playList.UserId, playList.OriginalPictureName); - var smallPictureKey = playListConfiguration.Routes.BuildSmallPicturePath(playList.SmallPictureName); - var mediumPictureKey = playListConfiguration.Routes.BuildMediumPicturePath(playList.MediumPictureName); - var largePictureKey = playListConfiguration.Routes.BuildLargePicturePath(playList.LargePictureName); - - var publishResult = await PublishRemoveFileEventsAsync( - playList.Id, - originalPictureName, - smallPictureName, - mediumPictureName, - largePictureName, - originalPictureKey, - smallPictureKey, - mediumPictureKey, - largePictureKey, - playListConfiguration.Routes, - cancellationToken); - - if (!publishResult.IsSuccess) - return publishResult; + try + { + await eventBus.PublishAsync( + new DeletePlayListEvent(playList.Id, request.UserId), + cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish delete playlist event for playlist {PlayListId}", request.PlayListId); + return Result.Error($"Failed to delete playlist {request.PlayListId}"); + } try { - database.PlayLists.Remove(playList); await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to delete playlist {PlayListId}", request.PlayListId); + logger.LogError(exception, "Failed to mark playlist {PlayListId} as removing", request.PlayListId); return Result.Error($"Failed to delete playlist {request.PlayListId}"); } return Result.NoContent(); } - - async Task PublishRemoveFileEventsAsync( - Guid playListId, - string originalPictureName, - string smallPictureName, - string mediumPictureName, - string largePictureName, - string originalPictureKey, - string smallPictureKey, - string mediumPictureKey, - string largePictureKey, - PlayListRoutes playListRoutes, - CancellationToken cancellationToken) - { - if (originalPictureName != playListRoutes.PresetOriginalPicture) - { - var result = await PublishRemoveFileEventAsync(playListId, originalPictureKey, cancellationToken); - if (!result.IsSuccess) - return result; - } - if (smallPictureName != playListRoutes.PresetSmallPicture) - { - var result = await PublishRemoveFileEventAsync(playListId, smallPictureKey, cancellationToken); - if (!result.IsSuccess) - return result; - } - if (mediumPictureName != playListRoutes.PresetMediumPicture) - { - var result = await PublishRemoveFileEventAsync(playListId, mediumPictureKey, cancellationToken); - if (!result.IsSuccess) - return result; - } - if (largePictureName != playListRoutes.PresetLargePicture) - { - var result = await PublishRemoveFileEventAsync(playListId, largePictureKey, cancellationToken); - if (!result.IsSuccess) - return result; - } - - return Result.Success(); - } - - async Task PublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) - { - try - { - await eventBus.PublishAsync(new RemoveFileEvent( - storageConfiguration.Bucket, - key), cancellationToken); - return Result.Success(); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish remove file event for playlist {PlayListId} with key {Key}", playListId, key); - return Result.Error($"Failed to publish remove file event for playlist {playListId}"); - } - } } } \ No newline at end of file diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index bd07a1d..069254a 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -40,8 +40,7 @@ namespace Musify.Application.Tracks.Handler return Result.Conflict("Track is currently being processed and cannot be deleted"); } - track.PicturesProcessingStatus = ProcessingStatus.Processing; - track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing; + track.LifeCycleStatus = LifeCycleStatus.Removing; try { diff --git a/Domain/Entities/LifeCycleStatus.cs b/Domain/Entities/LifeCycleStatus.cs new file mode 100644 index 0000000..bd4c7dc --- /dev/null +++ b/Domain/Entities/LifeCycleStatus.cs @@ -0,0 +1,8 @@ +namespace Musify.Domain.Entities +{ + public enum LifeCycleStatus + { + Active = 0, + Removing = 1 + } +} diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 3d73d19..e01985f 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -41,6 +41,9 @@ namespace Musify.Domain.Entities [MaxLength(64)] public required string LargePictureName { get; set; } + [Required] + public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; + [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index 82fa295..182b56b 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -44,6 +44,9 @@ namespace Musify.Domain.Entities [Required] public ProcessingStatus AudioTranscodeProcessingStatus { get; set; } = ProcessingStatus.Pending; + [Required] + public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; + [Required] public int RetryCount { get; set; } = 0; diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 45125d8..a28baa9 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -1,7 +1,9 @@ using MassTransit; +using Amazon.S3; using Microsoft.Extensions.Logging; using Musify.Application.Abstractions.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; +using System.Net; namespace Musify.Infrastructure.MassTransit.Activities.Files { @@ -22,6 +24,16 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files 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(); + } catch (Exception exception) { logger.LogError(exception, "Failed to remove {Key} from bucket {Bucket}", diff --git a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs new file mode 100644 index 0000000..e929f29 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs @@ -0,0 +1,35 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; + +namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +{ + internal class DeletePlayListFromDbActivity( + IDatabase database, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "delete-playList-from-db"; + + public async Task Execute(ExecuteContext executeContext) + { + var playList = await database.PlayLists + .SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken); + + if (playList is null) + { + logger.LogInformation( + "PlayList {PlayListId} not found while deleting from DB, skipping", + executeContext.Arguments.PlayListId); + return executeContext.Completed(); + } + + database.PlayLists.Remove(playList); + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Deleted playlist {PlayListId} from DB", playList.Id); + return executeContext.Completed(); + } + } +} diff --git a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbArguments.cs b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbArguments.cs new file mode 100644 index 0000000..9890c26 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbArguments.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +{ + internal record DeletePlayListFromDbArguments( + Guid PlayListId); +} diff --git a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs new file mode 100644 index 0000000..67d09ad --- /dev/null +++ b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs @@ -0,0 +1,36 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +{ + internal class MarkPlayListAsRemovingActivity( + IDatabase database, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "mark-playList-as-removing"; + + public async Task Execute(ExecuteContext executeContext) + { + var playList = await database.PlayLists + .SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken); + + if (playList is null) + { + logger.LogInformation( + "PlayList {PlayListId} not found while marking as removing, skipping", + executeContext.Arguments.PlayListId); + return executeContext.Completed(); + } + + playList.LifeCycleStatus = LifeCycleStatus.Removing; + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Marked playlist {PlayListId} as removing", playList.Id); + return executeContext.Completed(); + } + } +} diff --git a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingArguments.cs b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingArguments.cs new file mode 100644 index 0000000..fefbf99 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingArguments.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +{ + internal record MarkPlayListAsRemovingArguments( + Guid PlayListId); +} diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs deleted file mode 100644 index 2e4bf08..0000000 --- a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs +++ /dev/null @@ -1,115 +0,0 @@ -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Musify.Application.Configuration; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Domain.Entities; - -namespace Musify.Infrastructure.MassTransit.Activities.Tracks -{ - internal class DeleteTrackActivity( - IDatabase database, - IStorageService storageService, - TrackConfiguration trackConfiguration, - ApplicationStorageConfiguration storageConfiguration, - ILogger logger) - : IExecuteActivity - { - public const string ExecuteEndpointName = "delete-track"; - - public async Task Execute(ExecuteContext executeContext) - { - Track track; - try - { - var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); - track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found"); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to retrieve track {TrackId} for deletion", executeContext.Arguments.TrackId); - throw; - } - - try - { - await RemovePicturesAsync(track, executeContext.Arguments.UserId, executeContext.CancellationToken); - track.PicturesProcessingStatus = ProcessingStatus.Completed; - await database.SaveChangesAsync(executeContext.CancellationToken); - - await RemoveAudiosAsync(track, executeContext.Arguments.UserId, executeContext.CancellationToken); - track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; - await database.SaveChangesAsync(executeContext.CancellationToken); - - database.Tracks.Remove(track); - await database.SaveChangesAsync(executeContext.CancellationToken); - - logger.LogInformation("Deleted track {TrackId}", track.Id); - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to delete track {TrackId}", executeContext.Arguments.TrackId); - - try - { - track.PicturesProcessingStatus = ProcessingStatus.Failed; - track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; - await database.SaveChangesAsync(executeContext.CancellationToken); - } - catch (Exception dbException) - { - logger.LogError(dbException, "Failed to update processing statuses to Failed for track {TrackId}", executeContext.Arguments.TrackId); - } - - throw; - } - } - - async Task RemovePicturesAsync(Track track, Guid userId, CancellationToken cancellationToken) - { - await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName), cancellationToken); - - if (!track.IsPicturesProcessed) - { - logger.LogInformation( - "Track {TrackId} pictures are not fully processed, skipping removal of small, medium, and large pictures", - track.Id); - return; - } - - await RemoveFileAsync(trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName), cancellationToken); - await RemoveFileAsync(trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName), cancellationToken); - await RemoveFileAsync(trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName), cancellationToken); - } - - async Task RemoveAudiosAsync(Track track, Guid userId, CancellationToken cancellationToken) - { - await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName), cancellationToken); - - if (!track.IsAudioProcessed) - { - logger.LogInformation( - "Track {TrackId} audio is not fully processed, skipping removal of processed audio", - track.Id); - return; - } - - await RemoveFileAsync(trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName), cancellationToken); - } - - async Task RemoveFileAsync(string key, CancellationToken cancellationToken) - { - try - { - logger.LogDebug("Removing file {Key} from bucket {Bucket}", key, storageConfiguration.Bucket); - await storageService.RemoveFileAsync(storageConfiguration.Bucket, key, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to remove file {Key} from bucket {Bucket}", key, storageConfiguration.Bucket); - throw; - } - } - } -} diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs deleted file mode 100644 index 0caacb8..0000000 --- a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackArguments.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Tracks -{ - internal record DeleteTrackArguments( - Guid TrackId, - Guid UserId); -} diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs new file mode 100644 index 0000000..f1a5d0d --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs @@ -0,0 +1,35 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; + +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal class DeleteTrackFromDbActivity( + IDatabase database, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "delete-track-from-db"; + + public async Task Execute(ExecuteContext executeContext) + { + var track = await database.Tracks + .SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + + if (track is null) + { + logger.LogInformation( + "Track {TrackId} not found while deleting from DB, skipping", + executeContext.Arguments.TrackId); + return executeContext.Completed(); + } + + database.Tracks.Remove(track); + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Deleted track {TrackId} from DB", track.Id); + return executeContext.Completed(); + } + } +} diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbArguments.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbArguments.cs new file mode 100644 index 0000000..be5b3ae --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbArguments.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal record DeleteTrackFromDbArguments( + Guid TrackId); +} diff --git a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs new file mode 100644 index 0000000..4290155 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs @@ -0,0 +1,36 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal class MarkTrackAsRemovingActivity( + IDatabase database, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "mark-track-as-removing"; + + public async Task Execute(ExecuteContext executeContext) + { + var track = await database.Tracks + .SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + + if (track is null) + { + logger.LogInformation( + "Track {TrackId} not found while marking as removing, skipping", + executeContext.Arguments.TrackId); + return executeContext.Completed(); + } + + track.LifeCycleStatus = LifeCycleStatus.Removing; + await database.SaveChangesAsync(executeContext.CancellationToken); + + logger.LogInformation("Marked track {TrackId} as removing", track.Id); + return executeContext.Completed(); + } + } +} diff --git a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingArguments.cs b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingArguments.cs new file mode 100644 index 0000000..70625ca --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingArguments.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal record MarkTrackAsRemovingArguments( + Guid TrackId); +} diff --git a/Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs b/Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs new file mode 100644 index 0000000..8c19ed5 --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs @@ -0,0 +1,27 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class DeletePlayListConsumer( + IBus bus, + DeletePlayListRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "delete-playList"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = (await routingSlipBuilder + .BuildAsync( + consumeContext.Message.PlayListId, + consumeContext.Message.UserId, + consumeContext.CorrelationId, + consumeContext.CancellationToken)) + .Build(); + + await bus.Execute(routingSlip); + } + } +} diff --git a/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs b/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs index b5c3ee1..fbdc9a8 100644 --- a/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs +++ b/Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs @@ -13,8 +13,8 @@ namespace Musify.Infrastructure.MassTransit.Consumers public async Task Consume(ConsumeContext consumeContext) { - var routingSlip = routingSlipBuilder - .Build(consumeContext.Message.TrackId, consumeContext.Message.UserId, consumeContext.CorrelationId) + var routingSlip = (await routingSlipBuilder + .BuildAsync(consumeContext.Message.TrackId, consumeContext.Message.UserId, consumeContext.CorrelationId, consumeContext.CancellationToken)) .Build(); await bus.Execute(routingSlip); diff --git a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs b/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs deleted file mode 100644 index e56b117..0000000 --- a/Infrastructure/MassTransit/Consumers/RemoveFileConsumer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Application.Events; - -namespace Musify.Infrastructure.MassTransit.Consumers -{ - public class RemoveFileConsumer( - IStorageService storageService, - ILogger logger) - : IConsumer - { - public const string QueueName = "remove-file"; - - public async Task Consume(ConsumeContext consumeContext) - { - try - { - await storageService.RemoveFileAsync( - consumeContext.Message.Bucket, - consumeContext.Message.Key, - consumeContext.CancellationToken); - - logger.LogInformation( - "Removed file {Key} from bucket {Bucket}", - consumeContext.Message.Key, - consumeContext.Message.Bucket); - } - catch (Exception exception) - { - logger.LogError( - exception, - "Failed to remove file {Key} from bucket {Bucket}", - consumeContext.Message.Key, - consumeContext.Message.Bucket); - throw; - } - } - } -} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index f1620cd..860efac 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -6,6 +6,7 @@ using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Audio; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Infrastructure.MassTransit.Activities.PlayLists; using Musify.Infrastructure.MassTransit.Activities.Tracks; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; @@ -21,6 +22,7 @@ namespace Musify.Infrastructure.MassTransit services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options) @@ -30,12 +32,15 @@ namespace Musify.Infrastructure.MassTransit options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); - options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); - options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); options.AddActivity(); options.AddActivity(); @@ -75,10 +80,10 @@ namespace Musify.Infrastructure.MassTransit busRegistrationContext, DeleteTrackConsumer.QueueName); - ConfigureConsumerEndpoint( + ConfigureConsumerEndpoint( busFactoryConfigurator, busRegistrationContext, - RemoveFileConsumer.QueueName); + DeletePlayListConsumer.QueueName); ConfigureConsumerEndpoint( busFactoryConfigurator, @@ -100,6 +105,26 @@ namespace Musify.Infrastructure.MassTransit busRegistrationContext, GeneratePictureWorkflowPathsActivity.ExecuteEndpointName); + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + MarkTrackAsRemovingActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + DeleteTrackFromDbActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + MarkPlayListAsRemovingActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + DeletePlayListFromDbActivity.ExecuteEndpointName); + ConfigureActivityEndpoint( busFactoryConfigurator, busRegistrationContext, diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index b5d9ff5..71d037d 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -3,6 +3,10 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip internal static class ActivityNames { internal const string DeleteTrack = "DeleteTrack"; + internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving"; + internal const string DeleteTrackFromDb = "DeleteTrackFromDb"; + internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving"; + internal const string DeletePlayListFromDb = "DeletePlayListFromDb"; internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths"; internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths"; internal const string DownloadFile = "DownloadFile"; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs new file mode 100644 index 0000000..232744c --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs @@ -0,0 +1,94 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.PlayLists; +using Musify.Infrastructure.MassTransit.Consumers; + +namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders +{ + public class DeletePlayListRoutingSlipBuilder( + IDatabase database, + PlayListConfiguration playListConfiguration, + ApplicationStorageConfiguration storageConfiguration) + { + public async Task BuildAsync( + Guid playListId, + Guid userId, + Guid? correlationId, + CancellationToken cancellationToken) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + + routingSlipBuilder.AddSubscription( + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + + var playList = await database.PlayLists + .AsNoTracking() + .SingleOrDefaultAsync(p => p.Id == playListId, cancellationToken); + + routingSlipBuilder.AddActivity( + ActivityNames.MarkPlayListAsRemoving, + EndpointHelper.BuildExecuteActivityUri(MarkPlayListAsRemovingActivity.ExecuteEndpointName), + new MarkPlayListAsRemovingArguments(playListId)); + + if (playList is not null) + { + var bucket = storageConfiguration.Bucket; + var routes = playListConfiguration.Routes; + + if (playList.OriginalPictureName != routes.PresetOriginalPicture) + { + routingSlipBuilder.AddActivity( + "RemovePlayListOriginalPicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + routes.BuildOriginalPicturePath(userId, playList.OriginalPictureName))); + } + + if (playList.SmallPictureName != routes.PresetSmallPicture) + { + routingSlipBuilder.AddActivity( + "RemovePlayListSmallPicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + routes.BuildSmallPicturePath(playList.SmallPictureName))); + } + + if (playList.MediumPictureName != routes.PresetMediumPicture) + { + routingSlipBuilder.AddActivity( + "RemovePlayListMediumPicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + routes.BuildMediumPicturePath(playList.MediumPictureName))); + } + + if (playList.LargePictureName != routes.PresetLargePicture) + { + routingSlipBuilder.AddActivity( + "RemovePlayListLargePicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + routes.BuildLargePicturePath(playList.LargePictureName))); + } + } + + routingSlipBuilder.AddActivity( + ActivityNames.DeletePlayListFromDb, + EndpointHelper.BuildExecuteActivityUri(DeletePlayListFromDbActivity.ExecuteEndpointName), + new DeletePlayListFromDbArguments(playListId)); + + return routingSlipBuilder; + } + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index 942254d..6da734e 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -1,13 +1,25 @@ using MassTransit; using MassTransit.Courier.Contracts; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Tracks; using Musify.Infrastructure.MassTransit.Consumers; namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { - public class DeleteTrackRoutingSlipBuilder + public class DeleteTrackRoutingSlipBuilder( + IDatabase database, + TrackConfiguration trackConfiguration, + ApplicationStorageConfiguration storageConfiguration) { - public RoutingSlipBuilder Build(Guid trackId, Guid userId, Guid? correlationId) + public async Task BuildAsync( + Guid trackId, + Guid userId, + Guid? correlationId, + CancellationToken cancellationToken) { var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); @@ -16,10 +28,72 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + var track = await database.Tracks + .AsNoTracking() + .SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken); + routingSlipBuilder.AddActivity( - ActivityNames.DeleteTrack, - EndpointHelper.BuildExecuteActivityUri(DeleteTrackActivity.ExecuteEndpointName), - new DeleteTrackArguments(trackId, userId)); + ActivityNames.MarkTrackAsRemoving, + EndpointHelper.BuildExecuteActivityUri(MarkTrackAsRemovingActivity.ExecuteEndpointName), + new MarkTrackAsRemovingArguments(trackId)); + + if (track is not null) + { + var bucket = storageConfiguration.Bucket; + + routingSlipBuilder.AddActivity( + "RemoveTrackOriginalPicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName))); + + if (track.IsPicturesProcessed) + { + routingSlipBuilder.AddActivity( + "RemoveTrackSmallPicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName))); + + routingSlipBuilder.AddActivity( + "RemoveTrackMediumPicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName))); + + routingSlipBuilder.AddActivity( + "RemoveTrackLargePicture", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName))); + } + + routingSlipBuilder.AddActivity( + "RemoveTrackOriginalAudio", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName))); + + if (track.IsAudioProcessed) + { + routingSlipBuilder.AddActivity( + "RemoveTrackProcessedAudio", + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments( + bucket, + trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName))); + } + } + + routingSlipBuilder.AddActivity( + ActivityNames.DeleteTrackFromDb, + EndpointHelper.BuildExecuteActivityUri(DeleteTrackFromDbActivity.ExecuteEndpointName), + new DeleteTrackFromDbArguments(trackId)); return routingSlipBuilder; } diff --git a/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs b/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs new file mode 100644 index 0000000..98c3d87 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs @@ -0,0 +1,518 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260514161518_AddLifeCycleStatus")] + partial class AddLifeCycleStatus + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs b/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs new file mode 100644 index 0000000..68a1b0c --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddLifeCycleStatus : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LifeCycleStatus", + table: "Tracks", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "LifeCycleStatus", + table: "PlayLists", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LifeCycleStatus", + table: "Tracks"); + + migrationBuilder.DropColumn( + name: "LifeCycleStatus", + table: "PlayLists"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 12b1182..30ec587 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -209,6 +209,9 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + b.Property("MediumPictureName") .IsRequired() .HasMaxLength(64) @@ -297,6 +300,9 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("LastRetryAt") .HasColumnType("timestamp with time zone"); + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + b.Property("MediumPictureName") .HasMaxLength(64) .HasColumnType("character varying(64)"); diff --git a/docs/changes-summary.md b/docs/changes-summary.md index c6c57bc..0dbe664 100644 --- a/docs/changes-summary.md +++ b/docs/changes-summary.md @@ -97,8 +97,7 @@ Archivos clave: - Se añadieron overloads de rutas para construir keys con `userId`. - Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`. - Los eventos de workflows (sourceKey) se publican con la key prefijada. -- El borrado de playlist borra la key original prefijada. -- El borrado de track (`DeleteTrackActivity`) borra originales usando el `userId` presente en `DeleteTrackArguments`. +- El borrado de playlist y track usan keys prefijadas dentro de sus workflows de delete. Archivos: - `Application/Configuration/PlayListConfiguration.cs` @@ -109,7 +108,7 @@ Archivos: - `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` - `Application/Tracks/Handler/CreateTrackCommandHandler.cs` - `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` -- `Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs` + --- @@ -163,3 +162,33 @@ Archivo: ## 10) Estado final - `dotnet build MusifyBackend.slnx -c Release` ✅ + +--- + +## 11) Deletes unificados con Workflows (RoutingSlip) + +**Objetivo**: homogeneizar deletes de Track/PlayList usando Courier RoutingSlip con actividades granulares e idempotentes. + +### Cambios +- Se añadió `LifeCycleStatus` (`Active`/`Removing`) a `Track` y `PlayList` y se generó migración. +- Track delete: + - Se eliminó la activity monolítica y el slip ahora encadena: marcar como removing → borrar N ficheros (condicional) → borrar en DB. +- PlayList delete: + - Se añadió `DeletePlayListEvent` + consumer + routing slip builder + activities para marcar como removing, borrar pictures (si no son preset) y borrar en DB. +- Se endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado. +- Se eliminaron `RemoveFileEvent`/`RemoveFileConsumer` por quedar sin usos. + +Archivos clave: +- `Domain/Entities/LifeCycleStatus.cs` +- `Domain/Entities/Track.cs` +- `Domain/Entities/PlayList.cs` +- `Application/Events/DeletePlayListEvent.cs` +- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` +- `Application/Tracks/Handler/DeleteTrackCommandHandler.cs` +- `Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs` +- `Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs` +- `Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs` +- `Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs` +- `Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs` +- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs` +- `Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs` diff --git a/docs/delete-workflows-plan.md b/docs/delete-workflows-plan.md index 5b92b0c..22205d7 100644 --- a/docs/delete-workflows-plan.md +++ b/docs/delete-workflows-plan.md @@ -4,9 +4,9 @@ Actualmente hay dos problemas de diseño en los deletes: -**PlayLists:** el borrado dispara múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego elimina la entidad en DB. Sin workflow, sin trazabilidad. +**PlayLists:** el borrado disparaba múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego eliminaba la entidad en DB. Sin workflow, sin trazabilidad. -**Tracks:** ya existe un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hace **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales. +**Tracks:** existía un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hacía **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales. La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes. @@ -42,30 +42,54 @@ No objetivos (por ahora): ## Diseño propuesto +## Implementación final (nombres reales) + +Este plan se implementó con los siguientes nombres finales (para facilitar traceo/debug): + +### Colas (consumers) +- `delete-track` (`DeleteTrackConsumer`) +- `delete-playList` (`DeletePlayListConsumer`) + +### Execute endpoints (activities) +MassTransit crea las colas de execute como `_execute`. + +- `remove-file-from-bucket` → cola `remove-file-from-bucket_execute` +- `mark-track-as-removing` → cola `mark-track-as-removing_execute` +- `delete-track-from-db` → cola `delete-track-from-db_execute` +- `mark-playList-as-removing` → cola `mark-playList-as-removing_execute` +- `delete-playList-from-db` → cola `delete-playList-from-db_execute` + +### Estado de borrado (DB) +- Se introdujo `LifeCycleStatus` en `Track` y `PlayList` con valores `Active` y `Removing`. +- El handler marca `Removing` antes de publicar el evento (y persiste con `SaveChanges`). + +> Nota: se eliminó el enfoque de “estado Removing” implícito en `ProcessingStatus` para deletes. El tracking de delete queda en `LifeCycleStatus`. + ### Actividades compartidas / reutilizables Ya existe (y se mantiene): - `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip. -Se añaden actividades de DB compartibles por aggregate: -- `MarkAsRemovingActivity` — cambia estado de la entidad a `Removing` y persiste. (O una por aggregate si la lógica difiere.) +Se añaden actividades de DB por aggregate: +- `MarkTrackAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste. - `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones). +- `MarkPlayListAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste. - `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones). ### Estructura del RoutingSlip para Track ``` DeleteTrackEvent - └─► DeleteTrackConsumer - └─► DeleteTrackRoutingSlipBuilder - ├─ MarkTrackAsRemovingActivity (cambia estado → Removing, persiste) - ├─ RemoveFileFromBucketActivity (original picture) - ├─ RemoveFileFromBucketActivity (small picture) ← solo si IsPicturesProcessed - ├─ RemoveFileFromBucketActivity (medium picture) ← solo si IsPicturesProcessed - ├─ RemoveFileFromBucketActivity (large picture) ← solo si IsPicturesProcessed - ├─ RemoveFileFromBucketActivity (original audio) - ├─ RemoveFileFromBucketActivity (processed audio) ← solo si IsAudioProcessed - └─ DeleteTrackFromDbActivity (Remove() + SaveChanges) + └─► DeleteTrackConsumer (queue: `delete-track`) + └─► DeleteTrackRoutingSlipBuilder.BuildAsync(...) + ├─ MarkTrackAsRemovingActivity (endpoint: `mark-track-as-removing`) + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original picture + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small (si IsPicturesProcessed) + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium (si IsPicturesProcessed) + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large (si IsPicturesProcessed) + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original audio + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — processed audio (si IsAudioProcessed) + └─ DeleteTrackFromDbActivity (endpoint: `delete-track-from-db`) ``` > El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue). @@ -74,14 +98,14 @@ DeleteTrackEvent ``` DeletePlayListEvent - └─► DeletePlayListConsumer - └─► DeletePlayListRoutingSlipBuilder - ├─ MarkPlayListAsRemovingActivity (cambia estado → Removing, persiste) - ├─ RemoveFileFromBucketActivity (original — uploads/{userId}/...) ← solo si no es preset - ├─ RemoveFileFromBucketActivity (small) ← solo si no es preset - ├─ RemoveFileFromBucketActivity (medium) ← solo si no es preset - ├─ RemoveFileFromBucketActivity (large) ← solo si no es preset - └─ DeletePlayListFromDbActivity (Remove() + SaveChanges, cascade elimina PlayListHasTrack) + └─► DeletePlayListConsumer (queue: `delete-playList`) + └─► DeletePlayListRoutingSlipBuilder.BuildAsync(...) + ├─ MarkPlayListAsRemovingActivity (endpoint: `mark-playList-as-removing`) + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original (uploads/{userId}/...) ← solo si no es preset + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small ← solo si no es preset + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium ← solo si no es preset + ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large ← solo si no es preset + └─ DeletePlayListFromDbActivity (endpoint: `delete-playList-from-db`) ``` ### Handler de comando (ambos aggregates) @@ -93,14 +117,20 @@ database.PlayLists.Remove(playlist); await database.SaveChangesAsync(); // DESPUÉS +playlist.LifeCycleStatus = LifeCycleStatus.Removing; await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId)); -// Nada más. El workflow se encarga de todo. +await database.SaveChangesAsync(); +// El workflow se encarga de borrar ficheros y eliminar en DB. ``` ```csharp // DeleteTrackCommandHandler — ANTES // (ya publicaba DeleteTrackEvent, se mantiene igual) + +// DESPUÉS +track.LifeCycleStatus = LifeCycleStatus.Removing; await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId)); +await database.SaveChangesAsync(); // La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica. ``` @@ -120,7 +150,7 @@ await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId ## Idempotencia / reintentos - RoutingSlip + reintentos de endpoint ya configurados en MassTransit. -- Operaciones S3: idempotentes por diseño. Si el key no existe → log `Information` y `Completed()` (no throw). +- Operaciones S3: idempotentes por diseño. Si el key no existe (`NoSuchKey`/`NotFound`) → log `Information` y `Completed()` (no throw). - DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora). - Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip. From 0ac921f7489bbe20304f7f4b44c61140d4b020e8 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 14 May 2026 18:48:00 +0200 Subject: [PATCH 137/172] Update plans --- docs/presigned-upload-hardening.md | 216 +++++++++++++++++++++++++---- docs/tech-stack.md | 186 +++++++++++++++++++++++++ 2 files changed, 378 insertions(+), 24 deletions(-) create mode 100644 docs/tech-stack.md diff --git a/docs/presigned-upload-hardening.md b/docs/presigned-upload-hardening.md index a31b247..f9159b8 100644 --- a/docs/presigned-upload-hardening.md +++ b/docs/presigned-upload-hardening.md @@ -3,14 +3,14 @@ ## Contexto Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando `pictureName/audioName`. -Esto mejora latencia y descarga la API, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”). +Esto mejora latencia y descarga el host/servicio, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”). ## Qué pasa hoy si el usuario spamea - Se generan muchas URLs pre-firmadas (costo bajo pero no cero). - El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB. - El impacto principal es: - coste/uso de S3 (storage + requests) - - posible saturación del endpoint de generación de URLs + - posible saturación del componente de generación de URLs ## Objetivo Reducir abuso y coste, manteniendo: @@ -18,22 +18,54 @@ Reducir abuso y coste, manteniendo: - backend sin streams - workflows asíncronos + MassTransit Outbox +## Qué significa “sólido” aquí +Como el upload es directo a S3 (o compatible), el backend no puede garantizar “single-use” del upload en el lado del storage mientras una URL sea válida. Por tanto, el objetivo realista es: + +- Asegurar que **cualquier abuso solo pueda impactar un prefijo temporal** barato de limpiar. +- Poner límites duros (cuotas) en DB para que el coste tenga un techo. +- No aceptar referencias a objetos no válidos (verificación antes de consumir). +- Evitar pérdida de datos en reemplazos (no borrar la versión previa hasta éxito). + +## Principio de configuración (no hardcode) +Todas las rutas/prefijos/TTLs/límites deben ser **configurables**. + +Regla: +- En `Application`/`Infrastructure` no debe haber strings hardcodeadas de keys S3 (p.ej. `temp/`, `uploads/`, `PlayLists/`, `Tracks/`). +- Las rutas deben centralizarse en opciones/config (p.ej. `StorageRoutes`, `PlayListRoutes`, `TrackRoutes`, o similar), de forma que cambiar la convención de keys no requiera tocar lógica. + +Los ejemplos de este documento son **convenciones sugeridas** y deben leerse como plantillas parametrizadas. + +## Invariantes de seguridad (reglas que no se rompen) +- El cliente solo puede escribir en rutas temporales bajo `{TempRootPrefix}/...` (o el prefijo temporal equivalente). La ruta final **nunca** recibe credenciales pre-firmadas. +- Cada intent emite un `Key` **nuevo** (GUID). No hay keys “estables” para reusar. +- Una entidad solo apunta a un `pictureName/audioName` si su intent asociado se valida y se consume. +- Reemplazos: el borrado de la versión anterior ocurre **al final** del workflow, tras actualizar DB. + +## Prioridad (coste alto) +Si el coste es el principal riesgo, el orden recomendado es: +1) **Cuotas duras en DB** (intents activos + bytes activos) + job de expiración. +2) Prefijo temporal + cleanup por prefijo (lifecycle si existe, y job como fallback). +3) Validación de objeto (HEAD) antes de consumir. +4) (Opcional) Migrar a pre-signed POST si el backend de storage lo soporta bien. + +## Decisiones validadas (no tocaría) +- El orden de prioridad (cuotas → prefijo temporal → HEAD → POST) es el más sensato cuando el **coste** es el riesgo principal. +- La advertencia sobre S3-compatible / SeaweedFS es importante: el diseño debe ser seguro sin depender de features avanzadas. +- El principio de no hardcodear rutas y centralizar prefijos/convenciones en config es coherente con el estilo del repo. +- La semántica de errores debe permanecer separada del mapeo a códigos/respuestas (responsabilidad del host/presentación). +- En reemplazos, no borrar la versión anterior hasta éxito del workflow es clave para resiliencia con Courier. + ## Mitigaciones recomendadas (pueden coexistir) -### 1) Rate limiting por usuario/IP (primera línea de defensa) -Aplicar limitación en el host/API (no en `Application`). Ejemplos: -- `N` requests/min por `UserId` -- opcional: `M` requests/min por IP - -Ventajas: -- Corta abuso sin tocar DB/S3. -- No cambia dominio. - -Nota: requiere acceso al proyecto host (controllers/minimal API) para aplicarlo. - -### 2) “Upload intents” en DB (control y conciliación) +### 1) “Upload intents” en DB (control y conciliación) Persistir una intención de upload cuando se genera un pre-signed URL. +> Importante: **no usar el nombre original del fichero** como `pictureName/audioName`. La key debe usar un **identificador generado** (p.ej. GUID) para evitar colisiones, inputs “raros” y facilitar cleanup. + +Sugerencia práctica: +- `objectName = {guid}.{ext}` (o directamente `{guid}` si la extensión no aporta valor) +- Guardar `originalFileName` solo como metadata opcional (si se necesita para UI), pero **nunca** como key. + Modelo sugerido (mínimo): - `UploadIntent` - `Id` @@ -41,19 +73,67 @@ Modelo sugerido (mínimo): - `Bucket` - `Key` - `ContentType` + - `ExpectedSizeBytes` (opcional; si el caller conoce el tamaño) - `ExpiresAt` - `Status`: `Issued | Consumed | Expired` - `CreatedAt` +Opcional (útil para auditoría/UX): +- `OriginalFileName` +- `Purpose` (`TrackPicture | TrackAudio | PlayListPicture`) + Reglas: - Al pedir URL (Request*Upload*): - insertar intent `Issued` - - imponer límite por usuario: p.ej. `MaxActiveIntentsPerUser = 5` - - si supera el límite: devolver `429 Too Many Requests` (o `Conflict`) según convención del API + - imponer límites por usuario (ver “Cuotas”) + - si supera el límite: fallar con un error semántico (p.ej. `UploadIntentLimitExceeded`) - Al crear/actualizar entidad (Create*/Update* V2): - validar que el `pictureName/audioName` recibido corresponde a un intent `Issued` del mismo `UserId` y no expirado + - validar que el objeto existe en S3 (HEAD) y que tamaño / `Content-Type` cumplen la policy del backend - marcar intent como `Consumed` +#### Cuotas (anti-cost) 🔴 Alta +Además del conteo de intents, imponer **cuotas por bytes** para que el coste tenga techo. + +Recomendación mínima: +- `MaxActiveUploadIntentsPerUser` (número) +- `MaxActiveUploadBytesPerUser` (bytes) — suma de objetos temporales “en vuelo” por usuario + +Cómo aproximarlo sin streams: +- Guardar `ExpectedSizeBytes` en el intent (si está disponible). +- Si no se dispone del tamaño, usar un valor conservador por `Purpose` (p.ej. imagen <= X, audio <= Y) y validarlo en HEAD antes de consumir. + +> Nota: si el caller puede mentir sobre tamaño, no confíes ciegamente en `ExpectedSizeBytes`; úsalo como guardrail y valida al consumir. + +#### Semántica de errores (intent inválido) 🟡 Media +Definir errores semánticos y dejar el mapeo al “caller” (host/presentación): +- `UploadIntentLimitExceeded` +- `UploadIntentNotFoundOrNotAccessible` (si se quiere no filtrar existencia) +- `UploadIntentAlreadyConsumed` +- `UploadIntentExpired` +- `UploadObjectNotFound` +- `UploadObjectPolicyViolation` (tamaño, `Content-Type`, extensión, etc.) + +#### Nota sobre race conditions en el límite 🟡 Media +El check “`count(Issued) < Max`” es vulnerable a concurrencia (dos requests paralelas pueden pasar el check y crear 2 intents extra). + +Mitigaciones (de menor a mayor robustez): +- Ejecutar **check + insert** en la **misma transacción** con aislamiento fuerte (p.ej. `SERIALIZABLE`). +- Usar un mecanismo de lock por usuario (p.ej. advisory lock en PostgreSQL) durante la emisión. +- Modelar el límite con una tabla/contador por usuario y aplicar **update condicional** (estilo “compare-and-swap”). + +#### Job de expiración de intents 🔴 Alta +Aunque `ExpiresAt` esté en DB, hace falta un mecanismo que marque y limpie intents caducados: +- Un job periódico (worker/cron) que: + - marque intents `Issued` con `ExpiresAt < now` como `Expired` + - opcionalmente encole el borrado del objeto S3 (si existe) + +Si se aplica lifecycle en S3 (ver sección 2), el job puede ser “solo DB” (marcar `Expired`) y dejar el borrado real a S3. + +Adicionalmente, este job es un buen sitio para: +- borrar intents `Expired` antiguos (retención de auditoría configurable) +- emitir métricas/contadores (intents expirados por usuario) + Ventajas: - Previene spam con reglas duras y auditables. - Permite cleanup de huérfanos con certeza. @@ -61,7 +141,7 @@ Ventajas: Coste: - entidad + migración + lógica de validación. -### 3) Limitar el scope del pre-signed URL (reducción de daño) +### 2) Limitar el scope del pre-signed URL (reducción de daño) Aplicar restricciones conservadoras: - Reducir TTL: - `ExpiresInSeconds` más bajo (p.ej. 60–120s en vez de 600s) @@ -69,27 +149,115 @@ Aplicar restricciones conservadoras: - `ContentType` permitido - extensiones permitidas (`FileType`) - Usar keys temporales por usuario: - - `PlayLists/TempUploads/{userId}/{guid}.{ext}` - - `Tracks/TempUploads/{userId}/{guid}.{ext}` + - Opción recomendada (facilita lifecycle por prefijo): + - `{TempRootPrefix}/{userId}/playlists/{guid}.{ext}` + - `{TempRootPrefix}/{userId}/tracks/{guid}.{ext}` + - Alternativa (si se quiere mantener todo bajo `uploads/{userId}`): + - `{UploadsRootPrefix}/{userId}/PlayLists/TempUploads/{guid}.{ext}` + - `{UploadsRootPrefix}/{userId}/Tracks/TempUploads/{guid}.{ext}` Luego, en el workflow/worker: -- mover/copiar de `TempUploads/...` a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`) +- mover/copiar desde la ruta temporal (p.ej. `temp/...` o `.../TempUploads/...`) a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`) - borrar el objeto temporal Ventajas: -- Cleanup masivo sencillo (por prefijo `TempUploads/{userId}`) +- Cleanup masivo sencillo (por prefijo `{TempRootPrefix}/`) - Reduce el impacto de huérfanos. +#### S3 Lifecycle Rule en `TempUploads` 🔴 Alta +Configurar una **Lifecycle Rule** en el bucket para auto-expirar objetos bajo el prefijo de temporales, por ejemplo: +- Prefijo: `{TempRootPrefix}/` +- Expiración: 1–7 días (según tolerancia a reintentos) + +Nota: las reglas de lifecycle por **prefijo** no soportan comodines en medio (p.ej. `uploads/*/...`). +Si se usa la alternativa `uploads/{userId}/.../TempUploads/...`, para filtrar correctamente se recomienda: +- cambiar la convención de key para que el prefijo sea común (p.ej. `{TempRootPrefix}/...`), o +- usar **tags** en los objetos (p.ej. `temporary=true`) y filtrar la lifecycle rule por tag (si se puede firmar el tagging en el upload). + +Esto evita que el coste de huérfanos crezca indefinidamente incluso si el job de cleanup falla. + +#### Cleanup backend (fallback) 🔴 Alta +No dependas solo de lifecycle del storage (en S3 compatibles puede variar). Añadir un job de cleanup que: +- liste por prefijo temporal `{TempRootPrefix}/` +- borre objetos con edad > `TempUploadsRetentionDays` + +Este job debe ser: +- incremental (paginación) +- tolerante a errores (reintentos) +- con límites de throughput (para no auto-generar costes por borrar en masa) + +#### Restricción de `Content-Type` en S3 🟡 Media +Si el objetivo es enforcement “duro” a nivel de S3 (no solo validación posterior), **pre-signed POST** suele ser más robusto que PUT. + +**pre-signed PUT** (lo actual en este repo): +- Permite fijar `Content-Type` si se pre-firma incluyendo ese header. +- Es más limitado para imponer condiciones ricas (tamaño, tags, etc.). +- Recomendación: combinar con validación posterior (HEAD) antes de consumir el intent. + +**pre-signed POST** (recomendado si buscas más control): +- Permite policy conditions como: + - `content-length-range` (tamaño máximo) + - `eq`/`starts-with` para `Content-Type` + - key/prefijo (el cliente no elige keys fuera del scope) + - headers requeridos (p.ej. SSE) + - opcional: tagging (p.ej. `temporary=true`) para lifecycle por tag + +Trade-off: el cliente sube con `multipart/form-data` (POST), pero el backend sigue sin manejar streams. + +#### Nota S3-compatible / SeaweedFS 🟡 Media +No todos los backends S3 compatibles implementan al 100%: +- policy conditions de POST +- tagging en upload +- respuestas/errores idénticos a AWS + +Recomendación: +- Diseñar el sistema para ser seguro **sin depender** de condiciones avanzadas (keys únicas + prefijo temporal + cuotas + verificación HEAD). +- Tratar POST/condiciones como una mejora opcional, tras validar compatibilidad del backend. + +#### Reemplazos: no borrar originales antiguos hasta éxito ✅ +Al actualizar una imagen/audio existente: +- Subir el nuevo binario a la ruta temporal (p.ej. `{TempRootPrefix}/.../{newGuid}`). +- Procesar (resize/transcode) y subir a rutas finales con nombres nuevos. +- Actualizar la entidad en DB para apuntar a los nuevos `*PictureName/*AudioFolderName`. +- **Solo al final** borrar los objetos antiguos (original + sizes / audio procesado), cuando el workflow haya completado. + +Esto evita perder la versión anterior si el worker falla a mitad (punto crítico para resiliencia). + +## Validación del objeto antes de consumir (HEAD) 🔴 Alta +Antes de consumir un intent y actualizar DB: +- `HEAD Bucket/Key` +- validar `Content-Type` esperado (según `Purpose`) +- validar tamaño máximo (`MaxUploadBytes` o por propósito) +- (opcional) validar extensión inferida de `Key` si se usa + +Si falla la validación: +- no consumir el intent +- marcarlo como violación o dejarlo expirar +- el cleanup se encarga del objeto + ## Orden de implementación sugerido -1) Rate limiting en API (rápido + mayor impacto) -2) Upload intents en DB (límite de activos + validación al crear/actualizar) -3) Reducir TTL y mover a rutas temporales (`TempUploads/...`) + cleanup +1) Upload intents en DB (límite de activos + validación al crear/actualizar) +2) Reducir TTL y mover a rutas temporales + S3 lifecycle + cleanup +3) Si se requiere enforcement fuerte: migrar a pre-signed POST + policy conditions ## Parámetros recomendados (config) - `UploadUrlExpiresInSeconds` (default: 120) - `MaxActiveUploadIntentsPerUser` (default: 5) +- `MaxActiveUploadBytesPerUser` (default sugerido: depende de coste; p.ej. 200MB) - `AllowedPictureExtensions` / `AllowedAudioExtensions` - `AllowedContentTypes` (lista blanca) +- `TempUploadsRetentionDays` (default: 3) +- `PresignedUploadMethod` (default sugerido: `POST` si se quiere enforcement fuerte; `PUT` si se prioriza simplicidad) +- `MaxUploadBytes` (límite máximo de tamaño por upload) + +Jobs (config): +- `UploadIntentsExpirationJobIntervalSeconds` +- `TempUploadsCleanupJobIntervalSeconds` +- `ExpiredIntentsRetentionDays` + +Rutas/prefijos (ejemplos): +- `TempRootPrefix` (default sugerido: `temp`) +- `UploadsRootPrefix` (default sugerido: `uploads`) ## Notas - Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos. diff --git a/docs/tech-stack.md b/docs/tech-stack.md new file mode 100644 index 0000000..9243f62 --- /dev/null +++ b/docs/tech-stack.md @@ -0,0 +1,186 @@ +# Tech stack y arquitectura (MusifyBackend) + +Este documento describe **qué tecnologías usa este repo** y **cómo encajan** (capas, workers, mensajería, storage, observabilidad). Está pensado para alguien que llega de cero o para una IA que necesita contexto. + +## Foto rápida +- Runtime: **.NET 10** (C#) +- Estilo: **Clean Architecture** (capas `Domain` / `Application` / `Infrastructure`) +- Casos de uso: **CQRS** con **DispatchR** (similar a MediatR) +- Persistencia: **Entity Framework Core** + **PostgreSQL** (provider `Npgsql`) +- Workers / mensajería: **MassTransit** + **RabbitMQ** +- Orquestación de workflows: **MassTransit Courier / RoutingSlip** (Activities) +- Storage de objetos: **SeaweedFS** expuesto como **S3-compatible** (vía AWS SDK `IAmazonS3`) +- Observabilidad: **OpenTelemetry** (traces + metrics, export OTLP) +- Procesado multimedia: + - Imágenes: **SixLabors.ImageSharp** (resize, WebP) + - Audio: **ffmpeg** (transcode a DASH) + +## Estructura del repo (capas) + +### `Domain/` (núcleo de negocio) +- Entidades puras (sin dependencias de infraestructura). +- Ejemplos: `Track`, `PlayList`, `Upload`, etc. +- Estados de negocio relevantes: + - `ProcessingStatus` (procesado en curso/finalizado, etc.). + - `LifeCycleStatus` (p.ej. `Active`, `Removing`) para deletes asíncronos. + +### `Application/` (casos de uso + contratos) +- Contiene comandos/queries/handlers por feature. +- CQRS: + - `Commands/` y `Queries/` por feature (`PlayLists`, `Tracks`, `Users`, ...). + - `Handlers/` ejecutan la lógica de aplicación (coordinar DB + eventos), pero **no implementan** detalles de infraestructura. +- Contratos con infraestructura: `Application/Abstractions/Infrastructure/*` + - `IDatabase` (abstracto de EF DbContext) + - `IEventBus` (publicación de eventos) + - `IStorageService`, `IPictureService`, `IAudioTranscoderService`, etc. +- Eventos de integración: `Application/Events/*` + - Se publican desde `Application` y se consumen en `Infrastructure`. + +### `Infrastructure/` (implementaciones + integraciones) +- Implementa las abstracciones de `Application`. +- Contiene: + - EF Core DbContext y migraciones + - MassTransit (consumers, activities, routing slips, outbox) + - Integración S3 (SeaweedFS) mediante AWS SDK + - Observabilidad OpenTelemetry + - Servicios de procesado (ImageSharp/ffmpeg) + +## CQRS con DispatchR +- Los “use cases” se modelan como: + - **Commands**: mutaciones (crear/actualizar/borrar). + - **Queries**: lecturas. +- Los handlers viven en `Application/*/Handlers/` y dependen de: + - `IDatabase` para persistencia (sin acoplarse al DbContext concreto). + - `IEventBus` para emitir eventos cuando hay trabajo asíncrono. + +## Persistencia (EF Core + PostgreSQL) +- DbContext: `Infrastructure/Persistence/Database.cs` + - Usa `UseNpgsql(connectionString)`. + - Incluye tablas de MassTransit Outbox/Inbox (ver sección de mensajería). +- DI: `Infrastructure/Persistence/DatabaseDependencyInjection.cs` +- Config: `Infrastructure/Configuration/DatabaseConfiguration.cs` (sección `Database`) + - `ConnectionString` + +## Mensajería y workers (MassTransit + RabbitMQ) + +### Qué se usa +- Broker: RabbitMQ (`MassTransit.RabbitMQ`). +- MassTransit está configurado en dos modos: + 1) **Cliente** (publica eventos): `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` → `AddMassTransitClient()` + 2) **Workers** (consumen y ejecutan activities): `AddMassTransitConsumers()` + +### Outbox / Inbox (robustez) +- Se usa **EntityFramework Outbox** (`AddEntityFrameworkOutbox()`) con: + - `UsePostgres()` + - `UseBusOutbox()` + - `QueryDelay = 1s` + - `DuplicateDetectionWindow = 30m` +- En el DbContext se registran entidades de MassTransit: + - `AddInboxStateEntity()` + - `AddOutboxMessageEntity()` + - `AddOutboxStateEntity()` + +Idea clave: +- La outbox asegura que, tras confirmar la transacción de DB, los mensajes/eventos pendientes se publican de forma fiable. + +### Courier / RoutingSlip (workflows asíncronos) +Para operaciones multi-paso (p.ej. imagen/audio y deletes), se usa **MassTransit Courier**: +- Un “RoutingSlip” es una lista de pasos. +- Cada paso es una **Activity** (con args + log opcional). +- Existen dos tipos que aparecen en este repo: + - `ExecuteActivity` (endpoint `_execute`), típicamente acciones atómicas. + - `Activity` (con compensación/log), típico para pasos de workflow. + +Dónde mirar: +- Builders: `Infrastructure/MassTransit/RoutingSlip/Builders/*` +- Activities: `Infrastructure/MassTransit/Activities/*` +- Consumers (punto de entrada del workflow): `Infrastructure/MassTransit/Consumers/*` +- Registro de endpoints: `Infrastructure/MassTransit/DependencyInjection/*` + +Ejemplos de workflows presentes: +- Procesado de imagen (resize + upload + update DB) +- Procesado de audio (transcode DASH + upload + update DB) +- Deletes asíncronos (marcar “removing”, borrar objetos, borrar filas) + +### Config de MassTransit +- `Infrastructure/Configuration/MassTransitConfiguration.cs` (sección `MassTransit`) + - `Address` + - `Username` + - `Password` + +## Storage de objetos (SeaweedFS como S3-compatible) + +### Qué se usa +- SDK: `Amazon.S3` (`IAmazonS3`). +- Implementación: `Infrastructure/Services/StorageService.cs` (usa `IAmazonS3`). +- DI: `Infrastructure/Services/ServicesDependencyInjection.cs` → `AddStorageService()` + +### Config +- `Infrastructure/Configuration/InfrastructureStorageConfiguration.cs` (sección `InfrastructureStorage`) + - `Address` (ServiceURL) + - `AccessKey` / `SecretAccessKey` + - `ForcePathStyle` (útil en S3-compatibles) + - `UseHttp` + +Notas prácticas (S3-compatible): +- Algunas features avanzadas de AWS S3 pueden no comportarse igual (depende de SeaweedFS). Diseñar workflows para que sean correctos con el subconjunto básico: PUT/GET/HEAD/LIST/DELETE y paths bien definidos. + +## Procesado multimedia + +### Imágenes +- Librería: `SixLabors.ImageSharp` +- Servicio: `Infrastructure/Services/PictureService.cs` +- Contrato: `Application/Abstractions/Infrastructure/IPictureService.cs` +- Patrón: workers descargan/transforman/suben; la app coordina por eventos. + +### Audio +- Herramienta: `ffmpeg` (binario externo) +- Config: `Infrastructure/Configuration/AudioTranscoderConfiguration.cs` (sección `AudioTranscoder`) + - Timeout, codec/bitrate/channels/sample-rate, segmentación DASH, etc. +- Servicio: `Infrastructure/Services/AudioTranscoderService.cs` +- Contrato: `Application/Abstractions/Infrastructure/IAudioTranscoderService.cs` + +### Directorio temporal (workers) +- Config: `Infrastructure/Configuration/WorkerConfiguration.cs` (sección `Workers`) + - `Routes.TemporaryFilesDirectory` + +## Observabilidad (OpenTelemetry) +- DI: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs` +- Config: `Infrastructure/Configuration/OpenTelemetryConfiguration.cs` (sección `OpenTelemetry`) + - `OtlpEndpoint` + +Qué instrumenta: +- Tracing: + - `MassTransit` + - ASP.NET Core + - HttpClient + - EF Core + - Exporter OTLP +- Métricas: + - `MassTransit` + - ASP.NET Core + - HttpClient + - Runtime + Process + - Exporter OTLP + +## Identidad (Keycloak) +- El repo asume integración con **Keycloak** (ver `AGENTS.md` y nombres como `GetUserByKeycloakQuery`). +- La capa `Application` trata el “Keycloak Id” como identificador de usuario en algunos casos de uso. +- La integración concreta (tokens, validación, etc.) puede vivir fuera de estas 3 capas o en otro proyecto/host. + +## Glosario rápido +- **Command / Query**: operación de escritura / lectura. +- **Handler**: clase que ejecuta el caso de uso (CQRS). +- **EventBus**: abstracción para publicar eventos (MassTransit por debajo). +- **Consumer**: “listener” de mensajes en una cola. +- **RoutingSlip**: workflow compuesto por Activities. +- **Activity**: paso del workflow (puede tener log/compensación). +- **Outbox**: patrón para publicar eventos de forma fiable tras commit de DB. + +## “Dónde empiezo a leer” (ruta recomendada) +1) Arquitectura general: `AGENTS.md` +2) Casos de uso: `Application/*/Commands`, `Application/*/Queries` y sus `Handlers/` +3) Persistencia: `Infrastructure/Persistence/Database.cs` + migraciones +4) Mensajería/workflows: `Infrastructure/MassTransit/*` +5) Integraciones: `Infrastructure/Services/*` (S3, imagen, audio) +6) Observabilidad: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs` From 91ec85760e4999e54113e895a4648414f97c66a8 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 14 May 2026 19:53:26 +0200 Subject: [PATCH 138/172] Generated response --- .../Abstractions/Infrastructure/IDatabase.cs | 4 + .../Infrastructure/IDatabaseTransaction.cs | 7 + .../Infrastructure/IStorageService.cs | 6 + .../Infrastructure/ObjectMetadata.cs | 4 + .../Configuration/PlayListConfiguration.cs | 4 + .../Configuration/TrackConfiguration.cs | 7 + .../UploadIntentConfiguration.cs | 52 ++ .../Commands/CreatePlayListCommand.cs | 6 +- .../RequestPlayListPictureUploadCommand.cs | 3 +- .../Commands/UpdatePlayListCommand.cs | 6 +- .../Handlers/CreatePlayListCommandHandler.cs | 41 +- ...uestPlayListPictureUploadCommandHandler.cs | 76 ++- .../Handlers/UpdatePlayListCommandHandler.cs | 29 +- .../PlayListPictureUploadResponse.cs | 1 + .../Tracks/Commands/CreateTrackCommand.cs | 8 +- .../Commands/RequestTrackUploadUrlsCommand.cs | 4 +- .../Handler/CreateTrackCommandHandler.cs | 44 +- .../RequestTrackUploadUrlsCommandHandler.cs | 114 ++-- .../Responses/TrackUploadUrlsResponse.cs | 2 + .../UploadIntents/IntentValidationResult.cs | 13 + .../UploadIntents/UploadIntentHelpers.cs | 89 +++ Domain/Entities/UploadIntent.cs | 46 ++ Domain/Entities/UploadIntentPurpose.cs | 9 + Domain/Entities/UploadIntentStatus.cs | 9 + Infrastructure/Jobs/TempUploadsCleanupJob.cs | 70 +++ .../Jobs/UploadIntentExpirationJob.cs | 75 +++ Infrastructure/Persistence/Database.cs | 24 +- ...514172047_AddUploadIntentTable.Designer.cs | 580 ++++++++++++++++++ .../20260514172047_AddUploadIntentTable.cs | 54 ++ .../Migrations/DatabaseModelSnapshot.cs | 62 ++ .../Services/ServicesDependencyInjection.cs | 27 + Infrastructure/Services/StorageService.cs | 40 ++ docs/changes-summary.md | 141 ++++- 33 files changed, 1555 insertions(+), 102 deletions(-) create mode 100644 Application/Abstractions/Infrastructure/IDatabaseTransaction.cs create mode 100644 Application/Abstractions/Infrastructure/ObjectMetadata.cs create mode 100644 Application/Configuration/UploadIntentConfiguration.cs create mode 100644 Application/UploadIntents/IntentValidationResult.cs create mode 100644 Application/UploadIntents/UploadIntentHelpers.cs create mode 100644 Domain/Entities/UploadIntent.cs create mode 100644 Domain/Entities/UploadIntentPurpose.cs create mode 100644 Domain/Entities/UploadIntentStatus.cs create mode 100644 Infrastructure/Jobs/TempUploadsCleanupJob.cs create mode 100644 Infrastructure/Jobs/UploadIntentExpirationJob.cs create mode 100644 Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs diff --git a/Application/Abstractions/Infrastructure/IDatabase.cs b/Application/Abstractions/Infrastructure/IDatabase.cs index ea81d4e..9d87904 100644 --- a/Application/Abstractions/Infrastructure/IDatabase.cs +++ b/Application/Abstractions/Infrastructure/IDatabase.cs @@ -17,6 +17,10 @@ namespace Musify.Application.Abstractions.Infrastructure DbSet Uploads { get; } + DbSet UploadIntents { get; } + + Task BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken); + Task SaveChangesAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Abstractions/Infrastructure/IDatabaseTransaction.cs b/Application/Abstractions/Infrastructure/IDatabaseTransaction.cs new file mode 100644 index 0000000..9e5cddc --- /dev/null +++ b/Application/Abstractions/Infrastructure/IDatabaseTransaction.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Abstractions.Infrastructure +{ + public interface IDatabaseTransaction : IAsyncDisposable + { + Task CommitAsync(CancellationToken cancellationToken = default); + } +} diff --git a/Application/Abstractions/Infrastructure/IStorageService.cs b/Application/Abstractions/Infrastructure/IStorageService.cs index 26af204..cb1c49d 100644 --- a/Application/Abstractions/Infrastructure/IStorageService.cs +++ b/Application/Abstractions/Infrastructure/IStorageService.cs @@ -4,6 +4,9 @@ { Task GetFileAsync(string bucket, string key, CancellationToken cancellationToken); + // Returns null if the object does not exist + Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken); + Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); Task GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, CancellationToken cancellationToken); @@ -20,5 +23,8 @@ CancellationToken cancellationToken); Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken); + + // Returns (key, lastModifiedUtc) for all objects under the given prefix + IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)> ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Abstractions/Infrastructure/ObjectMetadata.cs b/Application/Abstractions/Infrastructure/ObjectMetadata.cs new file mode 100644 index 0000000..f92904e --- /dev/null +++ b/Application/Abstractions/Infrastructure/ObjectMetadata.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Abstractions.Infrastructure +{ + public record ObjectMetadata(string ContentType, long ContentLength); +} diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 5bd7a37..9b03e7c 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -69,6 +69,10 @@ namespace Musify.Application.Configuration public string BuildLargePicturePath(string pictureName) => CombineKey(ParentFolders, LargePicturesFolder, pictureName); + // Temp path used in Phase 2 for pre-signed upload keys (moved to final on consumption) + public string BuildTempPicturePath(string tempRootPrefix, Guid userId, string objectName) => + CombineKey(tempRootPrefix, userId.ToString(), ParentFolders, objectName); + static string CombineKey(params string[] segments) { return string.Join('/', segments diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index f7b5e76..10e2eec 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -85,6 +85,13 @@ namespace Musify.Application.Configuration public string BuildProcessedAudioPath(string audioName) => CombineKey(ProcessedAudiosPath, audioName); + // Temp paths used in Phase 2 for pre-signed upload keys (moved to final on consumption) + public string BuildTempPicturePath(string tempRootPrefix, Guid userId, string objectName) => + CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); + + public string BuildTempAudioPath(string tempRootPrefix, Guid userId, string objectName) => + CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); + static string CombineKey(params string[] segments) { return string.Join('/', segments diff --git a/Application/Configuration/UploadIntentConfiguration.cs b/Application/Configuration/UploadIntentConfiguration.cs new file mode 100644 index 0000000..d25741f --- /dev/null +++ b/Application/Configuration/UploadIntentConfiguration.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Application.Configuration +{ + public class UploadIntentConfiguration + { + public const string SectionName = "UploadIntent"; + + // Pre-signed URL TTL in seconds + [Range(10, 3600)] + public int UploadUrlExpiresInSeconds { get; set; } = 120; + + // Max concurrent Issued intents per user + [Range(1, 100)] + public int MaxActiveUploadIntentsPerUser { get; set; } = 5; + + // Max total bytes across all Issued intents for a user (default 200MB) + [Range(1, long.MaxValue)] + public long MaxActiveUploadBytesPerUser { get; set; } = 200L * 1024 * 1024; + + // Max size validated via HEAD before consuming an intent (default 100MB) + [Range(1, long.MaxValue)] + public long MaxUploadBytes { get; set; } = 100L * 1024 * 1024; + + // Conservative fallback size used when ExpectedSizeBytes is not provided + [Range(1, long.MaxValue)] + public long DefaultExpectedPictureSizeBytes { get; set; } = 10L * 1024 * 1024; // 10MB + + [Range(1, long.MaxValue)] + public long DefaultExpectedAudioSizeBytes { get; set; } = 100L * 1024 * 1024; // 100MB + + // How often the expiration job runs + [Range(30, 86400)] + public int ExpirationJobIntervalSeconds { get; set; } = 300; + + // How many days to retain Expired intents before deleting the DB row + [Range(0, 365)] + public int ExpiredIntentsRetentionDays { get; set; } = 7; + + // Root S3 prefix for temporary uploads (Phase 2) + [Required, MinLength(1)] + public string TempRootPrefix { get; set; } = "temp"; + + // How many days to retain orphaned temp objects in S3 before the cleanup job deletes them + [Range(1, 30)] + public int TempUploadsRetentionDays { get; set; } = 3; + + // How often the temp-upload cleanup job runs + [Range(60, 86400)] + public int TempCleanupJobIntervalSeconds { get; set; } = 3600; + } +} diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index 29ee783..fa7af4e 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; using Musify.Application.PlayLists.Responses; @@ -8,6 +8,6 @@ namespace Musify.Application.PlayLists.Commands Guid UserId, string Name, string Description, - string? OriginalPictureName) + Guid? PictureIntentId) : ICommand>; -} \ No newline at end of file +} diff --git a/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs b/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs index 33e01f6..fc14a80 100644 --- a/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs +++ b/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs @@ -7,6 +7,7 @@ namespace Musify.Application.PlayLists.Commands public record RequestPlayListPictureUploadCommand( Guid UserId, string FileType, - string ContentType) + string ContentType, + long? ExpectedSizeBytes = null) : ICommand>; } diff --git a/Application/PlayLists/Commands/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayListCommand.cs index 5671782..f6795c2 100644 --- a/Application/PlayLists/Commands/UpdatePlayListCommand.cs +++ b/Application/PlayLists/Commands/UpdatePlayListCommand.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; using Musify.Application.PlayLists.Responses; @@ -9,6 +9,6 @@ namespace Musify.Application.PlayLists.Commands Guid PlayListId, string? NewName, string? NewDescription, - string? NewOriginalPictureName) + Guid? NewPictureIntentId) : ICommand>; -} \ No newline at end of file +} diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 5fb98a0..7ac68aa 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -7,6 +7,7 @@ using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; +using Musify.Application.UploadIntents; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -14,9 +15,11 @@ namespace Musify.Application.PlayLists.Handlers public class CreatePlayListCommandHandler( IEventBus eventBus, IDatabase database, + IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, - PlayListConfiguration playListConfiguration) + PlayListConfiguration playListConfiguration, + UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { public async ValueTask> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) @@ -30,7 +33,35 @@ namespace Musify.Application.PlayLists.Handlers return Result.NotFound($"User {request.UserId} not found"); } - var originalPictureName = request.OriginalPictureName ?? playListConfiguration.Routes.PresetOriginalPicture; + string originalPictureName; + UploadIntent? pictureIntent = null; + + if (request.PictureIntentId.HasValue) + { + var validation = await UploadIntentHelpers.ValidateAndLoadAsync( + database, storageService, uploadIntentConfiguration, + request.PictureIntentId.Value, request.UserId, cancellationToken); + if (!validation.IsSuccess) + return validation.Error!.Value; + + pictureIntent = validation.Intent!; + originalPictureName = pictureIntent.ObjectName; + + var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName); + try + { + await storageService.CopyFileAsync(pictureIntent.Bucket, pictureIntent.Key, pictureIntent.Bucket, finalKey, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to copy playlist picture from temp {TempKey} to final {FinalKey}", pictureIntent.Key, finalKey); + return Result.Error("Failed to move uploaded file to its final location."); + } + } + else + { + originalPictureName = playListConfiguration.Routes.PresetOriginalPicture; + } var playList = new PlayList { @@ -46,8 +77,10 @@ namespace Musify.Application.PlayLists.Handlers await database.PlayLists.AddAsync(playList, cancellationToken); - if (request.OriginalPictureName is not null) + if (pictureIntent is not null) { + pictureIntent.Status = UploadIntentStatus.Consumed; + var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken); if (!publishResult.IsSuccess) return publishResult; @@ -98,4 +131,4 @@ namespace Musify.Application.PlayLists.Handlers } } } -} \ No newline at end of file +} diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index 3f2c589..a4ac2a1 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -6,6 +6,8 @@ using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Configuration; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; +using Musify.Application.UploadIntents; +using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { @@ -14,11 +16,11 @@ namespace Musify.Application.PlayLists.Handlers IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, - PlayListConfiguration playListConfiguration) + PlayListConfiguration playListConfiguration, + UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { static readonly string[] AllowedExtensions = [".webp", ".png", ".jpg", ".jpeg"]; - const int ExpiresInSeconds = 600; public async ValueTask> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) { @@ -31,31 +33,61 @@ namespace Musify.Application.PlayLists.Handlers return Result.NotFound($"User {request.UserId} not found"); } - var extensionResult = ValidateExtension(request.FileType); + var extensionResult = UploadIntentHelpers.ValidateExtension(request.FileType, AllowedExtensions); if (!extensionResult.IsSuccess) return Result.Invalid(extensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); - if (string.IsNullOrWhiteSpace(request.ContentType)) - return Result.Invalid(new ValidationError("ContentType is required")); + var effectiveSizeBytes = request.ExpectedSizeBytes + ?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes; - var pictureName = Guid.NewGuid() + extensionResult.Value; - var key = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureName); + var objectName = Guid.NewGuid() + extensionResult.Value; + + var tempKey = playListConfiguration.Routes.BuildTempPicturePath( + uploadIntentConfiguration.TempRootPrefix, request.UserId, objectName); try { var uploadUrl = await storageService.GetUploadUrlAsync( storageConfiguration.Bucket, - key, + tempKey, request.ContentType, - TimeSpan.FromSeconds(ExpiresInSeconds), + TimeSpan.FromSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds), cancellationToken); + await using var transaction = await database.BeginTransactionAsync( + System.Data.IsolationLevel.Serializable, cancellationToken); + + var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync( + database, uploadIntentConfiguration, logger, + request.UserId, effectiveSizeBytes, 1, cancellationToken); + if (!quotaCheck.IsSuccess) + return quotaCheck; + + var intent = new UploadIntent + { + UserId = request.UserId, + Bucket = storageConfiguration.Bucket, + Key = tempKey, + ObjectName = objectName, + ContentType = request.ContentType, + ExpectedSizeBytes = effectiveSizeBytes, + Purpose = UploadIntentPurpose.PlayListPicture, + ExpiresAt = DateTime.UtcNow.AddSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds), + }; + + await database.UploadIntents.AddAsync(intent, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + + logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (PlayListPicture, temp key: {Key})", intent.Id, request.UserId, tempKey); + return Result.Success(new PlayListPictureUploadResponse( + intent.Id, storageConfiguration.Bucket, - key, - pictureName, + tempKey, + objectName, request.ContentType, - ExpiresInSeconds, + uploadIntentConfiguration.UploadUrlExpiresInSeconds, uploadUrl)); } catch (Exception exception) @@ -64,25 +96,5 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error("Failed to generate upload URL"); } } - - static Result ValidateExtension(string fileType) - { - if (string.IsNullOrWhiteSpace(fileType)) - return Result.Error("FileType is required"); - - var extension = fileType.Trim(); - if (!extension.StartsWith('.')) - extension = "." + extension; - - extension = extension.ToLowerInvariant(); - - if (extension.Contains('/') || extension.Contains('\\')) - return Result.Error("Invalid FileType"); - - if (!AllowedExtensions.Contains(extension)) - return Result.Error($"Unsupported FileType '{extension}'"); - - return Result.Success(extension); - } } } diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 05a4501..fbbe76b 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -7,15 +7,19 @@ using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; +using Musify.Application.UploadIntents; +using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { public class UpdatePlayListCommandHandler( IEventBus eventBus, IDatabase database, + IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, - PlayListConfiguration playListConfiguration) + PlayListConfiguration playListConfiguration, + UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { public async ValueTask> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) @@ -44,9 +48,28 @@ namespace Musify.Application.PlayLists.Handlers playListEntity.Description = request.NewDescription; } - if (request.NewOriginalPictureName is not null) + if (request.NewPictureIntentId.HasValue) { - playListEntity.OriginalPictureName = request.NewOriginalPictureName; + var validation = await UploadIntentHelpers.ValidateAndLoadAsync( + database, storageService, uploadIntentConfiguration, + request.NewPictureIntentId.Value, request.UserId, cancellationToken); + if (!validation.IsSuccess) + return validation.Error!.Value; + + var intent = validation.Intent!; + var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName); + try + { + await storageService.CopyFileAsync(intent.Bucket, intent.Key, intent.Bucket, finalKey, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to copy playlist picture from temp {TempKey} to final {FinalKey}", intent.Key, finalKey); + return Result.Error("Failed to move uploaded file to its final location."); + } + + playListEntity.OriginalPictureName = intent.ObjectName; + intent.Status = UploadIntentStatus.Consumed; var publishResult = await PublishPlayListPictureEvent(playListEntity.UserId, playListEntity.Id, playListEntity.OriginalPictureName, cancellationToken); if (!publishResult.IsSuccess) diff --git a/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs b/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs index 1a8ba92..7928680 100644 --- a/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs +++ b/Application/PlayLists/Responses/PlayListPictureUploadResponse.cs @@ -1,6 +1,7 @@ namespace Musify.Application.PlayLists.Responses { public record PlayListPictureUploadResponse( + Guid IntentId, string Bucket, string Key, string PictureName, diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index 652015a..bda3529 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -1,4 +1,4 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; using Musify.Application.Tracks.Responses; @@ -7,7 +7,7 @@ namespace Musify.Application.Tracks.Commands public record CreateTrackCommand( Guid UserId, string Title, - string OriginalPictureName, - string OriginalAudioName) + Guid PictureIntentId, + Guid AudioIntentId) : ICommand>; -} \ No newline at end of file +} diff --git a/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs b/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs index 022a0e1..46b7151 100644 --- a/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs +++ b/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs @@ -9,6 +9,8 @@ namespace Musify.Application.Tracks.Commands string PictureFileType, string PictureContentType, string AudioFileType, - string AudioContentType) + string AudioContentType, + long? ExpectedPictureSizeBytes = null, + long? ExpectedAudioSizeBytes = null) : ICommand>; } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index c014064..1cfe978 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -7,6 +7,7 @@ using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; +using Musify.Application.UploadIntents; using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler @@ -14,9 +15,11 @@ namespace Musify.Application.Tracks.Handler public class CreateTrackCommandHandler( IDatabase database, IEventBus eventBus, + IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, - TrackConfiguration trackConfiguration) + TrackConfiguration trackConfiguration, + UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { public async ValueTask> Handle(CreateTrackCommand request, CancellationToken cancellationToken) @@ -29,12 +32,41 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } + var pictureValidation = await UploadIntentHelpers.ValidateAndLoadAsync( + database, storageService, uploadIntentConfiguration, + request.PictureIntentId, request.UserId, cancellationToken); + if (!pictureValidation.IsSuccess) + return pictureValidation.Error!.Value; + + var audioValidation = await UploadIntentHelpers.ValidateAndLoadAsync( + database, storageService, uploadIntentConfiguration, + request.AudioIntentId, request.UserId, cancellationToken); + if (!audioValidation.IsSuccess) + return audioValidation.Error!.Value; + + var pictureIntent = pictureValidation.Intent!; + var audioIntent = audioValidation.Intent!; + + var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); + var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName); + + try + { + await storageService.CopyFileAsync(pictureIntent.Bucket, pictureIntent.Key, pictureIntent.Bucket, finalPictureKey, cancellationToken); + await storageService.CopyFileAsync(audioIntent.Bucket, audioIntent.Key, audioIntent.Bucket, finalAudioKey, cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to copy track files from temp to final location for user {UserId}", request.UserId); + return Result.Error("Failed to move uploaded files to their final locations."); + } + var trackEntity = new Track { Title = request.Title, NormalizedTitle = request.Title.ToUpperInvariant(), - OriginalPictureName = request.OriginalPictureName, - OriginalAudioName = request.OriginalAudioName, + OriginalPictureName = pictureIntent.ObjectName, + OriginalAudioName = audioIntent.ObjectName, SmallPictureName = trackConfiguration.Routes.PresetSmallPicture, MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, LargePictureName = trackConfiguration.Routes.PresetLargePicture, @@ -44,6 +76,9 @@ namespace Musify.Application.Tracks.Handler await database.Tracks.AddAsync(trackEntity, cancellationToken); + pictureIntent.Status = UploadIntentStatus.Consumed; + audioIntent.Status = UploadIntentStatus.Consumed; + var publishPictureResult = await PublishUpdateEvent(request.UserId, trackEntity, cancellationToken); if (!publishPictureResult.IsSuccess) return Result.Error($"Failed to create track for {request.Title}"); @@ -114,6 +149,5 @@ namespace Musify.Application.Tracks.Handler return Result.Error($"Failed to publish track audio update event for track {track.Id}"); } } - } -} \ No newline at end of file +} diff --git a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs index d4c4d68..048cb66 100644 --- a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs +++ b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs @@ -6,6 +6,8 @@ using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Configuration; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; +using Musify.Application.UploadIntents; +using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler { @@ -14,12 +16,12 @@ namespace Musify.Application.Tracks.Handler IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, - TrackConfiguration trackConfiguration) + TrackConfiguration trackConfiguration, + UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { static readonly string[] AllowedPictureExtensions = [".webp", ".png", ".jpg", ".jpeg"]; static readonly string[] AllowedAudioExtensions = [".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".opus"]; - const int ExpiresInSeconds = 600; public async ValueTask> Handle(RequestTrackUploadUrlsCommand request, CancellationToken cancellationToken) { @@ -32,53 +34,101 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } - var pictureExtensionResult = ValidateExtension(request.PictureFileType, AllowedPictureExtensions); + var pictureExtensionResult = UploadIntentHelpers.ValidateExtension(request.PictureFileType, AllowedPictureExtensions); if (!pictureExtensionResult.IsSuccess) return Result.Invalid(pictureExtensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); - var audioExtensionResult = ValidateExtension(request.AudioFileType, AllowedAudioExtensions); + var audioExtensionResult = UploadIntentHelpers.ValidateExtension(request.AudioFileType, AllowedAudioExtensions); if (!audioExtensionResult.IsSuccess) return Result.Invalid(audioExtensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); - if (string.IsNullOrWhiteSpace(request.PictureContentType)) - return Result.Invalid(new ValidationError("PictureContentType is required")); + var effectivePictureSize = request.ExpectedPictureSizeBytes + ?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes; + var effectiveAudioSize = request.ExpectedAudioSizeBytes + ?? uploadIntentConfiguration.DefaultExpectedAudioSizeBytes; - if (string.IsNullOrWhiteSpace(request.AudioContentType)) - return Result.Invalid(new ValidationError("AudioContentType is required")); + var pictureObjectName = Guid.NewGuid() + pictureExtensionResult.Value; + var audioObjectName = Guid.NewGuid() + audioExtensionResult.Value; - var pictureName = Guid.NewGuid() + pictureExtensionResult.Value; - var audioName = Guid.NewGuid() + audioExtensionResult.Value; - - var pictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureName); - var audioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioName); + var tempPictureKey = trackConfiguration.Routes.BuildTempPicturePath( + uploadIntentConfiguration.TempRootPrefix, request.UserId, pictureObjectName); + var tempAudioKey = trackConfiguration.Routes.BuildTempAudioPath( + uploadIntentConfiguration.TempRootPrefix, request.UserId, audioObjectName); try { + var expiresIn = TimeSpan.FromSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds); + var pictureUploadUrl = await storageService.GetUploadUrlAsync( storageConfiguration.Bucket, - pictureKey, + tempPictureKey, request.PictureContentType, - TimeSpan.FromSeconds(ExpiresInSeconds), + expiresIn, cancellationToken); var audioUploadUrl = await storageService.GetUploadUrlAsync( storageConfiguration.Bucket, - audioKey, + tempAudioKey, request.AudioContentType, - TimeSpan.FromSeconds(ExpiresInSeconds), + expiresIn, cancellationToken); + var expiresAt = DateTime.UtcNow.AddSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds); + + await using var transaction = await database.BeginTransactionAsync( + System.Data.IsolationLevel.Serializable, cancellationToken); + + var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync( + database, uploadIntentConfiguration, logger, + request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken); + if (!quotaCheck.IsSuccess) + return quotaCheck; + + var pictureIntent = new UploadIntent + { + UserId = request.UserId, + Bucket = storageConfiguration.Bucket, + Key = tempPictureKey, + ObjectName = pictureObjectName, + ContentType = request.PictureContentType, + ExpectedSizeBytes = effectivePictureSize, + Purpose = UploadIntentPurpose.TrackPicture, + ExpiresAt = expiresAt, + }; + + var audioIntent = new UploadIntent + { + UserId = request.UserId, + Bucket = storageConfiguration.Bucket, + Key = tempAudioKey, + ObjectName = audioObjectName, + ContentType = request.AudioContentType, + ExpectedSizeBytes = effectiveAudioSize, + Purpose = UploadIntentPurpose.TrackAudio, + ExpiresAt = expiresAt, + }; + + await database.UploadIntents.AddAsync(pictureIntent, cancellationToken); + await database.UploadIntents.AddAsync(audioIntent, cancellationToken); + await database.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + + logger.LogInformation("Issued upload intents {PictureIntentId}/{AudioIntentId} for user {UserId} (TrackPicture/TrackAudio)", + pictureIntent.Id, audioIntent.Id, request.UserId); + return Result.Success(new TrackUploadUrlsResponse( + pictureIntent.Id, + audioIntent.Id, storageConfiguration.Bucket, - pictureKey, - pictureName, + tempPictureKey, + pictureObjectName, request.PictureContentType, pictureUploadUrl, - audioKey, - audioName, + tempAudioKey, + audioObjectName, request.AudioContentType, audioUploadUrl, - ExpiresInSeconds)); + uploadIntentConfiguration.UploadUrlExpiresInSeconds)); } catch (Exception exception) { @@ -86,25 +136,5 @@ namespace Musify.Application.Tracks.Handler return Result.Error("Failed to generate upload URLs"); } } - - static Result ValidateExtension(string fileType, string[] allowedExtensions) - { - if (string.IsNullOrWhiteSpace(fileType)) - return Result.Error("FileType is required"); - - var extension = fileType.Trim(); - if (!extension.StartsWith('.')) - extension = "." + extension; - - extension = extension.ToLowerInvariant(); - - if (extension.Contains('/') || extension.Contains('\\')) - return Result.Error("Invalid FileType"); - - if (!allowedExtensions.Contains(extension)) - return Result.Error($"Unsupported FileType '{extension}'"); - - return Result.Success(extension); - } } } diff --git a/Application/Tracks/Responses/TrackUploadUrlsResponse.cs b/Application/Tracks/Responses/TrackUploadUrlsResponse.cs index cc5eb04..8e1f5fb 100644 --- a/Application/Tracks/Responses/TrackUploadUrlsResponse.cs +++ b/Application/Tracks/Responses/TrackUploadUrlsResponse.cs @@ -1,6 +1,8 @@ namespace Musify.Application.Tracks.Responses { public record TrackUploadUrlsResponse( + Guid PictureIntentId, + Guid AudioIntentId, string Bucket, string PictureKey, string PictureName, diff --git a/Application/UploadIntents/IntentValidationResult.cs b/Application/UploadIntents/IntentValidationResult.cs new file mode 100644 index 0000000..5271fc0 --- /dev/null +++ b/Application/UploadIntents/IntentValidationResult.cs @@ -0,0 +1,13 @@ +using Ardalis.Result; +using Musify.Domain.Entities; + +namespace Musify.Application.UploadIntents +{ + internal sealed record IntentValidationResult(Result? Error, UploadIntent? Intent) + { + public bool IsSuccess => Error is null; + + public static IntentValidationResult Ok(UploadIntent intent) => new(null, intent); + public static IntentValidationResult Fail(Result error) => new(error, null); + } +} diff --git a/Application/UploadIntents/UploadIntentHelpers.cs b/Application/UploadIntents/UploadIntentHelpers.cs new file mode 100644 index 0000000..fdba870 --- /dev/null +++ b/Application/UploadIntents/UploadIntentHelpers.cs @@ -0,0 +1,89 @@ +using Ardalis.Result; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; +using Musify.Domain.Entities; + +namespace Musify.Application.UploadIntents +{ + internal static class UploadIntentHelpers + { + internal static async Task ValidateAndLoadAsync( + IDatabase database, + IStorageService storageService, + UploadIntentConfiguration config, + Guid intentId, + Guid userId, + CancellationToken cancellationToken) + { + var intent = await database.UploadIntents + .FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken); + + if (intent is null || intent.UserId != userId) + return IntentValidationResult.Fail(Result.NotFound("Upload intent not found or not accessible.")); + + if (intent.Status == UploadIntentStatus.Consumed) + return IntentValidationResult.Fail(Result.Conflict("Upload intent has already been consumed.")); + + if (intent.Status == UploadIntentStatus.Expired || intent.ExpiresAt < DateTime.UtcNow) + return IntentValidationResult.Fail(Result.Invalid(new ValidationError("Upload intent has expired."))); + + var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); + if (metadata is null) + return IntentValidationResult.Fail(Result.NotFound("Uploaded object not found in storage. Upload the file first.")); + + if (metadata.ContentLength > config.MaxUploadBytes) + return IntentValidationResult.Fail(Result.Invalid(new ValidationError($"Uploaded file exceeds the maximum allowed size of {config.MaxUploadBytes} bytes."))); + + return IntentValidationResult.Ok(intent); + } + + internal static async Task CheckQuotaAsync( + IDatabase database, + UploadIntentConfiguration config, + ILogger logger, + Guid userId, + long requiredBytes, + int requiredIntentCount, + CancellationToken cancellationToken) + { + var activeIntents = await database.UploadIntents + .AsNoTracking() + .Where(i => i.UserId == userId && i.Status == UploadIntentStatus.Issued) + .ToListAsync(cancellationToken); + + if (activeIntents.Count + requiredIntentCount > config.MaxActiveUploadIntentsPerUser) + { + logger.LogWarning("User {UserId} exceeded max active upload intents ({Max})", userId, config.MaxActiveUploadIntentsPerUser); + return Result.Invalid(new ValidationError("Upload intent limit exceeded. Wait for existing uploads to complete or expire.")); + } + + var activeBytes = activeIntents.Sum(i => i.ExpectedSizeBytes ?? config.DefaultExpectedPictureSizeBytes); + if (activeBytes + requiredBytes > config.MaxActiveUploadBytesPerUser) + { + logger.LogWarning("User {UserId} exceeded max active upload bytes ({Max})", userId, config.MaxActiveUploadBytesPerUser); + return Result.Invalid(new ValidationError("Upload byte quota exceeded. Wait for existing uploads to complete or expire.")); + } + + return Result.Success(); + } + + internal static Result ValidateExtension(string fileType, string[] allowedExtensions) + { + var extension = fileType.Trim(); + if (!extension.StartsWith('.')) + extension = "." + extension; + + extension = extension.ToLowerInvariant(); + + if (extension.Contains('/') || extension.Contains('\\')) + return Result.Error("Invalid FileType"); + + if (!allowedExtensions.Contains(extension)) + return Result.Error($"Unsupported FileType '{extension}'"); + + return Result.Success(extension); + } + } +} diff --git a/Domain/Entities/UploadIntent.cs b/Domain/Entities/UploadIntent.cs new file mode 100644 index 0000000..e15d5c7 --- /dev/null +++ b/Domain/Entities/UploadIntent.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ + [Table("UploadIntents")] + public class UploadIntent + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required Guid UserId { get; set; } + + [Required, MaxLength(128)] + public required string Bucket { get; set; } + + // Full S3 key (including prefix and object name) + [Required, MaxLength(512)] + public required string Key { get; set; } + + // Just the filename portion stored in Track/PlayList entities (e.g. "{guid}.webp") + [Required, MaxLength(128)] + public required string ObjectName { get; set; } + + [Required, MaxLength(128)] + public required string ContentType { get; set; } + + public long? ExpectedSizeBytes { get; set; } + + [Required] + public UploadIntentPurpose Purpose { get; set; } + + [Required] + public UploadIntentStatus Status { get; set; } = UploadIntentStatus.Issued; + + [Required] + public DateTime ExpiresAt { get; set; } + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [ForeignKey(nameof(UserId))] + public User User { get; set; } = null!; + } +} diff --git a/Domain/Entities/UploadIntentPurpose.cs b/Domain/Entities/UploadIntentPurpose.cs new file mode 100644 index 0000000..f0a7f08 --- /dev/null +++ b/Domain/Entities/UploadIntentPurpose.cs @@ -0,0 +1,9 @@ +namespace Musify.Domain.Entities +{ + public enum UploadIntentPurpose + { + PlayListPicture, + TrackPicture, + TrackAudio, + } +} diff --git a/Domain/Entities/UploadIntentStatus.cs b/Domain/Entities/UploadIntentStatus.cs new file mode 100644 index 0000000..be3956b --- /dev/null +++ b/Domain/Entities/UploadIntentStatus.cs @@ -0,0 +1,9 @@ +namespace Musify.Domain.Entities +{ + public enum UploadIntentStatus + { + Issued, + Consumed, + Expired, + } +} diff --git a/Infrastructure/Jobs/TempUploadsCleanupJob.cs b/Infrastructure/Jobs/TempUploadsCleanupJob.cs new file mode 100644 index 0000000..ac83776 --- /dev/null +++ b/Infrastructure/Jobs/TempUploadsCleanupJob.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; + +namespace Musify.Infrastructure.Jobs +{ + public class TempUploadsCleanupJob( + IServiceScopeFactory scopeFactory, + ILogger logger, + ApplicationStorageConfiguration storageConfiguration, + UploadIntentConfiguration uploadIntentConfiguration) + : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("TempUploadsCleanupJob started (interval: {Interval}s, prefix: {Prefix})", + uploadIntentConfiguration.TempCleanupJobIntervalSeconds, + uploadIntentConfiguration.TempRootPrefix); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await RunAsync(stoppingToken); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + logger.LogError(exception, "Error in TempUploadsCleanupJob"); + } + + await Task.Delay(TimeSpan.FromSeconds(uploadIntentConfiguration.TempCleanupJobIntervalSeconds), stoppingToken); + } + } + + async Task RunAsync(CancellationToken cancellationToken) + { + await using var scope = scopeFactory.CreateAsyncScope(); + var storageService = scope.ServiceProvider.GetRequiredService(); + + var cutoff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TempUploadsRetentionDays); + var deleted = 0; + var errors = 0; + + await foreach (var (key, lastModified) in storageService.ListObjectsAsync( + storageConfiguration.Bucket, + uploadIntentConfiguration.TempRootPrefix, + cancellationToken)) + { + if (lastModified >= cutoff) + continue; + + try + { + await storageService.RemoveFileAsync(storageConfiguration.Bucket, key, cancellationToken); + deleted++; + } + catch (Exception exception) + { + errors++; + logger.LogWarning(exception, "Failed to delete orphaned temp object {Key}", key); + } + } + + if (deleted > 0 || errors > 0) + logger.LogInformation("TempUploadsCleanupJob: deleted {Deleted} orphaned objects, {Errors} errors", deleted, errors); + } + } +} diff --git a/Infrastructure/Jobs/UploadIntentExpirationJob.cs b/Infrastructure/Jobs/UploadIntentExpirationJob.cs new file mode 100644 index 0000000..3b46a44 --- /dev/null +++ b/Infrastructure/Jobs/UploadIntentExpirationJob.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; +using Musify.Domain.Entities; + +namespace Musify.Infrastructure.Jobs +{ + public class UploadIntentExpirationJob( + IServiceScopeFactory scopeFactory, + ILogger logger, + UploadIntentConfiguration uploadIntentConfiguration) + : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("UploadIntentExpirationJob started (interval: {Interval}s)", uploadIntentConfiguration.ExpirationJobIntervalSeconds); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await RunAsync(stoppingToken); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + logger.LogError(exception, "Error in UploadIntentExpirationJob"); + } + + await Task.Delay(TimeSpan.FromSeconds(uploadIntentConfiguration.ExpirationJobIntervalSeconds), stoppingToken); + } + } + + async Task RunAsync(CancellationToken cancellationToken) + { + await using var scope = scopeFactory.CreateAsyncScope(); + var database = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + + // Mark expired intents + var expiredIntents = await database.UploadIntents + .Where(i => i.Status == UploadIntentStatus.Issued && i.ExpiresAt < now) + .ToListAsync(cancellationToken); + + if (expiredIntents.Count > 0) + { + foreach (var intent in expiredIntents) + intent.Status = UploadIntentStatus.Expired; + + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Marked {Count} upload intents as Expired", expiredIntents.Count); + } + + // Delete old Expired intents past retention window + if (uploadIntentConfiguration.ExpiredIntentsRetentionDays >= 0) + { + var retentionCutoff = now.AddDays(-uploadIntentConfiguration.ExpiredIntentsRetentionDays); + + var oldExpiredIntents = await database.UploadIntents + .Where(i => i.Status == UploadIntentStatus.Expired && i.ExpiresAt < retentionCutoff) + .ToListAsync(cancellationToken); + + if (oldExpiredIntents.Count > 0) + { + database.UploadIntents.RemoveRange(oldExpiredIntents); + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Deleted {Count} old Expired upload intents past retention window", oldExpiredIntents.Count); + } + } + } + } +} diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 778fc2d..c4df933 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -1,13 +1,15 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using MassTransit; using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; +using AppIDatabase = Musify.Application.Abstractions.Infrastructure.IDatabase; namespace Musify.Infrastructure.Persistence { public class Database(DatabaseConfiguration configuration) - : DbContext, IDatabase + : DbContext, AppIDatabase { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -34,5 +36,21 @@ namespace Musify.Infrastructure.Persistence public DbSet PlayListHasTracks => Set(); public DbSet Uploads => Set(); + + public DbSet UploadIntents => Set(); + + public async Task BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) + { + var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); + return new DatabaseTransaction(transaction); + } + + private sealed class DatabaseTransaction(IDbContextTransaction inner) : IDatabaseTransaction + { + public Task CommitAsync(CancellationToken cancellationToken = default) => + inner.CommitAsync(cancellationToken); + + public ValueTask DisposeAsync() => inner.DisposeAsync(); + } } -} \ No newline at end of file +} diff --git a/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs b/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs new file mode 100644 index 0000000..a27e8d3 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs @@ -0,0 +1,580 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260514172047_AddUploadIntentTable")] + partial class AddUploadIntentTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs b/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs new file mode 100644 index 0000000..6d39875 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddUploadIntentTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UploadIntents", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Bucket = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Key = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + ObjectName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ContentType = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ExpectedSizeBytes = table.Column(type: "bigint", nullable: true), + Purpose = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UploadIntents", x => x.Id); + table.ForeignKey( + name: "FK_UploadIntents_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UploadIntents_UserId", + table: "UploadIntents", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UploadIntents"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 30ec587..292cfc5 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -374,6 +374,57 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("Upload"); }); + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + modelBuilder.Entity("Musify.Domain.Entities.User", b => { b.Property("Id") @@ -472,6 +523,17 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("Track"); }); + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => { b.HasOne("Musify.Domain.Entities.Track", "Track") diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 56e48e7..4e92451 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Configuration; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.Jobs; namespace Musify.Infrastructure.Services { @@ -41,6 +43,31 @@ namespace Musify.Infrastructure.Services return serviceDescriptors; } + // Called by both the web API host and the worker + public static IServiceCollection AddUploadIntentConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(UploadIntentConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } + + // Called only by the worker host (registers background jobs) + public static IServiceCollection AddUploadIntentJobs(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors.AddUploadIntentConfiguration(configuration); + + serviceDescriptors.AddHostedService(); + serviceDescriptors.AddHostedService(); + + return serviceDescriptors; + } + public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 806a271..3c2efaf 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -6,6 +6,7 @@ using MimeMapping; using Musify.Application.Abstractions.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; +using ObjectMetadata = Musify.Application.Abstractions.Infrastructure.ObjectMetadata; namespace Musify.Infrastructure.Services { @@ -69,6 +70,24 @@ namespace Musify.Infrastructure.Services return await amazonS3.GetPreSignedURLAsync(request); } + public async Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken) + { + try + { + var response = await amazonS3.GetObjectMetadataAsync(bucket, key, cancellationToken); + return new ObjectMetadata(response.Headers.ContentType, response.ContentLength); + } + catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return null; + } + catch (Exception exception) + { + logger.LogError(exception, "HEAD failed for {Bucket}/{Key}", bucket, key); + throw; + } + } + public async Task RemoveFileAsync(string bucket, string key, CancellationToken cancellationToken) { var request = new DeleteObjectRequest @@ -204,6 +223,27 @@ namespace Musify.Infrastructure.Services } } + public async IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)> ListObjectsAsync( + string bucket, string prefix, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + var listRequest = new ListObjectsV2Request + { + BucketName = bucket, + Prefix = prefix.EndsWith('/') ? prefix : $"{prefix}/" + }; + + ListObjectsV2Response listResponse; + do + { + listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken); + foreach (var obj in listResponse.S3Objects) + yield return (obj.Key, (obj.LastModified ?? DateTime.UtcNow).ToUniversalTime()); + + listRequest.ContinuationToken = listResponse.NextContinuationToken; + } + while (listResponse.IsTruncated.HasValue && listResponse.IsTruncated.Value); + } + public async Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken) { var prefix = folderKey.EndsWith('/') ? folderKey : $"{folderKey}/"; diff --git a/docs/changes-summary.md b/docs/changes-summary.md index 0dbe664..c25c997 100644 --- a/docs/changes-summary.md +++ b/docs/changes-summary.md @@ -159,7 +159,7 @@ Archivo: --- -## 10) Estado final +## 10) Estado final (iteración anterior) - `dotnet build MusifyBackend.slnx -c Release` ✅ @@ -192,3 +192,142 @@ Archivos clave: - `Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs` - `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs` - `Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs` + +--- + +## 12) Hardening de pre-signed uploads (Fases 1 y 2) + +**Objetivo**: evitar abuso de coste/almacenamiento cuando usuarios hacen spam de endpoints presigned sin crear la entidad final (objetos huérfanos en S3). + +Referencia: `docs/presigned-upload-hardening.md` + +--- + +### Fase 1 — Upload Intents en DB + +#### Nuevas entidades (Domain) +- `Domain/Entities/UploadIntent.cs` — tabla `UploadIntents` con campos: `Id`, `UserId`, `Bucket`, `Key`, `ObjectName`, `ContentType`, `ExpectedSizeBytes`, `Purpose`, `Status`, `ExpiresAt`, `CreatedAt`. +- `Domain/Entities/UploadIntentStatus.cs` — enum: `Issued`, `Consumed`, `Expired`. +- `Domain/Entities/UploadIntentPurpose.cs` — enum: `PlayListPicture`, `TrackPicture`, `TrackAudio`. + +#### Nueva configuración (Application) +- `Application/Configuration/UploadIntentConfiguration.cs` — sección `UploadIntent`: + - `UploadUrlExpiresInSeconds` (default: 120) + - `MaxActiveUploadIntentsPerUser` (default: 5) + - `MaxActiveUploadBytesPerUser` (default: 200 MB) + - `MaxUploadBytes` (default: 100 MB, para validación HEAD) + - `DefaultExpectedPictureSizeBytes` / `DefaultExpectedAudioSizeBytes` (fallback cuando el caller no envía tamaño) + - `ExpirationJobIntervalSeconds` / `ExpiredIntentsRetentionDays` + - `TempRootPrefix` / `TempUploadsRetentionDays` / `TempCleanupJobIntervalSeconds` (Fase 2) + +#### Interfaces actualizadas +- `Application/Abstractions/Infrastructure/IDatabase.cs` — añadido `DbSet UploadIntents`. +- `Application/Abstractions/Infrastructure/IStorageService.cs` — añadidos: + - `HeadObjectAsync(bucket, key)` → `ObjectMetadata?` (para validación antes de consumir). + - `ListObjectsAsync(bucket, prefix)` → `IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)>` (para el cleanup job). +- `Application/Abstractions/Infrastructure/ObjectMetadata.cs` — record `(ContentType, ContentLength)`. + +#### Infrastructure actualizada +- `Infrastructure/Persistence/Database.cs` — añadido `DbSet`. +- `Infrastructure/Services/StorageService.cs` — implementaciones de `HeadObjectAsync` y `ListObjectsAsync`. + +#### Commands actualizados (cambio de contrato) +- `RequestPlayListPictureUploadCommand` — nuevo campo opcional `ExpectedSizeBytes?`. +- `RequestTrackUploadUrlsCommand` — nuevos campos opcionales `ExpectedPictureSizeBytes?`, `ExpectedAudioSizeBytes?`. +- `CreatePlayListCommand` — `OriginalPictureName?` → `PictureIntentId?` (Guid). +- `CreateTrackCommand` — `OriginalPictureName` + `OriginalAudioName` → `PictureIntentId` + `AudioIntentId` (Guid). +- `UpdatePlayListCommand` — `NewOriginalPictureName?` → `NewPictureIntentId?` (Guid). + +#### Responses actualizadas +- `PlayListPictureUploadResponse` — añadido `IntentId` (Guid). +- `TrackUploadUrlsResponse` — añadidos `PictureIntentId` + `AudioIntentId` (Guid). + +#### Handlers actualizados + +**Request upload** (`RequestPlayListPictureUploadCommandHandler`, `RequestTrackUploadUrlsCommandHandler`): +- Comprueban cuota de usuario (conteo de intents activos + suma de bytes) antes de generar la URL. +- Crean un registro `UploadIntent` (Status = `Issued`) en la misma llamada. +- TTL reducido a `UploadUrlExpiresInSeconds` (configurable, default 120 s vs. los anteriores 600 s hardcodeados). + +**Create/Update** (`CreatePlayListCommandHandler`, `CreateTrackCommandHandler`, `UpdatePlayListCommandHandler`): +- Reciben `IntentId` en lugar del nombre de fichero. +- Validan el intent: existe, pertenece al usuario, Status = `Issued`, no caducado. +- Realizan `HEAD` del objeto en S3: debe existir y tamaño ≤ `MaxUploadBytes`. +- Marcan el intent como `Consumed` y guardan todo en el mismo `SaveChangesAsync`. + +#### Job de expiración +- `Infrastructure/Jobs/UploadIntentExpirationJob.cs` — `BackgroundService` que: + - Marca como `Expired` los intents `Issued` con `ExpiresAt < now`. + - Elimina intents `Expired` cuya fecha de expiración supera la ventana de retención configurable. + +#### Migración +- `Infrastructure/Persistence/Migrations/*_AddUploadIntentTable.cs` + +--- + +### Fase 2 — Prefijo temporal + cleanup de S3 + +#### Rutas temporales +- `TrackRoutes.BuildTempPicturePath` / `BuildTempAudioPath` — construyen `{TempRootPrefix}/{userId}/{ParentFolder}/{objectName}`. +- `PlayListRoutes.BuildTempPicturePath` — ídem para playlists. + +#### Handlers de request upload +- La presigned URL ahora apunta a `temp/{userId}/...` (prefijo temporal) en lugar de la ruta final. +- El intent almacena la temp key. + +#### Handlers de create/update (consume) +- Al consumir un intent, se llama `CopyFileAsync(tempKey → finalKey)` antes de publicar el evento de procesado. +- El objeto temporal queda en S3 y es eliminado posteriormente por el cleanup job. + +#### Job de cleanup de temporales +- `Infrastructure/Jobs/TempUploadsCleanupJob.cs` — `BackgroundService` que: + - Lista objetos bajo `TempRootPrefix/` usando `ListObjectsAsync`. + - Borra los que superan `TempUploadsRetentionDays` (default 3 días). + - Tolerante a errores por objeto (continúa si un borrado falla). + +#### DI +- `Infrastructure/Services/ServicesDependencyInjection.AddUploadIntentJobs()` — registra `UploadIntentConfiguration` + ambos background jobs. + +--- + +## 13) Refactor: IntentValidationResult (eliminar tuplas) + +**Objetivo**: evitar retornar tuplas `(Result?, UploadIntent?)` desde métodos privados — patrón no idiomático en C#. + +### Cambios +- Creado `Application/UploadIntents/IntentValidationResult.cs` — `internal sealed record` con: + - `Result? Error` / `UploadIntent? Intent` + - `bool IsSuccess` + - Factory methods `Ok(intent)` / `Fail(error)` +- Los tres handlers (`CreatePlayListCommandHandler`, `CreateTrackCommandHandler`, `UpdatePlayListCommandHandler`) ahora usan `IntentValidationResult` en `ValidateAndLoadIntentAsync`. +- El tipo es `internal` (no `private`) porque es compartido entre 3 handlers de distintas carpetas. + +--- + +## 14) Fix: race condition en cuota de intents + +**Objetivo**: garantizar que el check de cuota + insert del intent sean atómicos y no admitan concurrent bypass. + +### Cambios +- Añadida `IDatabaseTransaction` en `Application/Abstractions/Infrastructure/IDatabaseTransaction.cs` — interfaz limpia (`CommitAsync`, `DisposeAsync`) que no expone tipos EF. +- Añadido `BeginTransactionAsync(IsolationLevel, CancellationToken)` a `IDatabase`. +- Implementado en `Infrastructure/Persistence/Database.cs` con un `private sealed class DatabaseTransaction` que envuelve `IDbContextTransaction` de EF. +- `RequestPlayListPictureUploadCommandHandler` y `RequestTrackUploadUrlsCommandHandler`: el bloque quota check + insert ahora se ejecuta dentro de una transacción `SERIALIZABLE`, eliminando la ventana de race condition entre el check y el commit. + +--- + +## 15) Fix: separación AddUploadIntentConfiguration / AddUploadIntentJobs + +**Objetivo**: `UploadIntentConfiguration` es necesaria tanto en el host del API (para los handlers) como en el worker (para los jobs). El método anterior mezclaba config + jobs en uno solo, haciendo imposible registrar la config sin registrar los `BackgroundService`. + +### Cambios +- `AddUploadIntentConfiguration(services, configuration)` — registra solo la config. Lo llama el host del API **y** el worker. +- `AddUploadIntentJobs(services, configuration)` — llama internamente a `AddUploadIntentConfiguration` y además registra `UploadIntentExpirationJob` + `TempUploadsCleanupJob`. Solo lo llama el worker. + +Archivo: `Infrastructure/Services/ServicesDependencyInjection.cs` + +--- + +## 16) Estado final + +- `dotnet build` ✅ — 0 errores, 0 advertencias. From 80f44422a0d64da77af8a97b0dcdf2cdd26c6c68 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 21:42:31 +0200 Subject: [PATCH 139/172] Changes --- .../Application/IUploadIntentService.cs | 23 ++++++ Application/Extensions/ResultExtensions.cs | 25 +++++++ .../Handlers/CreatePlayListCommandHandler.cs | 13 ++-- ...uestPlayListPictureUploadCommandHandler.cs | 6 +- .../Handlers/UpdatePlayListCommandHandler.cs | 13 ++-- Application/Services/UploadIntentService.cs | 71 +++++++++++++++++++ .../Handler/CreateTrackCommandHandler.cs | 19 ++--- .../RequestTrackUploadUrlsCommandHandler.cs | 6 +- .../UploadIntents/IntentValidationResult.cs | 13 ---- .../UploadIntents/UploadIntentHelpers.cs | 65 ----------------- .../Services/ServicesDependencyInjection.cs | 4 ++ 11 files changed, 158 insertions(+), 100 deletions(-) create mode 100644 Application/Abstractions/Application/IUploadIntentService.cs create mode 100644 Application/Extensions/ResultExtensions.cs create mode 100644 Application/Services/UploadIntentService.cs delete mode 100644 Application/UploadIntents/IntentValidationResult.cs diff --git a/Application/Abstractions/Application/IUploadIntentService.cs b/Application/Abstractions/Application/IUploadIntentService.cs new file mode 100644 index 0000000..91c0bb7 --- /dev/null +++ b/Application/Abstractions/Application/IUploadIntentService.cs @@ -0,0 +1,23 @@ +using Ardalis.Result; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Domain.Entities; + +namespace Musify.Application.Abstractions.Application; + +public interface IUploadIntentService +{ + Task CheckQuotaAsync( + UploadIntentConfiguration config, + ILogger logger, + Guid userId, + long requiredBytes, + int requiredIntentCount, + CancellationToken cancellationToken); + + Task> ValidateAndLoadAsync( + UploadIntentConfiguration config, + Guid intentId, + Guid userId, + CancellationToken cancellationToken); +} diff --git a/Application/Extensions/ResultExtensions.cs b/Application/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..04bf866 --- /dev/null +++ b/Application/Extensions/ResultExtensions.cs @@ -0,0 +1,25 @@ +using Ardalis.Result; + +namespace Musify.Application.Extensions; + +public static class ResultExtensions +{ + public static Result As(this Result result) + { + return result.Status switch + { + ResultStatus.Ok or + ResultStatus.Created or + ResultStatus.NoContent => throw new InvalidOperationException($"Cannot convert a successful result ( {result.Status} ) to another type. Use Map or Bind instead."), + ResultStatus.NotFound => Result.NotFound(result.Errors.ToArray()), + ResultStatus.Forbidden => Result.Forbidden(result.Errors.ToArray()), + ResultStatus.Unauthorized => Result.Unauthorized(result.Errors.ToArray()), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors.ToArray()), + ResultStatus.Conflict => Result.Conflict(result.Errors.ToArray()), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + ResultStatus.Error => Result.Error(string.Join(";", result.Errors)), + _ => Result.Error(string.Join(";", result.Errors)) + }; + } +} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 7ac68aa..930a718 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -8,6 +8,8 @@ using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.UploadIntents; +using Musify.Application.Extensions; +using Musify.Application.Abstractions.Application; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -15,6 +17,7 @@ namespace Musify.Application.PlayLists.Handlers public class CreatePlayListCommandHandler( IEventBus eventBus, IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -38,13 +41,13 @@ namespace Musify.Application.PlayLists.Handlers if (request.PictureIntentId.HasValue) { - var validation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var validation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.PictureIntentId.Value, request.UserId, cancellationToken); if (!validation.IsSuccess) - return validation.Error!.Value; + return validation.As(); - pictureIntent = validation.Intent!; + pictureIntent = validation.Value; originalPictureName = pictureIntent.ObjectName; var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName); @@ -131,4 +134,4 @@ namespace Musify.Application.PlayLists.Handlers } } } -} +} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index a4ac2a1..ee6b51b 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -3,6 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Abstractions.Application; using Musify.Application.Configuration; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; @@ -13,6 +14,7 @@ namespace Musify.Application.PlayLists.Handlers { public class RequestPlayListPictureUploadCommandHandler( IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -57,8 +59,8 @@ namespace Musify.Application.PlayLists.Handlers await using var transaction = await database.BeginTransactionAsync( System.Data.IsolationLevel.Serializable, cancellationToken); - var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync( - database, uploadIntentConfiguration, logger, + var quotaCheck = await uploadIntentService.CheckQuotaAsync( + uploadIntentConfiguration, logger, request.UserId, effectiveSizeBytes, 1, cancellationToken); if (!quotaCheck.IsSuccess) return quotaCheck; diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index fbbe76b..70b064a 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -8,6 +8,8 @@ using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.UploadIntents; +using Musify.Application.Extensions; +using Musify.Application.Abstractions.Application; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -15,6 +17,7 @@ namespace Musify.Application.PlayLists.Handlers public class UpdatePlayListCommandHandler( IEventBus eventBus, IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -50,13 +53,13 @@ namespace Musify.Application.PlayLists.Handlers if (request.NewPictureIntentId.HasValue) { - var validation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var validation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.NewPictureIntentId.Value, request.UserId, cancellationToken); if (!validation.IsSuccess) - return validation.Error!.Value; + return validation.As(); - var intent = validation.Intent!; + var intent = validation.Value; var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName); try { @@ -121,4 +124,4 @@ namespace Musify.Application.PlayLists.Handlers } } } -} +} \ No newline at end of file diff --git a/Application/Services/UploadIntentService.cs b/Application/Services/UploadIntentService.cs new file mode 100644 index 0000000..f3c736f --- /dev/null +++ b/Application/Services/UploadIntentService.cs @@ -0,0 +1,71 @@ +using Ardalis.Result; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Configuration; +using Musify.Domain.Entities; + +namespace Musify.Application.Services; + +public sealed class UploadIntentService( + IDatabase database, + IStorageService storageService) : IUploadIntentService +{ + public async Task CheckQuotaAsync( + UploadIntentConfiguration config, + ILogger logger, + Guid userId, + long requiredBytes, + int requiredIntentCount, + CancellationToken cancellationToken) + { + var activeIntents = await database.UploadIntents + .AsNoTracking() + .Where(i => i.UserId == userId && i.Status == UploadIntentStatus.Issued) + .ToListAsync(cancellationToken); + + if (activeIntents.Count + requiredIntentCount > config.MaxActiveUploadIntentsPerUser) + { + logger.LogWarning("User {UserId} exceeded max active upload intents ({Max})", userId, config.MaxActiveUploadIntentsPerUser); + return Result.Invalid(new ValidationError("Upload intent limit exceeded. Wait for existing uploads to complete or expire.")); + } + + var activeBytes = activeIntents.Sum(i => i.ExpectedSizeBytes ?? config.DefaultExpectedPictureSizeBytes); + if (activeBytes + requiredBytes > config.MaxActiveUploadBytesPerUser) + { + logger.LogWarning("User {UserId} exceeded max active upload bytes ({Max})", userId, config.MaxActiveUploadBytesPerUser); + return Result.Invalid(new ValidationError("Upload byte quota exceeded. Wait for existing uploads to complete or expire.")); + } + + return Result.Success(); + } + + public async Task> ValidateAndLoadAsync( + UploadIntentConfiguration config, + Guid intentId, + Guid userId, + CancellationToken cancellationToken) + { + var intent = await database.UploadIntents + .FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken); + + if (intent is null || intent.UserId != userId) + return Result.NotFound("Upload intent not found or not accessible."); + + if (intent.Status == UploadIntentStatus.Consumed) + return Result.Conflict("Upload intent has already been consumed."); + + if (intent.Status == UploadIntentStatus.Expired || intent.ExpiresAt < DateTime.UtcNow) + return Result.Invalid(new ValidationError("Upload intent has expired.")); + + var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); + if (metadata is null) + return Result.NotFound("Uploaded object not found in storage. Upload the file first."); + + if (metadata.ContentLength > config.MaxUploadBytes) + return Result.Invalid(new ValidationError($"Uploaded file exceeds the maximum allowed size of {config.MaxUploadBytes} bytes.")); + + return Result.Success(intent); + } +} diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 1cfe978..7578ed3 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -8,6 +8,8 @@ using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; using Musify.Application.UploadIntents; +using Musify.Application.Extensions; +using Musify.Application.Abstractions.Application; using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler @@ -15,6 +17,7 @@ namespace Musify.Application.Tracks.Handler public class CreateTrackCommandHandler( IDatabase database, IEventBus eventBus, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -32,20 +35,20 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } - var pictureValidation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var pictureValidation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.PictureIntentId, request.UserId, cancellationToken); if (!pictureValidation.IsSuccess) - return pictureValidation.Error!.Value; + return pictureValidation.As(); - var audioValidation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var audioValidation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.AudioIntentId, request.UserId, cancellationToken); if (!audioValidation.IsSuccess) - return audioValidation.Error!.Value; + return audioValidation.As(); - var pictureIntent = pictureValidation.Intent!; - var audioIntent = audioValidation.Intent!; + var pictureIntent = pictureValidation.Value; + var audioIntent = audioValidation.Value; var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName); diff --git a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs index 048cb66..d40fbf8 100644 --- a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs +++ b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs @@ -2,6 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Application; using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Configuration; using Musify.Application.Tracks.Commands; @@ -13,6 +14,7 @@ namespace Musify.Application.Tracks.Handler { public class RequestTrackUploadUrlsCommandHandler( IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -78,8 +80,8 @@ namespace Musify.Application.Tracks.Handler await using var transaction = await database.BeginTransactionAsync( System.Data.IsolationLevel.Serializable, cancellationToken); - var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync( - database, uploadIntentConfiguration, logger, + var quotaCheck = await uploadIntentService.CheckQuotaAsync( + uploadIntentConfiguration, logger, request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken); if (!quotaCheck.IsSuccess) return quotaCheck; diff --git a/Application/UploadIntents/IntentValidationResult.cs b/Application/UploadIntents/IntentValidationResult.cs deleted file mode 100644 index 5271fc0..0000000 --- a/Application/UploadIntents/IntentValidationResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Ardalis.Result; -using Musify.Domain.Entities; - -namespace Musify.Application.UploadIntents -{ - internal sealed record IntentValidationResult(Result? Error, UploadIntent? Intent) - { - public bool IsSuccess => Error is null; - - public static IntentValidationResult Ok(UploadIntent intent) => new(null, intent); - public static IntentValidationResult Fail(Result error) => new(error, null); - } -} diff --git a/Application/UploadIntents/UploadIntentHelpers.cs b/Application/UploadIntents/UploadIntentHelpers.cs index fdba870..8551a27 100644 --- a/Application/UploadIntents/UploadIntentHelpers.cs +++ b/Application/UploadIntents/UploadIntentHelpers.cs @@ -1,74 +1,9 @@ using Ardalis.Result; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Application.Configuration; -using Musify.Domain.Entities; namespace Musify.Application.UploadIntents { internal static class UploadIntentHelpers { - internal static async Task ValidateAndLoadAsync( - IDatabase database, - IStorageService storageService, - UploadIntentConfiguration config, - Guid intentId, - Guid userId, - CancellationToken cancellationToken) - { - var intent = await database.UploadIntents - .FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken); - - if (intent is null || intent.UserId != userId) - return IntentValidationResult.Fail(Result.NotFound("Upload intent not found or not accessible.")); - - if (intent.Status == UploadIntentStatus.Consumed) - return IntentValidationResult.Fail(Result.Conflict("Upload intent has already been consumed.")); - - if (intent.Status == UploadIntentStatus.Expired || intent.ExpiresAt < DateTime.UtcNow) - return IntentValidationResult.Fail(Result.Invalid(new ValidationError("Upload intent has expired."))); - - var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); - if (metadata is null) - return IntentValidationResult.Fail(Result.NotFound("Uploaded object not found in storage. Upload the file first.")); - - if (metadata.ContentLength > config.MaxUploadBytes) - return IntentValidationResult.Fail(Result.Invalid(new ValidationError($"Uploaded file exceeds the maximum allowed size of {config.MaxUploadBytes} bytes."))); - - return IntentValidationResult.Ok(intent); - } - - internal static async Task CheckQuotaAsync( - IDatabase database, - UploadIntentConfiguration config, - ILogger logger, - Guid userId, - long requiredBytes, - int requiredIntentCount, - CancellationToken cancellationToken) - { - var activeIntents = await database.UploadIntents - .AsNoTracking() - .Where(i => i.UserId == userId && i.Status == UploadIntentStatus.Issued) - .ToListAsync(cancellationToken); - - if (activeIntents.Count + requiredIntentCount > config.MaxActiveUploadIntentsPerUser) - { - logger.LogWarning("User {UserId} exceeded max active upload intents ({Max})", userId, config.MaxActiveUploadIntentsPerUser); - return Result.Invalid(new ValidationError("Upload intent limit exceeded. Wait for existing uploads to complete or expire.")); - } - - var activeBytes = activeIntents.Sum(i => i.ExpectedSizeBytes ?? config.DefaultExpectedPictureSizeBytes); - if (activeBytes + requiredBytes > config.MaxActiveUploadBytesPerUser) - { - logger.LogWarning("User {UserId} exceeded max active upload bytes ({Max})", userId, config.MaxActiveUploadBytesPerUser); - return Result.Invalid(new ValidationError("Upload byte quota exceeded. Wait for existing uploads to complete or expire.")); - } - - return Result.Success(); - } - internal static Result ValidateExtension(string fileType, string[] allowedExtensions) { var extension = fileType.Trim(); diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 4e92451..47fa365 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -3,9 +3,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Abstractions.Application; using Musify.Application.Configuration; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Jobs; +using Musify.Application.Services; namespace Musify.Infrastructure.Services { @@ -54,6 +56,8 @@ namespace Musify.Infrastructure.Services serviceDescriptors.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); + serviceDescriptors.AddScoped(); + return serviceDescriptors; } From f62cf057d23ce26f2d22d1212a2dac4ab5deea56 Mon Sep 17 00:00:00 2001 From: NotS1mp Date: Thu, 14 May 2026 22:13:22 +0200 Subject: [PATCH 140/172] Fixes and a lot of changes --- .../Application/IUploadIntentService.cs | 23 --------------- .../Application/IFileData.cs | 2 +- .../Infrastructure/IAudioTranscoderService.cs | 2 +- .../Infrastructure/IDatabase.cs | 4 +-- .../Infrastructure/IDatabaseTransaction.cs | 2 +- .../Infrastructure/IEventBus.cs | 2 +- .../Infrastructure/IPictureService.cs | 2 +- .../Infrastructure/IStorageService.cs | 2 +- .../Infrastructure/ObjectMetadata.cs | 2 +- .../PaginatedResponse.cs | 4 +-- .../Handlers/CreatePlayListCommandHandler.cs | 8 ++--- .../Handlers/DeletePlayListCommandHandler.cs | 2 +- .../Handlers/GetPlayListByIdQueryHandler.cs | 2 +- .../GetPlayListsByUserIdQueryHandler.cs | 6 ++-- .../Handlers/GetPlayListsQueryHandler.cs | 6 ++-- ...uestPlayListPictureUploadCommandHandler.cs | 22 ++++++-------- .../Handlers/UpdatePlayListCommandHandler.cs | 8 ++--- .../Queries/GetPlayListsByUserIdQuery.cs | 4 +-- .../PlayLists/Queries/GetPlayListsQuery.cs | 4 +-- ...entService.cs => UploadIntentValidator.cs} | 15 ++-------- .../Handler/CreateTrackCommandHandler.cs | 10 +++---- .../Handler/DeleteTrackCommandHandler.cs | 2 +- .../Handler/GetTrackByIdQueryHandler.cs | 4 +-- .../Handler/GetTracksByUserIdQueryHandler.cs | 6 ++-- .../Tracks/Handler/GetTracksQueryHandler.cs | 6 ++-- .../RequestTrackUploadUrlsCommandHandler.cs | 29 +++++++------------ .../Tracks/Queries/GetTracksByUserIdQuery.cs | 4 +-- Application/Tracks/Queries/GetTracksQuery.cs | 4 +-- .../UploadIntents/UploadIntentHelpers.cs | 24 --------------- .../Handlers/CreateUserCommandHandler.cs | 2 +- .../Users/Handlers/GetUserByIdQueryHandler.cs | 2 +- .../Handlers/GetUserByKeycloakQueryHandler.cs | 2 +- .../Users/Handlers/GetUsersQueryHandler.cs | 4 +-- Application/Users/Queries/GetUsersQuery.cs | 2 +- Domain/Domain.csproj | 1 + Infrastructure/Jobs/TempUploadsCleanupJob.cs | 2 +- .../Jobs/UploadIntentExpirationJob.cs | 2 +- .../GenerateAudioWorkflowPathsActivity.cs | 2 +- .../Audio/TranscodeDashAudioActivity.cs | 2 +- .../Audio/UpdateTrackAudioActivity.cs | 2 +- .../Files/DownloadFileFromBucketActivity.cs | 2 +- .../Files/RemoveFileFromBucketActivity.cs | 2 +- .../Files/RemoveFolderFromBucketActivity.cs | 4 +-- .../Files/TransferFilesToBucketActivity.cs | 2 +- .../Files/UploadFileToBucketActivity.cs | 2 +- .../Pictures/ResizePictureActivity.cs | 2 +- .../Pictures/UpdatePlayListPictureActivity.cs | 2 +- .../Pictures/UpdateTrackPictureActivity.cs | 2 +- .../PlayLists/DeletePlayListFromDbActivity.cs | 2 +- .../MarkPlayListAsRemovingActivity.cs | 2 +- .../Tracks/DeleteTrackFromDbActivity.cs | 2 +- .../Tracks/MarkTrackAsRemovingActivity.cs | 2 +- .../MassTransitDependencyInjection.cs | 4 +-- .../EventBus/MassTransitEventBus.cs | 4 +-- .../DeletePlayListRoutingSlipBuilder.cs | 2 +- .../Builders/DeleteTrackRoutingSlipBuilder.cs | 2 +- Infrastructure/Persistence/Database.cs | 4 +-- .../DatabaseDependencyInjection.cs | 4 +-- .../Services/AudioTranscoderService.cs | 2 +- Infrastructure/Services/PictureService.cs | 2 +- .../Services/ServicesDependencyInjection.cs | 7 ++--- Infrastructure/Services/StorageService.cs | 4 +-- 62 files changed, 113 insertions(+), 182 deletions(-) delete mode 100644 Application/Abstractions/Application/IUploadIntentService.cs rename Application/{Abstractions => Contracts}/Application/IFileData.cs (76%) rename Application/{Abstractions => Contracts}/Infrastructure/IAudioTranscoderService.cs (82%) rename Application/{Abstractions => Contracts}/Infrastructure/IDatabase.cs (86%) rename Application/{Abstractions => Contracts}/Infrastructure/IDatabaseTransaction.cs (72%) rename Application/{Abstractions => Contracts}/Infrastructure/IEventBus.cs (70%) rename Application/{Abstractions => Contracts}/Infrastructure/IPictureService.cs (75%) rename Application/{Abstractions => Contracts}/Infrastructure/IStorageService.cs (96%) rename Application/{Abstractions => Contracts}/Infrastructure/ObjectMetadata.cs (57%) rename Application/{Abstractions/Application => Pagination}/PaginatedResponse.cs (89%) rename Application/Services/{UploadIntentService.cs => UploadIntentValidator.cs} (81%) delete mode 100644 Application/UploadIntents/UploadIntentHelpers.cs diff --git a/Application/Abstractions/Application/IUploadIntentService.cs b/Application/Abstractions/Application/IUploadIntentService.cs deleted file mode 100644 index 91c0bb7..0000000 --- a/Application/Abstractions/Application/IUploadIntentService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Ardalis.Result; -using Microsoft.Extensions.Logging; -using Musify.Application.Configuration; -using Musify.Domain.Entities; - -namespace Musify.Application.Abstractions.Application; - -public interface IUploadIntentService -{ - Task CheckQuotaAsync( - UploadIntentConfiguration config, - ILogger logger, - Guid userId, - long requiredBytes, - int requiredIntentCount, - CancellationToken cancellationToken); - - Task> ValidateAndLoadAsync( - UploadIntentConfiguration config, - Guid intentId, - Guid userId, - CancellationToken cancellationToken); -} diff --git a/Application/Abstractions/Application/IFileData.cs b/Application/Contracts/Application/IFileData.cs similarity index 76% rename from Application/Abstractions/Application/IFileData.cs rename to Application/Contracts/Application/IFileData.cs index 8d92e55..af7438c 100644 --- a/Application/Abstractions/Application/IFileData.cs +++ b/Application/Contracts/Application/IFileData.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Application +namespace Musify.Application.Contracts.Application { public interface IFileData { diff --git a/Application/Abstractions/Infrastructure/IAudioTranscoderService.cs b/Application/Contracts/Infrastructure/IAudioTranscoderService.cs similarity index 82% rename from Application/Abstractions/Infrastructure/IAudioTranscoderService.cs rename to Application/Contracts/Infrastructure/IAudioTranscoderService.cs index 69b86b3..41ec61d 100644 --- a/Application/Abstractions/Infrastructure/IAudioTranscoderService.cs +++ b/Application/Contracts/Infrastructure/IAudioTranscoderService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public interface IAudioTranscoderService { diff --git a/Application/Abstractions/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs similarity index 86% rename from Application/Abstractions/Infrastructure/IDatabase.cs rename to Application/Contracts/Infrastructure/IDatabase.cs index 9d87904..bf75e6a 100644 --- a/Application/Abstractions/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -1,7 +1,7 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Musify.Domain.Entities; -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public interface IDatabase { diff --git a/Application/Abstractions/Infrastructure/IDatabaseTransaction.cs b/Application/Contracts/Infrastructure/IDatabaseTransaction.cs similarity index 72% rename from Application/Abstractions/Infrastructure/IDatabaseTransaction.cs rename to Application/Contracts/Infrastructure/IDatabaseTransaction.cs index 9e5cddc..1aedc2e 100644 --- a/Application/Abstractions/Infrastructure/IDatabaseTransaction.cs +++ b/Application/Contracts/Infrastructure/IDatabaseTransaction.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public interface IDatabaseTransaction : IAsyncDisposable { diff --git a/Application/Abstractions/Infrastructure/IEventBus.cs b/Application/Contracts/Infrastructure/IEventBus.cs similarity index 70% rename from Application/Abstractions/Infrastructure/IEventBus.cs rename to Application/Contracts/Infrastructure/IEventBus.cs index 18925d0..2d41fea 100644 --- a/Application/Abstractions/Infrastructure/IEventBus.cs +++ b/Application/Contracts/Infrastructure/IEventBus.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public interface IEventBus { diff --git a/Application/Abstractions/Infrastructure/IPictureService.cs b/Application/Contracts/Infrastructure/IPictureService.cs similarity index 75% rename from Application/Abstractions/Infrastructure/IPictureService.cs rename to Application/Contracts/Infrastructure/IPictureService.cs index 666f6e5..f881fc7 100644 --- a/Application/Abstractions/Infrastructure/IPictureService.cs +++ b/Application/Contracts/Infrastructure/IPictureService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public interface IPictureService { diff --git a/Application/Abstractions/Infrastructure/IStorageService.cs b/Application/Contracts/Infrastructure/IStorageService.cs similarity index 96% rename from Application/Abstractions/Infrastructure/IStorageService.cs rename to Application/Contracts/Infrastructure/IStorageService.cs index cb1c49d..f58df25 100644 --- a/Application/Abstractions/Infrastructure/IStorageService.cs +++ b/Application/Contracts/Infrastructure/IStorageService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public interface IStorageService { diff --git a/Application/Abstractions/Infrastructure/ObjectMetadata.cs b/Application/Contracts/Infrastructure/ObjectMetadata.cs similarity index 57% rename from Application/Abstractions/Infrastructure/ObjectMetadata.cs rename to Application/Contracts/Infrastructure/ObjectMetadata.cs index f92904e..13061c9 100644 --- a/Application/Abstractions/Infrastructure/ObjectMetadata.cs +++ b/Application/Contracts/Infrastructure/ObjectMetadata.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Abstractions.Infrastructure +namespace Musify.Application.Contracts.Infrastructure { public record ObjectMetadata(string ContentType, long ContentLength); } diff --git a/Application/Abstractions/Application/PaginatedResponse.cs b/Application/Pagination/PaginatedResponse.cs similarity index 89% rename from Application/Abstractions/Application/PaginatedResponse.cs rename to Application/Pagination/PaginatedResponse.cs index fe38d61..57a24ab 100644 --- a/Application/Abstractions/Application/PaginatedResponse.cs +++ b/Application/Pagination/PaginatedResponse.cs @@ -1,6 +1,6 @@ -using X.PagedList; +using X.PagedList; -namespace Musify.Application.Abstractions.Application +namespace Musify.Application.Pagination { public record PaginatedResponse( IReadOnlyCollection Items, diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 930a718..13e67e4 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -3,13 +3,13 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.UploadIntents; using Musify.Application.Extensions; -using Musify.Application.Abstractions.Application; +using Musify.Application.Services; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -17,7 +17,7 @@ namespace Musify.Application.PlayLists.Handlers public class CreatePlayListCommandHandler( IEventBus eventBus, IDatabase database, - IUploadIntentService uploadIntentService, + UploadIntentValidator uploadIntentValidator, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -41,7 +41,7 @@ namespace Musify.Application.PlayLists.Handlers if (request.PictureIntentId.HasValue) { - var validation = await uploadIntentService.ValidateAndLoadAsync( + var validation = await uploadIntentValidator.ValidateAndLoadAsync( uploadIntentConfiguration, request.PictureIntentId.Value, request.UserId, cancellationToken); if (!validation.IsSuccess) diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index e7be170..dbb6acb 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -2,7 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Domain.Entities; diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index f54ae91..3c65440 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using Mediator; diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index 8cfd94d..b493509 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -1,7 +1,7 @@ -using Ardalis.Result; +using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Application; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Pagination; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; diff --git a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index e05fbe2..89b3b24 100644 --- a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -1,7 +1,7 @@ -using Ardalis.Result; +using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Application; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Pagination; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index ee6b51b..77de4cb 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -2,19 +2,18 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Application.Abstractions.Application; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; -using Musify.Application.UploadIntents; +using Musify.Application.Services; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers { public class RequestPlayListPictureUploadCommandHandler( IDatabase database, - IUploadIntentService uploadIntentService, + UploadIntentValidator uploadIntentValidator, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -22,8 +21,6 @@ namespace Musify.Application.PlayLists.Handlers UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { - static readonly string[] AllowedExtensions = [".webp", ".png", ".jpg", ".jpeg"]; - public async ValueTask> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) { var userExists = await database.Users @@ -35,14 +32,10 @@ namespace Musify.Application.PlayLists.Handlers return Result.NotFound($"User {request.UserId} not found"); } - var extensionResult = UploadIntentHelpers.ValidateExtension(request.FileType, AllowedExtensions); - if (!extensionResult.IsSuccess) - return Result.Invalid(extensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); - var effectiveSizeBytes = request.ExpectedSizeBytes ?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes; - var objectName = Guid.NewGuid() + extensionResult.Value; + var objectName = $"{Guid.NewGuid()}.{request.FileType.TrimStart('.').ToLowerInvariant()}"; var tempKey = playListConfiguration.Routes.BuildTempPicturePath( uploadIntentConfiguration.TempRootPrefix, request.UserId, objectName); @@ -59,11 +52,14 @@ namespace Musify.Application.PlayLists.Handlers await using var transaction = await database.BeginTransactionAsync( System.Data.IsolationLevel.Serializable, cancellationToken); - var quotaCheck = await uploadIntentService.CheckQuotaAsync( - uploadIntentConfiguration, logger, + var quotaCheck = await uploadIntentValidator.CheckQuotaAsync( + uploadIntentConfiguration, request.UserId, effectiveSizeBytes, 1, cancellationToken); if (!quotaCheck.IsSuccess) + { + logger.LogWarning("User {UserId} failed upload intent quota check", request.UserId); return quotaCheck; + } var intent = new UploadIntent { diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 70b064a..e4e3464 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -3,13 +3,13 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.UploadIntents; using Musify.Application.Extensions; -using Musify.Application.Abstractions.Application; +using Musify.Application.Services; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -17,7 +17,7 @@ namespace Musify.Application.PlayLists.Handlers public class UpdatePlayListCommandHandler( IEventBus eventBus, IDatabase database, - IUploadIntentService uploadIntentService, + UploadIntentValidator uploadIntentValidator, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -53,7 +53,7 @@ namespace Musify.Application.PlayLists.Handlers if (request.NewPictureIntentId.HasValue) { - var validation = await uploadIntentService.ValidateAndLoadAsync( + var validation = await uploadIntentValidator.ValidateAndLoadAsync( uploadIntentConfiguration, request.NewPictureIntentId.Value, request.UserId, cancellationToken); if (!validation.IsSuccess) diff --git a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index 788853c..c2e9da5 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -1,6 +1,6 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; +using Musify.Application.Pagination; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries diff --git a/Application/PlayLists/Queries/GetPlayListsQuery.cs b/Application/PlayLists/Queries/GetPlayListsQuery.cs index f532d99..18d7b44 100644 --- a/Application/PlayLists/Queries/GetPlayListsQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsQuery.cs @@ -1,6 +1,6 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; +using Musify.Application.Pagination; using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries diff --git a/Application/Services/UploadIntentService.cs b/Application/Services/UploadIntentValidator.cs similarity index 81% rename from Application/Services/UploadIntentService.cs rename to Application/Services/UploadIntentValidator.cs index f3c736f..aeb43aa 100644 --- a/Application/Services/UploadIntentService.cs +++ b/Application/Services/UploadIntentValidator.cs @@ -1,20 +1,17 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Application.Abstractions.Application; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Domain.Entities; namespace Musify.Application.Services; -public sealed class UploadIntentService( +public sealed class UploadIntentValidator( IDatabase database, - IStorageService storageService) : IUploadIntentService + IStorageService storageService) { public async Task CheckQuotaAsync( UploadIntentConfiguration config, - ILogger logger, Guid userId, long requiredBytes, int requiredIntentCount, @@ -26,17 +23,11 @@ public sealed class UploadIntentService( .ToListAsync(cancellationToken); if (activeIntents.Count + requiredIntentCount > config.MaxActiveUploadIntentsPerUser) - { - logger.LogWarning("User {UserId} exceeded max active upload intents ({Max})", userId, config.MaxActiveUploadIntentsPerUser); return Result.Invalid(new ValidationError("Upload intent limit exceeded. Wait for existing uploads to complete or expire.")); - } var activeBytes = activeIntents.Sum(i => i.ExpectedSizeBytes ?? config.DefaultExpectedPictureSizeBytes); if (activeBytes + requiredBytes > config.MaxActiveUploadBytesPerUser) - { - logger.LogWarning("User {UserId} exceeded max active upload bytes ({Max})", userId, config.MaxActiveUploadBytesPerUser); return Result.Invalid(new ValidationError("Upload byte quota exceeded. Wait for existing uploads to complete or expire.")); - } return Result.Success(); } diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 7578ed3..fc58123 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -3,13 +3,13 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; using Musify.Application.UploadIntents; using Musify.Application.Extensions; -using Musify.Application.Abstractions.Application; +using Musify.Application.Services; using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler @@ -17,7 +17,7 @@ namespace Musify.Application.Tracks.Handler public class CreateTrackCommandHandler( IDatabase database, IEventBus eventBus, - IUploadIntentService uploadIntentService, + UploadIntentValidator uploadIntentValidator, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -35,13 +35,13 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } - var pictureValidation = await uploadIntentService.ValidateAndLoadAsync( + var pictureValidation = await uploadIntentValidator.ValidateAndLoadAsync( uploadIntentConfiguration, request.PictureIntentId, request.UserId, cancellationToken); if (!pictureValidation.IsSuccess) return pictureValidation.As(); - var audioValidation = await uploadIntentService.ValidateAndLoadAsync( + var audioValidation = await uploadIntentValidator.ValidateAndLoadAsync( uploadIntentConfiguration, request.AudioIntentId, request.UserId, cancellationToken); if (!audioValidation.IsSuccess) diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs index 069254a..66a85c5 100644 --- a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -2,7 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Commands; using Musify.Domain.Entities; using Musify.Application.Events; diff --git a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs index 85607e3..df277c0 100644 --- a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs @@ -1,7 +1,7 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; diff --git a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs index 9710a2b..6412908 100644 --- a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs @@ -1,8 +1,8 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Application; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Pagination; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; using X.PagedList.EF; diff --git a/Application/Tracks/Handler/GetTracksQueryHandler.cs b/Application/Tracks/Handler/GetTracksQueryHandler.cs index c1875d1..2219fda 100644 --- a/Application/Tracks/Handler/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handler/GetTracksQueryHandler.cs @@ -1,8 +1,8 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Application; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Pagination; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; using X.PagedList.EF; diff --git a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs index d40fbf8..22cf8a4 100644 --- a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs +++ b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs @@ -2,19 +2,18 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Application; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; +using Musify.Application.Services; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; -using Musify.Application.UploadIntents; using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler { public class RequestTrackUploadUrlsCommandHandler( IDatabase database, - IUploadIntentService uploadIntentService, + UploadIntentValidator uploadIntentValidator, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -22,9 +21,6 @@ namespace Musify.Application.Tracks.Handler UploadIntentConfiguration uploadIntentConfiguration) : ICommandHandler> { - static readonly string[] AllowedPictureExtensions = [".webp", ".png", ".jpg", ".jpeg"]; - static readonly string[] AllowedAudioExtensions = [".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".opus"]; - public async ValueTask> Handle(RequestTrackUploadUrlsCommand request, CancellationToken cancellationToken) { var userExists = await database.Users @@ -36,21 +32,13 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } - var pictureExtensionResult = UploadIntentHelpers.ValidateExtension(request.PictureFileType, AllowedPictureExtensions); - if (!pictureExtensionResult.IsSuccess) - return Result.Invalid(pictureExtensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); - - var audioExtensionResult = UploadIntentHelpers.ValidateExtension(request.AudioFileType, AllowedAudioExtensions); - if (!audioExtensionResult.IsSuccess) - return Result.Invalid(audioExtensionResult.Errors.Select(error => new ValidationError(error)).ToArray()); - var effectivePictureSize = request.ExpectedPictureSizeBytes ?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes; var effectiveAudioSize = request.ExpectedAudioSizeBytes ?? uploadIntentConfiguration.DefaultExpectedAudioSizeBytes; - var pictureObjectName = Guid.NewGuid() + pictureExtensionResult.Value; - var audioObjectName = Guid.NewGuid() + audioExtensionResult.Value; + var pictureObjectName = $"{Guid.NewGuid()}.{request.PictureFileType.TrimStart('.').ToLowerInvariant()}"; + var audioObjectName = $"{Guid.NewGuid()}.{request.AudioFileType.TrimStart('.').ToLowerInvariant()}"; var tempPictureKey = trackConfiguration.Routes.BuildTempPicturePath( uploadIntentConfiguration.TempRootPrefix, request.UserId, pictureObjectName); @@ -80,11 +68,14 @@ namespace Musify.Application.Tracks.Handler await using var transaction = await database.BeginTransactionAsync( System.Data.IsolationLevel.Serializable, cancellationToken); - var quotaCheck = await uploadIntentService.CheckQuotaAsync( - uploadIntentConfiguration, logger, + var quotaCheck = await uploadIntentValidator.CheckQuotaAsync( + uploadIntentConfiguration, request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken); if (!quotaCheck.IsSuccess) + { + logger.LogWarning("User {UserId} failed upload intent quota check", request.UserId); return quotaCheck; + } var pictureIntent = new UploadIntent { diff --git a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs index 21c378e..00a00f6 100644 --- a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs +++ b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs @@ -1,6 +1,6 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; +using Musify.Application.Pagination; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries diff --git a/Application/Tracks/Queries/GetTracksQuery.cs b/Application/Tracks/Queries/GetTracksQuery.cs index 8b0dd64..b483e56 100644 --- a/Application/Tracks/Queries/GetTracksQuery.cs +++ b/Application/Tracks/Queries/GetTracksQuery.cs @@ -1,6 +1,6 @@ -using Ardalis.Result; +using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; +using Musify.Application.Pagination; using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries diff --git a/Application/UploadIntents/UploadIntentHelpers.cs b/Application/UploadIntents/UploadIntentHelpers.cs deleted file mode 100644 index 8551a27..0000000 --- a/Application/UploadIntents/UploadIntentHelpers.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Ardalis.Result; - -namespace Musify.Application.UploadIntents -{ - internal static class UploadIntentHelpers - { - internal static Result ValidateExtension(string fileType, string[] allowedExtensions) - { - var extension = fileType.Trim(); - if (!extension.StartsWith('.')) - extension = "." + extension; - - extension = extension.ToLowerInvariant(); - - if (extension.Contains('/') || extension.Contains('\\')) - return Result.Error("Invalid FileType"); - - if (!allowedExtensions.Contains(extension)) - return Result.Error($"Unsupported FileType '{extension}'"); - - return Result.Success(extension); - } - } -} diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index 3fd02cf..84a9c0e 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -2,7 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Commands; using Musify.Application.Users.Responses; diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index 6441884..1d14715 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index 26c3c51..10fb204 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index 4d474c2..4688869 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -1,8 +1,8 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Application; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Pagination; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; using X.PagedList.EF; diff --git a/Application/Users/Queries/GetUsersQuery.cs b/Application/Users/Queries/GetUsersQuery.cs index 5d78679..edc2e7b 100644 --- a/Application/Users/Queries/GetUsersQuery.cs +++ b/Application/Users/Queries/GetUsersQuery.cs @@ -1,6 +1,6 @@ using Ardalis.Result; using Mediator; -using Musify.Application.Abstractions.Application; +using Musify.Application.Pagination; using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index 879a55c..6338edf 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -11,4 +11,5 @@ https://github.com/XShine555/MusifyBackend + diff --git a/Infrastructure/Jobs/TempUploadsCleanupJob.cs b/Infrastructure/Jobs/TempUploadsCleanupJob.cs index ac83776..745b6bc 100644 --- a/Infrastructure/Jobs/TempUploadsCleanupJob.cs +++ b/Infrastructure/Jobs/TempUploadsCleanupJob.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; namespace Musify.Infrastructure.Jobs diff --git a/Infrastructure/Jobs/UploadIntentExpirationJob.cs b/Infrastructure/Jobs/UploadIntentExpirationJob.cs index 3b46a44..ce74e86 100644 --- a/Infrastructure/Jobs/UploadIntentExpirationJob.cs +++ b/Infrastructure/Jobs/UploadIntentExpirationJob.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Domain.Entities; diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 606f66e..73f4dcc 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 6da0ffb..4766ae5 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 7fc302d..cf32e81 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index 3612ffa..f7d7db8 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index a28baa9..911baf9 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Amazon.S3; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using System.Net; diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs index 6170921..91addc2 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs @@ -1,6 +1,6 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Arguments; namespace Musify.Infrastructure.MassTransit.Activities.Files diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 70dd5b5..5f6ceb8 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index 286feff..d3e18b6 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 90b9c3c..941132b 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 2a7afb2..c6df282 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 8f56c5c..3c807b8 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Logs; diff --git a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs index e929f29..8cff6f7 100644 --- a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs +++ b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; namespace Musify.Infrastructure.MassTransit.Activities.PlayLists { diff --git a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs index 67d09ad..0f6084a 100644 --- a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs +++ b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; namespace Musify.Infrastructure.MassTransit.Activities.PlayLists diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs index f1a5d0d..5e3cbf3 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; namespace Musify.Infrastructure.MassTransit.Activities.Tracks { diff --git a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs index 4290155..9f07a62 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; namespace Musify.Infrastructure.MassTransit.Activities.Tracks diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 1bc01e6..1c497ce 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -1,7 +1,7 @@ -using MassTransit; +using MassTransit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Persistence; diff --git a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs index 50b398c..23f69de 100644 --- a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs +++ b/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs @@ -1,5 +1,5 @@ -using MassTransit; -using Musify.Application.Abstractions.Infrastructure; +using MassTransit; +using Musify.Application.Contracts.Infrastructure; namespace Musify.Infrastructure.MassTransit { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs index 232744c..f12750d 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs @@ -1,7 +1,7 @@ using MassTransit; using MassTransit.Courier.Contracts; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Arguments; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index 6da734e..ca161aa 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -1,7 +1,7 @@ using MassTransit; using MassTransit.Courier.Contracts; using Microsoft.EntityFrameworkCore; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Arguments; diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index c4df933..f74cf4f 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -1,10 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using MassTransit; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; -using AppIDatabase = Musify.Application.Abstractions.Infrastructure.IDatabase; +using AppIDatabase = Musify.Application.Contracts.Infrastructure.IDatabase; namespace Musify.Infrastructure.Persistence { diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index c5db7ac..a5eb939 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Persistence diff --git a/Infrastructure/Services/AudioTranscoderService.cs b/Infrastructure/Services/AudioTranscoderService.cs index f7086ac..1bec040 100644 --- a/Infrastructure/Services/AudioTranscoderService.cs +++ b/Infrastructure/Services/AudioTranscoderService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Infrastructure.Configuration; using System.Diagnostics; diff --git a/Infrastructure/Services/PictureService.cs b/Infrastructure/Services/PictureService.cs index 390a487..f50f9ba 100644 --- a/Infrastructure/Services/PictureService.cs +++ b/Infrastructure/Services/PictureService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 47fa365..04176c5 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -1,9 +1,8 @@ -using Amazon.S3; +using Amazon.S3; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Application.Abstractions.Application; +using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Jobs; @@ -56,7 +55,7 @@ namespace Musify.Infrastructure.Services serviceDescriptors.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); - serviceDescriptors.AddScoped(); + serviceDescriptors.AddScoped(); return serviceDescriptors; } diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 3c2efaf..757499c 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -3,10 +3,10 @@ using Amazon.S3.Model; using Amazon.S3.Transfer; using Microsoft.Extensions.Logging; using MimeMapping; -using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; -using ObjectMetadata = Musify.Application.Abstractions.Infrastructure.ObjectMetadata; +using ObjectMetadata = Musify.Application.Contracts.Infrastructure.ObjectMetadata; namespace Musify.Infrastructure.Services { From e003c186b89e6a2c9fd5db0c60c0751227593d26 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 22:15:26 +0200 Subject: [PATCH 141/172] Compilation fixes --- .../PlayLists/Handlers/CreatePlayListCommandHandler.cs | 1 - .../PlayLists/Handlers/UpdatePlayListCommandHandler.cs | 1 - .../Tracks/Handler/CreateTrackCommandHandler.cs | 1 - ...loadsCleanupJob.cs => TemporalUploadsCleanupJob.cs} | 10 +++++----- Infrastructure/Services/ServicesDependencyInjection.cs | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) rename Infrastructure/Jobs/{TempUploadsCleanupJob.cs => TemporalUploadsCleanupJob.cs} (84%) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 13e67e4..fe9620b 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -7,7 +7,6 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; -using Musify.Application.UploadIntents; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index e4e3464..c1952d8 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -7,7 +7,6 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; -using Musify.Application.UploadIntents; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index fc58123..9670714 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -7,7 +7,6 @@ using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; -using Musify.Application.UploadIntents; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; diff --git a/Infrastructure/Jobs/TempUploadsCleanupJob.cs b/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs similarity index 84% rename from Infrastructure/Jobs/TempUploadsCleanupJob.cs rename to Infrastructure/Jobs/TemporalUploadsCleanupJob.cs index 745b6bc..24b71f4 100644 --- a/Infrastructure/Jobs/TempUploadsCleanupJob.cs +++ b/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs @@ -6,16 +6,16 @@ using Musify.Application.Configuration; namespace Musify.Infrastructure.Jobs { - public class TempUploadsCleanupJob( + public class TemporalUploadsCleanupJob( IServiceScopeFactory scopeFactory, - ILogger logger, + ILogger logger, ApplicationStorageConfiguration storageConfiguration, UploadIntentConfiguration uploadIntentConfiguration) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - logger.LogInformation("TempUploadsCleanupJob started (interval: {Interval}s, prefix: {Prefix})", + logger.LogInformation("TemporalUploadsCleanupJob started (interval: {Interval}s, prefix: {Prefix})", uploadIntentConfiguration.TempCleanupJobIntervalSeconds, uploadIntentConfiguration.TempRootPrefix); @@ -27,7 +27,7 @@ namespace Musify.Infrastructure.Jobs } catch (Exception exception) when (exception is not OperationCanceledException) { - logger.LogError(exception, "Error in TempUploadsCleanupJob"); + logger.LogError(exception, "Error in TemporalUploadsCleanupJob"); } await Task.Delay(TimeSpan.FromSeconds(uploadIntentConfiguration.TempCleanupJobIntervalSeconds), stoppingToken); @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.Jobs } if (deleted > 0 || errors > 0) - logger.LogInformation("TempUploadsCleanupJob: deleted {Deleted} orphaned objects, {Errors} errors", deleted, errors); + logger.LogInformation("TemporalUploadsCleanupJob: deleted {Deleted} orphaned objects, {Errors} errors", deleted, errors); } } } diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 04176c5..af6ed83 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -66,7 +66,7 @@ namespace Musify.Infrastructure.Services serviceDescriptors.AddUploadIntentConfiguration(configuration); serviceDescriptors.AddHostedService(); - serviceDescriptors.AddHostedService(); + serviceDescriptors.AddHostedService(); return serviceDescriptors; } From 868af6dc49a986c9988b26022312f84194b7f3ff Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 22:22:47 +0200 Subject: [PATCH 142/172] Changes --- .../Handlers/RequestPlayListPictureUploadCommandHandler.cs | 2 +- Domain/Entities/UploadIntent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index 77de4cb..d1ccb96 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -35,7 +35,7 @@ namespace Musify.Application.PlayLists.Handlers var effectiveSizeBytes = request.ExpectedSizeBytes ?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes; - var objectName = $"{Guid.NewGuid()}.{request.FileType.TrimStart('.').ToLowerInvariant()}"; + var objectName = $"{Guid.NewGuid() }.{request.FileType.TrimStart('.').ToLowerInvariant() }"; var tempKey = playListConfiguration.Routes.BuildTempPicturePath( uploadIntentConfiguration.TempRootPrefix, request.UserId, objectName); diff --git a/Domain/Entities/UploadIntent.cs b/Domain/Entities/UploadIntent.cs index e15d5c7..3502950 100644 --- a/Domain/Entities/UploadIntent.cs +++ b/Domain/Entities/UploadIntent.cs @@ -40,7 +40,7 @@ namespace Musify.Domain.Entities [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - [ForeignKey(nameof(UserId))] + [ForeignKey(nameof(UserId)) ] public User User { get; set; } = null!; } } From 5c75b611f05f9001d701cfa0c268612dbdb7cee3 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 18:37:54 +0200 Subject: [PATCH 143/172] 1 --- .../Events/CreatePlayListResourcesEvent.cs | 12 ++ .../Events/CreateTrackResourcesEvent.cs | 16 +++ .../UpdatePlayListPictureSourceEvent.cs | 12 ++ .../Handlers/CreatePlayListCommandHandler.cs | 70 ++++++------ .../Handlers/UpdatePlayListCommandHandler.cs | 75 +++++++------ .../Handler/CreateTrackCommandHandler.cs | 103 ++++++------------ .../GenerateAudioWorkflowPathsActivity.cs | 1 - .../Files/CopyFileInBucketActivity.cs | 61 +++++++++++ .../Files/RemoveFolderFromBucketActivity.cs | 42 ------- .../GeneratePictureWorkflowPathsActivity.cs | 6 +- ...hPlayListPictureProcessingEventActivity.cs | 34 ++++++ .../PublishTrackProcessingEventsActivity.cs | 43 ++++++++ .../ConsumeUploadIntentsActivity.cs | 62 +++++++++++ .../ConsumeUploadIntentsArguments.cs | 5 + .../Arguments/CopyFileInBucketArguments.cs | 8 ++ ...PlayListPictureProcessingEventArguments.cs | 12 ++ .../PublishTrackProcessingEventsArguments.cs | 14 +++ .../RemoveFolderFromBucketArguments.cs | 6 - .../Consumers/CreatePlayListConsumer.cs | 23 ++++ .../Consumers/CreateTrackConsumer.cs | 23 ++++ .../UpdatePlayListPictureSourceConsumer.cs | 23 ++++ ...TransitDependencyInjection.Registration.cs | 45 ++++++++ .../Logs/GenerateAudioWorkflowPathsLog.cs | 4 - .../MassTransit/RoutingSlip/ActivityNames.cs | 30 ++++- .../Builders/CreateTrackRoutingSlipBuilder.cs | 63 +++++++++++ .../DeletePlayListRoutingSlipBuilder.cs | 8 +- .../Builders/DeleteTrackRoutingSlipBuilder.cs | 14 +-- ...PlayListPictureSourceRoutingSlipBuilder.cs | 84 ++++++++++++++ .../Variables/RoutingSlipVariableNames.cs | 3 - 29 files changed, 683 insertions(+), 219 deletions(-) create mode 100644 Application/Events/CreatePlayListResourcesEvent.cs create mode 100644 Application/Events/CreateTrackResourcesEvent.cs create mode 100644 Application/Events/UpdatePlayListPictureSourceEvent.cs create mode 100644 Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs delete mode 100644 Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/PlayLists/PublishPlayListPictureProcessingEventActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/Tracks/PublishTrackProcessingEventsActivity.cs create mode 100644 Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs create mode 100644 Infrastructure/MassTransit/Arguments/ConsumeUploadIntentsArguments.cs create mode 100644 Infrastructure/MassTransit/Arguments/CopyFileInBucketArguments.cs create mode 100644 Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs create mode 100644 Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs delete mode 100644 Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs create mode 100644 Infrastructure/MassTransit/Consumers/CreatePlayListConsumer.cs create mode 100644 Infrastructure/MassTransit/Consumers/CreateTrackConsumer.cs create mode 100644 Infrastructure/MassTransit/Consumers/UpdatePlayListPictureSourceConsumer.cs delete mode 100644 Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Builders/CreateTrackRoutingSlipBuilder.cs create mode 100644 Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs diff --git a/Application/Events/CreatePlayListResourcesEvent.cs b/Application/Events/CreatePlayListResourcesEvent.cs new file mode 100644 index 0000000..89523fb --- /dev/null +++ b/Application/Events/CreatePlayListResourcesEvent.cs @@ -0,0 +1,12 @@ +namespace Musify.Application.Events +{ + public record CreatePlayListResourcesEvent( + Guid PlayListId, + Guid PictureIntentId, + string Bucket, + string PictureSourceKey, + string PictureDestinationKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); +} diff --git a/Application/Events/CreateTrackResourcesEvent.cs b/Application/Events/CreateTrackResourcesEvent.cs new file mode 100644 index 0000000..987ce4c --- /dev/null +++ b/Application/Events/CreateTrackResourcesEvent.cs @@ -0,0 +1,16 @@ +namespace Musify.Application.Events +{ + public record CreateTrackResourcesEvent( + Guid TrackId, + Guid PictureIntentId, + Guid AudioIntentId, + string Bucket, + string PictureSourceKey, + string PictureDestinationKey, + string AudioSourceKey, + string AudioDestinationKey, + string AudioProcessedFolderKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); +} diff --git a/Application/Events/UpdatePlayListPictureSourceEvent.cs b/Application/Events/UpdatePlayListPictureSourceEvent.cs new file mode 100644 index 0000000..2638c8d --- /dev/null +++ b/Application/Events/UpdatePlayListPictureSourceEvent.cs @@ -0,0 +1,12 @@ +namespace Musify.Application.Events +{ + public record UpdatePlayListPictureSourceEvent( + Guid PlayListId, + Guid PictureIntentId, + string Bucket, + string PictureSourceKey, + string PictureDestinationKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); +} diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index fe9620b..85dd51b 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -17,7 +17,6 @@ namespace Musify.Application.PlayLists.Handlers IEventBus eventBus, IDatabase database, UploadIntentValidator uploadIntentValidator, - IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, @@ -35,8 +34,8 @@ namespace Musify.Application.PlayLists.Handlers return Result.NotFound($"User {request.UserId} not found"); } - string originalPictureName; UploadIntent? pictureIntent = null; + string originalPictureName; if (request.PictureIntentId.HasValue) { @@ -48,17 +47,6 @@ namespace Musify.Application.PlayLists.Handlers pictureIntent = validation.Value; originalPictureName = pictureIntent.ObjectName; - - var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName); - try - { - await storageService.CopyFileAsync(pictureIntent.Bucket, pictureIntent.Key, pictureIntent.Bucket, finalKey, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to copy playlist picture from temp {TempKey} to final {FinalKey}", pictureIntent.Key, finalKey); - return Result.Error("Failed to move uploaded file to its final location."); - } } else { @@ -81,9 +69,9 @@ namespace Musify.Application.PlayLists.Handlers if (pictureIntent is not null) { - pictureIntent.Status = UploadIntentStatus.Consumed; - - var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken); + var finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName); + var publishResult = await PublishCreatePlayListEventAsync( + playList.Id, pictureIntent, finalPictureKey, cancellationToken); if (!publishResult.IsSuccess) return publishResult; } @@ -102,35 +90,41 @@ namespace Musify.Application.PlayLists.Handlers return Result.Created(PlayListApplicationResponse.FromEntity(playList)); } - async Task PublishPictureUpdateEventAsync(PlayList playList, CancellationToken cancellationToken) + async Task> PublishCreatePlayListEventAsync( + Guid playListId, + UploadIntent pictureIntent, + string finalPictureKey, + CancellationToken cancellationToken) { try { - var updateEvent = new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.Bucket, - playListConfiguration.Routes.BuildOriginalPicturePath(playList.UserId, playList.OriginalPictureName), - new ImageSize( - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)); - - await eventBus.PublishAsync(updateEvent, cancellationToken); + await eventBus.PublishAsync( + new CreatePlayListResourcesEvent( + playListId, + pictureIntent.Id, + storageConfiguration.Bucket, + pictureIntent.Key, + finalPictureKey, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); return Result.Success(); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish picture update event for playlist {PlayListId}", playList.Id); - return Result.Error($"Failed to publish picture update event for playlist {playList.Id}"); + logger.LogError(exception, "Failed to publish create playlist event for playlist {PlayListId}", playListId); + return Result.Error($"Failed to publish create playlist event for playlist {playListId}"); } } } -} \ No newline at end of file +} diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index c1952d8..aeea8f1 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -17,7 +17,6 @@ namespace Musify.Application.PlayLists.Handlers IEventBus eventBus, IDatabase database, UploadIntentValidator uploadIntentValidator, - IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, @@ -50,6 +49,9 @@ namespace Musify.Application.PlayLists.Handlers playListEntity.Description = request.NewDescription; } + UploadIntent? pictureIntent = null; + string? finalPictureKey = null; + if (request.NewPictureIntentId.HasValue) { var validation = await uploadIntentValidator.ValidateAndLoadAsync( @@ -58,29 +60,23 @@ namespace Musify.Application.PlayLists.Handlers if (!validation.IsSuccess) return validation.As(); - var intent = validation.Value; - var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName); - try - { - await storageService.CopyFileAsync(intent.Bucket, intent.Key, intent.Bucket, finalKey, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to copy playlist picture from temp {TempKey} to final {FinalKey}", intent.Key, finalKey); - return Result.Error("Failed to move uploaded file to its final location."); - } + pictureIntent = validation.Value; + playListEntity.OriginalPictureName = pictureIntent.ObjectName; + finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); + } - playListEntity.OriginalPictureName = intent.ObjectName; - intent.Status = UploadIntentStatus.Consumed; + database.PlayLists.Update(playListEntity); - var publishResult = await PublishPlayListPictureEvent(playListEntity.UserId, playListEntity.Id, playListEntity.OriginalPictureName, cancellationToken); + if (pictureIntent is not null && finalPictureKey is not null) + { + var publishResult = await PublishUpdatePlayListPictureSourceEventAsync( + playListEntity.Id, pictureIntent, finalPictureKey, cancellationToken); if (!publishResult.IsSuccess) return publishResult; } try { - database.PlayLists.Update(playListEntity); await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) @@ -93,34 +89,41 @@ namespace Musify.Application.PlayLists.Handlers return Result.Success(PlayListApplicationResponse.FromEntity(playListEntity)); } - async Task PublishPlayListPictureEvent(Guid userId, Guid playListId, string originalPictureName, CancellationToken cancellationToken) + async Task> PublishUpdatePlayListPictureSourceEventAsync( + Guid playListId, + UploadIntent pictureIntent, + string finalPictureKey, + CancellationToken cancellationToken) { try { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playListId, - storageConfiguration.Bucket, - playListConfiguration.Routes.BuildOriginalPicturePath(userId, originalPictureName), - new ImageSize( - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)), + await eventBus.PublishAsync( + new UpdatePlayListPictureSourceEvent( + playListId, + pictureIntent.Id, + storageConfiguration.Bucket, + pictureIntent.Key, + finalPictureKey, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); return Result.Success(); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playListId); - return Result.Error($"Failed to publish playlist {playListId} picture update event"); + logger.LogError(exception, "Failed to publish update playlist picture source event for playlist {PlayListId}", playListId); + return Result.Error($"Failed to publish update playlist picture source event for playlist {playListId}"); } } } -} \ No newline at end of file +} diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 9670714..7f4fbe9 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -17,7 +17,6 @@ namespace Musify.Application.Tracks.Handler IDatabase database, IEventBus eventBus, UploadIntentValidator uploadIntentValidator, - IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, TrackConfiguration trackConfiguration, @@ -51,17 +50,7 @@ namespace Musify.Application.Tracks.Handler var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName); - - try - { - await storageService.CopyFileAsync(pictureIntent.Bucket, pictureIntent.Key, pictureIntent.Bucket, finalPictureKey, cancellationToken); - await storageService.CopyFileAsync(audioIntent.Bucket, audioIntent.Key, audioIntent.Bucket, finalAudioKey, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to copy track files from temp to final location for user {UserId}", request.UserId); - return Result.Error("Failed to move uploaded files to their final locations."); - } + var audioProcessedFolderKey = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString()); var trackEntity = new Track { @@ -78,16 +67,38 @@ namespace Musify.Application.Tracks.Handler await database.Tracks.AddAsync(trackEntity, cancellationToken); - pictureIntent.Status = UploadIntentStatus.Consumed; - audioIntent.Status = UploadIntentStatus.Consumed; - - var publishPictureResult = await PublishUpdateEvent(request.UserId, trackEntity, cancellationToken); - if (!publishPictureResult.IsSuccess) - return Result.Error($"Failed to create track for {request.Title}"); - - var publishTranscodeResult = await PublishTranscodeEvent(request.UserId, trackEntity, cancellationToken); - if (!publishTranscodeResult.IsSuccess) + try + { + await eventBus.PublishAsync( + new CreateTrackResourcesEvent( + trackEntity.Id, + pictureIntent.Id, + audioIntent.Id, + storageConfiguration.Bucket, + pictureIntent.Key, + finalPictureKey, + audioIntent.Key, + finalAudioKey, + audioProcessedFolderKey, + new ImageSize( + trackConfiguration.Routes.SmallPicturesPath, + trackConfiguration.PicturesSizes.SmallPictureWidth, + trackConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + trackConfiguration.Routes.MediumPicturesPath, + trackConfiguration.PicturesSizes.MediumPictureWidth, + trackConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + trackConfiguration.Routes.LargePicturesPath, + trackConfiguration.PicturesSizes.LargePictureWidth, + trackConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish create track event for track {TrackId}", trackEntity.Id); return Result.Error($"Failed to create track for {request.Title}"); + } try { @@ -101,55 +112,5 @@ namespace Musify.Application.Tracks.Handler return Result.Created(TrackApplicationResponse.FromEntity(trackEntity)); } - - async Task PublishUpdateEvent(Guid userId, Track track, CancellationToken cancellationToken) - { - try - { - await eventBus.PublishAsync(new UpdateTrackPictureEvent( - track.Id, - storageConfiguration.Bucket, - trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName), - new ImageSize( - trackConfiguration.Routes.SmallPicturesPath, - trackConfiguration.PicturesSizes.SmallPictureWidth, - trackConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - trackConfiguration.Routes.MediumPicturesPath, - trackConfiguration.PicturesSizes.MediumPictureWidth, - trackConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - trackConfiguration.Routes.LargePicturesPath, - trackConfiguration.PicturesSizes.LargePictureWidth, - trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); - return Result.Success(); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish track picture update event for track {TrackId}", track.Id); - return Result.Error($"Failed to publish track picture update event for track {track.Id}"); - } - } - - async Task PublishTranscodeEvent(Guid userId, Track track, CancellationToken cancellationToken) - { - var destinationFolderAudio = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString()); - - try - { - await eventBus.PublishAsync(new UpdateTrackAudioEvent( - track.Id, - storageConfiguration.Bucket, - trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName), - storageConfiguration.Bucket, - destinationFolderAudio), cancellationToken); - return Result.Success(); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish track audio update event for track {TrackId}", track.Id); - return Result.Error($"Failed to publish track audio update event for track {track.Id}"); - } - } } } diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 73f4dcc..b53337f 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -45,7 +45,6 @@ namespace Musify.Infrastructure.MassTransit.Activities return executeContext.CompletedWithVariables(new Dictionary { [RoutingSlipVariableNames.Workflow.TemporalDirectory] = workingDirectory, - [RoutingSlipVariableNames.Audio.WorkingDirectory] = workingDirectory, [RoutingSlipVariableNames.Audio.SourceFilePath] = sourceFilePath, [RoutingSlipVariableNames.Audio.TranscodedDirectory] = workingDirectory } ); diff --git a/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs new file mode 100644 index 0000000..52ce5a7 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs @@ -0,0 +1,61 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; + +namespace Musify.Infrastructure.MassTransit.Activities.Files +{ + internal class CopyFileInBucketActivity( + IStorageService storageHandler, + ILogger logger) + : IActivity + { + public const string ExecuteEndpointName = "copy-file-in-bucket"; + + public async Task Execute(ExecuteContext executeContext) + { + try + { + await storageHandler.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) + { + logger.LogError(exception, "Failed to copy {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}", + executeContext.Arguments.SourceBucket, + executeContext.Arguments.SourceKey, + executeContext.Arguments.DestinationBucket, + executeContext.Arguments.DestinationKey); + throw; + } + } + + public async Task Compensate(CompensateContext compensateContext) + { + try + { + await storageHandler.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); + } + } + } +} diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs deleted file mode 100644 index 91addc2..0000000 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFolderFromBucketActivity.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Arguments; - -namespace Musify.Infrastructure.MassTransit.Activities.Files -{ - internal class RemoveFolderFromBucketActivity( - IStorageService storageHandler, - ILogger logger) - : IExecuteActivity - { - public const string ExecuteEndpointName = "remove-folder-from-bucket"; - - public async Task Execute(ExecuteContext executeContext) - { - var folderKey = executeContext.GetVariable(executeContext.Arguments.FolderKeyVariable); - ArgumentException.ThrowIfNullOrEmpty(folderKey, nameof(folderKey)); - - try - { - await storageHandler.RemoveFolderAsync( - executeContext.Arguments.Bucket, - folderKey, - executeContext.CancellationToken); - - logger.LogInformation("Removed folder {FolderKey} from bucket {Bucket}", - folderKey, - executeContext.Arguments.Bucket); - - return executeContext.Completed(); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to remove folder {FolderKey} from bucket {Bucket}", - folderKey, - executeContext.Arguments.Bucket); - throw; - } - } - } -} \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index 5d2570a..be2f675 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -16,8 +16,7 @@ namespace Musify.Infrastructure.MassTransit.Activities { try { - var destinationFolderName = Guid.NewGuid().ToString(); - var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, destinationFolderName); + var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, Guid.NewGuid().ToString()); var sourceFilePath = Path.Combine( workingDirectory, Guid.NewGuid().ToString() + Path.GetExtension(executeContext.Arguments.SourceKey)); @@ -38,8 +37,7 @@ namespace Musify.Infrastructure.MassTransit.Activities [RoutingSlipVariableNames.Picture.OriginalFilePath] = sourceFilePath, [RoutingSlipVariableNames.Picture.SmallResizedFilePath] = smallPictureFilePath, [RoutingSlipVariableNames.Picture.MediumResizedFilePath] = mediumPictureFilePath, - [RoutingSlipVariableNames.Picture.LargeResizedFilePath] = largePictureFilePath, - [RoutingSlipVariableNames.Picture.DestinationFolderName] = destinationFolderName + [RoutingSlipVariableNames.Picture.LargeResizedFilePath] = largePictureFilePath } ); } catch (Exception exception) diff --git a/Infrastructure/MassTransit/Activities/PlayLists/PublishPlayListPictureProcessingEventActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/PublishPlayListPictureProcessingEventActivity.cs new file mode 100644 index 0000000..7754510 --- /dev/null +++ b/Infrastructure/MassTransit/Activities/PlayLists/PublishPlayListPictureProcessingEventActivity.cs @@ -0,0 +1,34 @@ +using MassTransit; +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 logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "publish-playList-picture-processing-event"; + + public async Task Execute(ExecuteContext executeContext) + { + var arguments = executeContext.Arguments; + + await publishEndpoint.Publish( + new UpdatePlayListPictureEvent( + arguments.PlayListId, + arguments.Bucket, + arguments.FinalPictureKey, + arguments.Small, + arguments.Medium, + arguments.Large), + executeContext.CancellationToken); + + logger.LogInformation("Published picture processing event for playlist {PlayListId}", arguments.PlayListId); + + return executeContext.Completed(); + } + } +} diff --git a/Infrastructure/MassTransit/Activities/Tracks/PublishTrackProcessingEventsActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/PublishTrackProcessingEventsActivity.cs new file mode 100644 index 0000000..a82865c --- /dev/null +++ b/Infrastructure/MassTransit/Activities/Tracks/PublishTrackProcessingEventsActivity.cs @@ -0,0 +1,43 @@ +using MassTransit; +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 logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "publish-track-processing-events"; + + public async Task Execute(ExecuteContext executeContext) + { + var arguments = executeContext.Arguments; + + await publishEndpoint.Publish( + new UpdateTrackPictureEvent( + arguments.TrackId, + arguments.Bucket, + arguments.FinalPictureKey, + arguments.Small, + arguments.Medium, + arguments.Large), + executeContext.CancellationToken); + + await publishEndpoint.Publish( + new UpdateTrackAudioEvent( + arguments.TrackId, + arguments.Bucket, + arguments.FinalAudioKey, + arguments.Bucket, + arguments.AudioDestinationFolderKey), + executeContext.CancellationToken); + + logger.LogInformation("Published processing events for track {TrackId}", arguments.TrackId); + + return executeContext.Completed(); + } + } +} diff --git a/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs b/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs new file mode 100644 index 0000000..15cb3ef --- /dev/null +++ b/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs @@ -0,0 +1,62 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; + +namespace Musify.Infrastructure.MassTransit.Activities.UploadIntents +{ + internal class ConsumeUploadIntentsActivity( + IDatabase database, + ILogger logger) + : IActivity + { + public const string ExecuteEndpointName = "consume-upload-intents"; + + public async Task Execute(ExecuteContext executeContext) + { + var intentIds = executeContext.Arguments.IntentIds; + + 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 Compensate(CompensateContext compensateContext) + { + 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); + } + } + } +} diff --git a/Infrastructure/MassTransit/Arguments/ConsumeUploadIntentsArguments.cs b/Infrastructure/MassTransit/Arguments/ConsumeUploadIntentsArguments.cs new file mode 100644 index 0000000..70cd9c2 --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/ConsumeUploadIntentsArguments.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record ConsumeUploadIntentsArguments( + Guid[] IntentIds); +} diff --git a/Infrastructure/MassTransit/Arguments/CopyFileInBucketArguments.cs b/Infrastructure/MassTransit/Arguments/CopyFileInBucketArguments.cs new file mode 100644 index 0000000..ade186a --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/CopyFileInBucketArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record CopyFileInBucketArguments( + string SourceBucket, + string SourceKey, + string DestinationBucket, + string DestinationKey); +} diff --git a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs new file mode 100644 index 0000000..2b402ca --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs @@ -0,0 +1,12 @@ +using Musify.Application.Events; + +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record PublishPlayListPictureProcessingEventArguments( + Guid PlayListId, + string Bucket, + string FinalPictureKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); +} diff --git a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs new file mode 100644 index 0000000..aebba17 --- /dev/null +++ b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs @@ -0,0 +1,14 @@ +using Musify.Application.Events; + +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record PublishTrackProcessingEventsArguments( + Guid TrackId, + string Bucket, + string FinalPictureKey, + string FinalAudioKey, + string AudioDestinationFolderKey, + ImageSize Small, + ImageSize Medium, + ImageSize Large); +} diff --git a/Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs deleted file mode 100644 index 5628a4f..0000000 --- a/Infrastructure/MassTransit/Arguments/RemoveFolderFromBucketArguments.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Musify.Infrastructure.MassTransit.Arguments -{ - public record RemoveFolderFromBucketArguments( - string Bucket, - string FolderKeyVariable); -} diff --git a/Infrastructure/MassTransit/Consumers/CreatePlayListConsumer.cs b/Infrastructure/MassTransit/Consumers/CreatePlayListConsumer.cs new file mode 100644 index 0000000..218e9bf --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/CreatePlayListConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class CreatePlayListConsumer( + IBus bus, + PlayListPictureSourceRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "create-playList"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} diff --git a/Infrastructure/MassTransit/Consumers/CreateTrackConsumer.cs b/Infrastructure/MassTransit/Consumers/CreateTrackConsumer.cs new file mode 100644 index 0000000..06d4441 --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/CreateTrackConsumer.cs @@ -0,0 +1,23 @@ +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 + { + public const string QueueName = "create-track"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} diff --git a/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureSourceConsumer.cs b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureSourceConsumer.cs new file mode 100644 index 0000000..f24e9dd --- /dev/null +++ b/Infrastructure/MassTransit/Consumers/UpdatePlayListPictureSourceConsumer.cs @@ -0,0 +1,23 @@ +using MassTransit; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.RoutingSlip.Builders; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class UpdatePlayListPictureSourceConsumer( + IBus bus, + PlayListPictureSourceRoutingSlipBuilder routingSlipBuilder) + : IConsumer + { + public const string QueueName = "update-playList-picture-source"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlip = routingSlipBuilder + .Build(consumeContext.Message, consumeContext.CorrelationId) + .Build(); + + await bus.Execute(routingSlip); + } + } +} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index 860efac..438b5e6 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -8,6 +8,7 @@ using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Logs; using Musify.Infrastructure.MassTransit.Activities.PlayLists; using Musify.Infrastructure.MassTransit.Activities.Tracks; +using Musify.Infrastructure.MassTransit.Activities.UploadIntents; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; using Musify.Infrastructure.MassTransit.Logs; @@ -23,6 +24,8 @@ namespace Musify.Infrastructure.MassTransit services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options) @@ -33,6 +36,9 @@ namespace Musify.Infrastructure.MassTransit options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); @@ -41,6 +47,8 @@ namespace Musify.Infrastructure.MassTransit options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); + options.AddExecuteActivity(); + options.AddExecuteActivity(); options.AddActivity(); options.AddActivity(); @@ -50,6 +58,8 @@ namespace Musify.Infrastructure.MassTransit options.AddActivity(); options.AddActivity(); options.AddActivity(); + options.AddActivity(); + options.AddActivity(); } static void ConfigureRabbitMqWorkerEndpoints( @@ -85,6 +95,21 @@ namespace Musify.Infrastructure.MassTransit busRegistrationContext, DeletePlayListConsumer.QueueName); + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + CreateTrackConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + CreatePlayListConsumer.QueueName); + + ConfigureConsumerEndpoint( + busFactoryConfigurator, + busRegistrationContext, + UpdatePlayListPictureSourceConsumer.QueueName); + ConfigureConsumerEndpoint( busFactoryConfigurator, busRegistrationContext, @@ -125,6 +150,16 @@ namespace Musify.Infrastructure.MassTransit busRegistrationContext, DeletePlayListFromDbActivity.ExecuteEndpointName); + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + PublishTrackProcessingEventsActivity.ExecuteEndpointName); + + ConfigureExecuteActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName); + ConfigureActivityEndpoint( busFactoryConfigurator, busRegistrationContext, @@ -164,6 +199,16 @@ namespace Musify.Infrastructure.MassTransit busFactoryConfigurator, busRegistrationContext, UploadFileToBucketActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + CopyFileInBucketActivity.ExecuteEndpointName); + + ConfigureActivityEndpoint( + busFactoryConfigurator, + busRegistrationContext, + ConsumeUploadIntentsActivity.ExecuteEndpointName); } } } diff --git a/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs b/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs deleted file mode 100644 index fc2d8ac..0000000 --- a/Infrastructure/MassTransit/Logs/GenerateAudioWorkflowPathsLog.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs -{ - public record GenerateAudioWorkflowPathsLog(string WorkingDirectory); -} diff --git a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 71d037d..3767c6c 100644 --- a/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -2,26 +2,50 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip { internal static class ActivityNames { - internal const string DeleteTrack = "DeleteTrack"; internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving"; internal const string DeleteTrackFromDb = "DeleteTrackFromDb"; internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving"; internal const string DeletePlayListFromDb = "DeletePlayListFromDb"; + internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths"; internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths"; + internal const string DownloadFile = "DownloadFile"; + internal const string TranscodeAudio = "TranscodeAudio"; + 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 TransferFiles = "TransferFiles"; - internal const string RemoveFile = "RemoveFile"; - internal const string DeleteOriginal = "DeleteOriginal"; + internal const string UpdateTrackPicture = "UpdateTrackPicture"; internal const string UpdatePlayListPicture = "UpdatePlayListPicture"; internal const string UpdateTrackAudio = "UpdateTrackAudio"; + + internal const string CopyPictureToFinal = "CopyPictureToFinal"; + internal const string CopyAudioToFinal = "CopyAudioToFinal"; + + internal const string ConsumeUploadIntents = "ConsumeUploadIntents"; + + internal const string PublishTrackProcessingEvents = "PublishTrackProcessingEvents"; + internal const string PublishPlayListPictureProcessingEvent = "PublishPlayListPictureProcessingEvent"; + + 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"; } } diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/CreateTrackRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/CreateTrackRoutingSlipBuilder.cs new file mode 100644 index 0000000..fd9612f --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/CreateTrackRoutingSlipBuilder.cs @@ -0,0 +1,63 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.Tracks; +using Musify.Infrastructure.MassTransit.Activities.UploadIntents; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Consumers; + +namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders +{ + public class CreateTrackRoutingSlipBuilder + { + public RoutingSlipBuilder Build(CreateTrackResourcesEvent message, Guid? correlationId) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + + routingSlipBuilder.AddSubscription( + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + + routingSlipBuilder.AddActivity( + ActivityNames.CopyPictureToFinal, + EndpointHelper.BuildExecuteActivityUri(CopyFileInBucketActivity.ExecuteEndpointName), + new CopyFileInBucketArguments( + message.Bucket, + message.PictureSourceKey, + message.Bucket, + message.PictureDestinationKey)); + + routingSlipBuilder.AddActivity( + ActivityNames.CopyAudioToFinal, + EndpointHelper.BuildExecuteActivityUri(CopyFileInBucketActivity.ExecuteEndpointName), + new CopyFileInBucketArguments( + message.Bucket, + message.AudioSourceKey, + message.Bucket, + message.AudioDestinationKey)); + + routingSlipBuilder.AddActivity( + ActivityNames.ConsumeUploadIntents, + EndpointHelper.BuildExecuteActivityUri(ConsumeUploadIntentsActivity.ExecuteEndpointName), + new ConsumeUploadIntentsArguments( + [message.PictureIntentId, message.AudioIntentId])); + + routingSlipBuilder.AddActivity( + ActivityNames.PublishTrackProcessingEvents, + EndpointHelper.BuildExecuteActivityUri(PublishTrackProcessingEventsActivity.ExecuteEndpointName), + new PublishTrackProcessingEventsArguments( + message.TrackId, + message.Bucket, + message.PictureDestinationKey, + message.AudioDestinationKey, + message.AudioProcessedFolderKey, + message.Small, + message.Medium, + message.Large)); + + return routingSlipBuilder; + } + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs index f12750d..1d6efa8 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs @@ -45,7 +45,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders if (playList.OriginalPictureName != routes.PresetOriginalPicture) { routingSlipBuilder.AddActivity( - "RemovePlayListOriginalPicture", + ActivityNames.RemovePlayListOriginalPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, @@ -55,7 +55,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders if (playList.SmallPictureName != routes.PresetSmallPicture) { routingSlipBuilder.AddActivity( - "RemovePlayListSmallPicture", + ActivityNames.RemovePlayListSmallPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, @@ -65,7 +65,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders if (playList.MediumPictureName != routes.PresetMediumPicture) { routingSlipBuilder.AddActivity( - "RemovePlayListMediumPicture", + ActivityNames.RemovePlayListMediumPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, @@ -75,7 +75,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders if (playList.LargePictureName != routes.PresetLargePicture) { routingSlipBuilder.AddActivity( - "RemovePlayListLargePicture", + ActivityNames.RemovePlayListLargePicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index ca161aa..eb2e944 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -3,8 +3,8 @@ using MassTransit.Courier.Contracts; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; -using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Tracks; using Musify.Infrastructure.MassTransit.Consumers; @@ -42,7 +42,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders var bucket = storageConfiguration.Bucket; routingSlipBuilder.AddActivity( - "RemoveTrackOriginalPicture", + ActivityNames.RemoveTrackOriginalPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, @@ -51,21 +51,21 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders if (track.IsPicturesProcessed) { routingSlipBuilder.AddActivity( - "RemoveTrackSmallPicture", + ActivityNames.RemoveTrackSmallPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName))); routingSlipBuilder.AddActivity( - "RemoveTrackMediumPicture", + ActivityNames.RemoveTrackMediumPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName))); routingSlipBuilder.AddActivity( - "RemoveTrackLargePicture", + ActivityNames.RemoveTrackLargePicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, @@ -73,7 +73,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders } routingSlipBuilder.AddActivity( - "RemoveTrackOriginalAudio", + ActivityNames.RemoveTrackOriginalAudio, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, @@ -82,7 +82,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders if (track.IsAudioProcessed) { routingSlipBuilder.AddActivity( - "RemoveTrackProcessedAudio", + ActivityNames.RemoveTrackProcessedAudio, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs new file mode 100644 index 0000000..80cf5c2 --- /dev/null +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs @@ -0,0 +1,84 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.PlayLists; +using Musify.Infrastructure.MassTransit.Activities.UploadIntents; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Consumers; + +namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders +{ + public class PlayListPictureSourceRoutingSlipBuilder + { + public RoutingSlipBuilder Build(CreatePlayListResourcesEvent message, Guid? correlationId) + => BuildSlip( + message.PlayListId, + message.PictureIntentId, + message.Bucket, + message.PictureSourceKey, + message.PictureDestinationKey, + message.Small, + message.Medium, + message.Large, + correlationId); + + public RoutingSlipBuilder Build(UpdatePlayListPictureSourceEvent message, Guid? correlationId) + => BuildSlip( + message.PlayListId, + message.PictureIntentId, + message.Bucket, + message.PictureSourceKey, + message.PictureDestinationKey, + message.Small, + message.Medium, + message.Large, + correlationId); + + static RoutingSlipBuilder BuildSlip( + Guid playListId, + Guid pictureIntentId, + string bucket, + string pictureSourceKey, + string pictureDestinationKey, + ImageSize small, + ImageSize medium, + ImageSize large, + Guid? correlationId) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty); + + routingSlipBuilder.AddSubscription( + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + + routingSlipBuilder.AddActivity( + ActivityNames.CopyPictureToFinal, + EndpointHelper.BuildExecuteActivityUri(CopyFileInBucketActivity.ExecuteEndpointName), + new CopyFileInBucketArguments( + bucket, + pictureSourceKey, + bucket, + pictureDestinationKey)); + + routingSlipBuilder.AddActivity( + ActivityNames.ConsumeUploadIntents, + EndpointHelper.BuildExecuteActivityUri(ConsumeUploadIntentsActivity.ExecuteEndpointName), + new ConsumeUploadIntentsArguments([pictureIntentId])); + + routingSlipBuilder.AddActivity( + ActivityNames.PublishPlayListPictureProcessingEvent, + EndpointHelper.BuildExecuteActivityUri(PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName), + new PublishPlayListPictureProcessingEventArguments( + playListId, + bucket, + pictureDestinationKey, + small, + medium, + large)); + + return routingSlipBuilder; + } + } +} diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs index 5392816..2667a18 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs @@ -11,9 +11,7 @@ namespace Musify.Infrastructure.MassTransit internal static class Audio { internal const string SourceFilePath = "Audio.SourceFilePath"; - internal const string WorkingDirectory = "Audio.WorkingDirectory"; internal const string TranscodedDirectory = "Audio.TranscodedDirectory"; - internal const string PreviousAudioFolderName = "Audio.PreviousAudioFolderName"; } internal static class Picture @@ -22,7 +20,6 @@ namespace Musify.Infrastructure.MassTransit internal const string SmallResizedFilePath = "Picture.SmallResizedFilePath"; internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath"; internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath"; - internal const string DestinationFolderName = "Picture.DestinationFolderName"; } } } From 663495c941bd43932df4eddf695969087a9935b3 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 20:41:03 +0200 Subject: [PATCH 144/172] Update structure --- Application/{Events => Common}/ImageSize.cs | 0 .../Tracks/{Handler => Handlers}/CreateTrackCommandHandler.cs | 0 .../Tracks/{Handler => Handlers}/DeleteTrackCommandHandler.cs | 0 .../Tracks/{Handler => Handlers}/GetTrackByIdQueryHandler.cs | 0 .../Tracks/{Handler => Handlers}/GetTracksByUserIdQueryHandler.cs | 0 Application/Tracks/{Handler => Handlers}/GetTracksQueryHandler.cs | 0 .../{Handler => Handlers}/RequestTrackUploadUrlsCommandHandler.cs | 0 Domain/{Entities => ValueObjects}/LifeCycleStatus.cs | 0 Domain/{Entities => ValueObjects}/ProcessingStatus.cs | 0 Domain/{Entities => ValueObjects}/UploadIntentPurpose.cs | 0 Domain/{Entities => ValueObjects}/UploadIntentStatus.cs | 0 Domain/{Entities => ValueObjects}/UploadState.cs | 0 .../PlayLists => Arguments}/DeletePlayListFromDbArguments.cs | 0 .../Tracks => Arguments}/DeleteTrackFromDbArguments.cs | 0 .../PlayLists => Arguments}/MarkPlayListAsRemovingArguments.cs | 0 .../Tracks => Arguments}/MarkTrackAsRemovingArguments.cs | 0 Infrastructure/MassTransit/{EventBus => }/MassTransitEventBus.cs | 0 .../RoutingSlip/{Variables => }/RoutingSlipVariableNames.cs | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename Application/{Events => Common}/ImageSize.cs (100%) rename Application/Tracks/{Handler => Handlers}/CreateTrackCommandHandler.cs (100%) rename Application/Tracks/{Handler => Handlers}/DeleteTrackCommandHandler.cs (100%) rename Application/Tracks/{Handler => Handlers}/GetTrackByIdQueryHandler.cs (100%) rename Application/Tracks/{Handler => Handlers}/GetTracksByUserIdQueryHandler.cs (100%) rename Application/Tracks/{Handler => Handlers}/GetTracksQueryHandler.cs (100%) rename Application/Tracks/{Handler => Handlers}/RequestTrackUploadUrlsCommandHandler.cs (100%) rename Domain/{Entities => ValueObjects}/LifeCycleStatus.cs (100%) rename Domain/{Entities => ValueObjects}/ProcessingStatus.cs (100%) rename Domain/{Entities => ValueObjects}/UploadIntentPurpose.cs (100%) rename Domain/{Entities => ValueObjects}/UploadIntentStatus.cs (100%) rename Domain/{Entities => ValueObjects}/UploadState.cs (100%) rename Infrastructure/MassTransit/{Activities/PlayLists => Arguments}/DeletePlayListFromDbArguments.cs (100%) rename Infrastructure/MassTransit/{Activities/Tracks => Arguments}/DeleteTrackFromDbArguments.cs (100%) rename Infrastructure/MassTransit/{Activities/PlayLists => Arguments}/MarkPlayListAsRemovingArguments.cs (100%) rename Infrastructure/MassTransit/{Activities/Tracks => Arguments}/MarkTrackAsRemovingArguments.cs (100%) rename Infrastructure/MassTransit/{EventBus => }/MassTransitEventBus.cs (100%) rename Infrastructure/MassTransit/RoutingSlip/{Variables => }/RoutingSlipVariableNames.cs (100%) diff --git a/Application/Events/ImageSize.cs b/Application/Common/ImageSize.cs similarity index 100% rename from Application/Events/ImageSize.cs rename to Application/Common/ImageSize.cs diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs similarity index 100% rename from Application/Tracks/Handler/CreateTrackCommandHandler.cs rename to Application/Tracks/Handlers/CreateTrackCommandHandler.cs diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs similarity index 100% rename from Application/Tracks/Handler/DeleteTrackCommandHandler.cs rename to Application/Tracks/Handlers/DeleteTrackCommandHandler.cs diff --git a/Application/Tracks/Handler/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs similarity index 100% rename from Application/Tracks/Handler/GetTrackByIdQueryHandler.cs rename to Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs diff --git a/Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs similarity index 100% rename from Application/Tracks/Handler/GetTracksByUserIdQueryHandler.cs rename to Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs diff --git a/Application/Tracks/Handler/GetTracksQueryHandler.cs b/Application/Tracks/Handlers/GetTracksQueryHandler.cs similarity index 100% rename from Application/Tracks/Handler/GetTracksQueryHandler.cs rename to Application/Tracks/Handlers/GetTracksQueryHandler.cs diff --git a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs similarity index 100% rename from Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs rename to Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs diff --git a/Domain/Entities/LifeCycleStatus.cs b/Domain/ValueObjects/LifeCycleStatus.cs similarity index 100% rename from Domain/Entities/LifeCycleStatus.cs rename to Domain/ValueObjects/LifeCycleStatus.cs diff --git a/Domain/Entities/ProcessingStatus.cs b/Domain/ValueObjects/ProcessingStatus.cs similarity index 100% rename from Domain/Entities/ProcessingStatus.cs rename to Domain/ValueObjects/ProcessingStatus.cs diff --git a/Domain/Entities/UploadIntentPurpose.cs b/Domain/ValueObjects/UploadIntentPurpose.cs similarity index 100% rename from Domain/Entities/UploadIntentPurpose.cs rename to Domain/ValueObjects/UploadIntentPurpose.cs diff --git a/Domain/Entities/UploadIntentStatus.cs b/Domain/ValueObjects/UploadIntentStatus.cs similarity index 100% rename from Domain/Entities/UploadIntentStatus.cs rename to Domain/ValueObjects/UploadIntentStatus.cs diff --git a/Domain/Entities/UploadState.cs b/Domain/ValueObjects/UploadState.cs similarity index 100% rename from Domain/Entities/UploadState.cs rename to Domain/ValueObjects/UploadState.cs diff --git a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbArguments.cs b/Infrastructure/MassTransit/Arguments/DeletePlayListFromDbArguments.cs similarity index 100% rename from Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbArguments.cs rename to Infrastructure/MassTransit/Arguments/DeletePlayListFromDbArguments.cs diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbArguments.cs b/Infrastructure/MassTransit/Arguments/DeleteTrackFromDbArguments.cs similarity index 100% rename from Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbArguments.cs rename to Infrastructure/MassTransit/Arguments/DeleteTrackFromDbArguments.cs diff --git a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingArguments.cs b/Infrastructure/MassTransit/Arguments/MarkPlayListAsRemovingArguments.cs similarity index 100% rename from Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingArguments.cs rename to Infrastructure/MassTransit/Arguments/MarkPlayListAsRemovingArguments.cs diff --git a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingArguments.cs b/Infrastructure/MassTransit/Arguments/MarkTrackAsRemovingArguments.cs similarity index 100% rename from Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingArguments.cs rename to Infrastructure/MassTransit/Arguments/MarkTrackAsRemovingArguments.cs diff --git a/Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs b/Infrastructure/MassTransit/MassTransitEventBus.cs similarity index 100% rename from Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs rename to Infrastructure/MassTransit/MassTransitEventBus.cs diff --git a/Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs b/Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs similarity index 100% rename from Infrastructure/MassTransit/RoutingSlip/Variables/RoutingSlipVariableNames.cs rename to Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs From 689144eae4bce33b5bb0eaab757e2306fb376426 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 20:42:35 +0200 Subject: [PATCH 145/172] Update structure --- Infrastructure/MassTransit/Logs/ConsumeUploadIntentsLog.cs | 5 +++++ Infrastructure/MassTransit/Logs/CopyFileInBucketLog.cs | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 Infrastructure/MassTransit/Logs/ConsumeUploadIntentsLog.cs create mode 100644 Infrastructure/MassTransit/Logs/CopyFileInBucketLog.cs diff --git a/Infrastructure/MassTransit/Logs/ConsumeUploadIntentsLog.cs b/Infrastructure/MassTransit/Logs/ConsumeUploadIntentsLog.cs new file mode 100644 index 0000000..275ddd5 --- /dev/null +++ b/Infrastructure/MassTransit/Logs/ConsumeUploadIntentsLog.cs @@ -0,0 +1,5 @@ +namespace Musify.Infrastructure.MassTransit.Logs +{ + public record ConsumeUploadIntentsLog( + Guid[] IntentIds); +} diff --git a/Infrastructure/MassTransit/Logs/CopyFileInBucketLog.cs b/Infrastructure/MassTransit/Logs/CopyFileInBucketLog.cs new file mode 100644 index 0000000..609258a --- /dev/null +++ b/Infrastructure/MassTransit/Logs/CopyFileInBucketLog.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.MassTransit.Logs +{ + public record CopyFileInBucketLog( + string DestinationBucket, + string DestinationKey); +} From d60daf6272cb7653c97368846b96f4a7fbab52aa Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 20:42:38 +0200 Subject: [PATCH 146/172] Update structure --- Application/Common/ImageSize.cs | 2 +- Application/Contracts/Application/IFileData.cs | 13 ------------- .../IAudioTranscoderService.cs | 2 +- .../Contracts/{Infrastructure => }/IDatabase.cs | 2 +- .../{Infrastructure => }/IDatabaseTransaction.cs | 2 +- .../Contracts/{Infrastructure => }/IEventBus.cs | 2 +- .../{Infrastructure => }/IPictureService.cs | 2 +- .../{Infrastructure => }/IStorageService.cs | 4 ++-- .../Contracts/Infrastructure/ObjectMetadata.cs | 4 ---- Application/Contracts/ObjectMetaData.cs | 4 ++++ .../Events/CreatePlayListResourcesEvent.cs | 2 ++ Application/Events/CreateTrackResourcesEvent.cs | 2 ++ Application/Events/UpdatePlayListPictureEvent.cs | 2 ++ .../Events/UpdatePlayListPictureSourceEvent.cs | 2 ++ Application/Events/UpdateTrackPictureEvent.cs | 2 ++ .../Handlers/CreatePlayListCommandHandler.cs | 4 +++- .../Handlers/DeletePlayListCommandHandler.cs | 3 ++- .../Handlers/GetPlayListByIdQueryHandler.cs | 2 +- .../Handlers/GetPlayListsByUserIdQueryHandler.cs | 2 +- .../Handlers/GetPlayListsQueryHandler.cs | 2 +- ...RequestPlayListPictureUploadCommandHandler.cs | 3 ++- .../Handlers/UpdatePlayListCommandHandler.cs | 4 +++- Application/Services/UploadIntentValidator.cs | 3 ++- .../Tracks/Handlers/CreateTrackCommandHandler.cs | 6 ++++-- .../Tracks/Handlers/DeleteTrackCommandHandler.cs | 5 +++-- .../Tracks/Handlers/GetTrackByIdQueryHandler.cs | 4 ++-- .../Handlers/GetTracksByUserIdQueryHandler.cs | 4 ++-- .../Tracks/Handlers/GetTracksQueryHandler.cs | 4 ++-- .../RequestTrackUploadUrlsCommandHandler.cs | 5 +++-- .../Users/Handlers/CreateUserCommandHandler.cs | 2 +- .../Users/Handlers/GetUserByIdQueryHandler.cs | 2 +- .../Handlers/GetUserByKeycloakQueryHandler.cs | 2 +- .../Users/Handlers/GetUsersQueryHandler.cs | 2 +- Domain/Entities/PlayList.cs | 2 ++ Domain/Entities/Track.cs | 2 ++ Domain/Entities/Upload.cs | 2 ++ Domain/Entities/UploadIntent.cs | 2 ++ Domain/ValueObjects/LifeCycleStatus.cs | 2 +- Domain/ValueObjects/ProcessingStatus.cs | 2 +- Domain/ValueObjects/UploadIntentPurpose.cs | 2 +- Domain/ValueObjects/UploadIntentStatus.cs | 2 +- Domain/ValueObjects/UploadState.cs | 2 +- Infrastructure/Jobs/TemporalUploadsCleanupJob.cs | 16 ++++++++-------- Infrastructure/Jobs/UploadIntentExpirationJob.cs | 4 ++-- .../Audio/GenerateAudioWorkflowPathsActivity.cs | 5 +++-- .../Audio/TranscodeDashAudioActivity.cs | 7 ++++--- .../Activities/Audio/UpdateTrackAudioActivity.cs | 3 ++- .../Activities/Files/CopyFileInBucketActivity.cs | 2 +- .../Files/DownloadFileFromBucketActivity.cs | 6 +++--- .../Files/RemoveFileFromBucketActivity.cs | 4 ++-- .../Files/TransferFilesToBucketActivity.cs | 6 +++--- .../Files/UploadFileToBucketActivity.cs | 2 +- .../GeneratePictureWorkflowPathsActivity.cs | 2 +- .../Activities/Pictures/ResizePictureActivity.cs | 6 +++--- .../Pictures/UpdatePlayListPictureActivity.cs | 6 +++--- .../Pictures/UpdateTrackPictureActivity.cs | 6 +++--- .../PlayLists/DeletePlayListFromDbActivity.cs | 3 ++- .../PlayLists/MarkPlayListAsRemovingActivity.cs | 4 +++- .../Tracks/DeleteTrackFromDbActivity.cs | 3 ++- .../Tracks/MarkTrackAsRemovingActivity.cs | 4 +++- .../ConsumeUploadIntentsActivity.cs | 3 ++- .../Arguments/DeletePlayListFromDbArguments.cs | 2 +- .../Arguments/DeleteTrackFromDbArguments.cs | 2 +- .../Arguments/DownloadFileFromBucketArguments.cs | 2 +- .../GenerateAudioWorkflowPathsArguments.cs | 2 +- .../GeneratePictureWorkflowPathsArguments.cs | 2 +- .../Arguments/MarkPlayListAsRemovingArguments.cs | 2 +- .../Arguments/MarkTrackAsRemovingArguments.cs | 2 +- ...ishPlayListPictureProcessingEventArguments.cs | 1 + .../PublishTrackProcessingEventsArguments.cs | 1 + .../Arguments/RemoveFileFromBucketArguments.cs | 2 +- .../Arguments/ResizePictureLocalArguments.cs | 2 +- .../Arguments/TranscodeDashAudioArguments.cs | 2 +- .../Arguments/TransferFilesToBucketArguments.cs | 2 +- .../Arguments/UpdatePlayListPictureArguments.cs | 2 +- .../Arguments/UpdateTrackPictureArguments.cs | 2 +- ...assTransitDependencyInjection.Registration.cs | 2 -- .../MassTransitDependencyInjection.cs | 2 +- .../Logs/DownloadFileFromBucketLog.cs | 2 +- .../Logs/GeneratePictureWorkflowPathsLog.cs | 2 +- .../MassTransit/Logs/ResizePictureLog.cs | 2 +- .../MassTransit/Logs/TranscodeDashAudioLog.cs | 2 +- .../MassTransit/Logs/TransferFilesToBucketLog.cs | 2 +- .../MassTransit/Logs/UpdatePlayListPictureLog.cs | 2 +- .../MassTransit/Logs/UpdateTrackPictureLog.cs | 2 +- .../MassTransit/MassTransitEventBus.cs | 2 +- .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 3 +-- .../Builders/DeletePlayListRoutingSlipBuilder.cs | 4 ++-- .../Builders/DeleteTrackRoutingSlipBuilder.cs | 4 ++-- .../PictureWorkflowRoutingSlipBuilder.cs | 2 +- .../PlayListPictureSourceRoutingSlipBuilder.cs | 1 + Infrastructure/Persistence/Database.cs | 4 ++-- .../Persistence/DatabaseDependencyInjection.cs | 2 +- .../Services/AudioTranscoderService.cs | 2 +- Infrastructure/Services/PictureService.cs | 2 +- .../Services/ServicesDependencyInjection.cs | 4 ++-- Infrastructure/Services/StorageService.cs | 9 +++++---- 97 files changed, 164 insertions(+), 137 deletions(-) delete mode 100644 Application/Contracts/Application/IFileData.cs rename Application/Contracts/{Infrastructure => }/IAudioTranscoderService.cs (83%) rename Application/Contracts/{Infrastructure => }/IDatabase.cs (92%) rename Application/Contracts/{Infrastructure => }/IDatabaseTransaction.cs (73%) rename Application/Contracts/{Infrastructure => }/IEventBus.cs (72%) rename Application/Contracts/{Infrastructure => }/IPictureService.cs (77%) rename Application/Contracts/{Infrastructure => }/IStorageService.cs (92%) delete mode 100644 Application/Contracts/Infrastructure/ObjectMetadata.cs create mode 100644 Application/Contracts/ObjectMetaData.cs diff --git a/Application/Common/ImageSize.cs b/Application/Common/ImageSize.cs index 1a1d107..17b104f 100644 --- a/Application/Common/ImageSize.cs +++ b/Application/Common/ImageSize.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Events +namespace Musify.Application.Common { public record ImageSize( string SavePath, diff --git a/Application/Contracts/Application/IFileData.cs b/Application/Contracts/Application/IFileData.cs deleted file mode 100644 index af7438c..0000000 --- a/Application/Contracts/Application/IFileData.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Musify.Application.Contracts.Application -{ - public interface IFileData - { - Stream FileStream { get; } - - string FileName { get; } - - string FileType { get; } - - string ContentType { get; } - } -} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IAudioTranscoderService.cs b/Application/Contracts/IAudioTranscoderService.cs similarity index 83% rename from Application/Contracts/Infrastructure/IAudioTranscoderService.cs rename to Application/Contracts/IAudioTranscoderService.cs index 41ec61d..d2c06dd 100644 --- a/Application/Contracts/Infrastructure/IAudioTranscoderService.cs +++ b/Application/Contracts/IAudioTranscoderService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Contracts { public interface IAudioTranscoderService { diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/IDatabase.cs similarity index 92% rename from Application/Contracts/Infrastructure/IDatabase.cs rename to Application/Contracts/IDatabase.cs index bf75e6a..8fbaf26 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/IDatabase.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Musify.Domain.Entities; -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Contracts { public interface IDatabase { diff --git a/Application/Contracts/Infrastructure/IDatabaseTransaction.cs b/Application/Contracts/IDatabaseTransaction.cs similarity index 73% rename from Application/Contracts/Infrastructure/IDatabaseTransaction.cs rename to Application/Contracts/IDatabaseTransaction.cs index 1aedc2e..42e6185 100644 --- a/Application/Contracts/Infrastructure/IDatabaseTransaction.cs +++ b/Application/Contracts/IDatabaseTransaction.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Contracts { public interface IDatabaseTransaction : IAsyncDisposable { diff --git a/Application/Contracts/Infrastructure/IEventBus.cs b/Application/Contracts/IEventBus.cs similarity index 72% rename from Application/Contracts/Infrastructure/IEventBus.cs rename to Application/Contracts/IEventBus.cs index 2d41fea..1f5acb9 100644 --- a/Application/Contracts/Infrastructure/IEventBus.cs +++ b/Application/Contracts/IEventBus.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Contracts { public interface IEventBus { diff --git a/Application/Contracts/Infrastructure/IPictureService.cs b/Application/Contracts/IPictureService.cs similarity index 77% rename from Application/Contracts/Infrastructure/IPictureService.cs rename to Application/Contracts/IPictureService.cs index f881fc7..8c8ccd2 100644 --- a/Application/Contracts/Infrastructure/IPictureService.cs +++ b/Application/Contracts/IPictureService.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Contracts { public interface IPictureService { diff --git a/Application/Contracts/Infrastructure/IStorageService.cs b/Application/Contracts/IStorageService.cs similarity index 92% rename from Application/Contracts/Infrastructure/IStorageService.cs rename to Application/Contracts/IStorageService.cs index f58df25..388d680 100644 --- a/Application/Contracts/Infrastructure/IStorageService.cs +++ b/Application/Contracts/IStorageService.cs @@ -1,11 +1,11 @@ -namespace Musify.Application.Contracts.Infrastructure +namespace Musify.Application.Contracts { public interface IStorageService { Task GetFileAsync(string bucket, string key, CancellationToken cancellationToken); // Returns null if the object does not exist - Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken); + Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken); Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); diff --git a/Application/Contracts/Infrastructure/ObjectMetadata.cs b/Application/Contracts/Infrastructure/ObjectMetadata.cs deleted file mode 100644 index 13061c9..0000000 --- a/Application/Contracts/Infrastructure/ObjectMetadata.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Musify.Application.Contracts.Infrastructure -{ - public record ObjectMetadata(string ContentType, long ContentLength); -} diff --git a/Application/Contracts/ObjectMetaData.cs b/Application/Contracts/ObjectMetaData.cs new file mode 100644 index 0000000..6406463 --- /dev/null +++ b/Application/Contracts/ObjectMetaData.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Contracts +{ + public record ObjectMetaData(string ContentType, long ContentLength); +} diff --git a/Application/Events/CreatePlayListResourcesEvent.cs b/Application/Events/CreatePlayListResourcesEvent.cs index 89523fb..3954a90 100644 --- a/Application/Events/CreatePlayListResourcesEvent.cs +++ b/Application/Events/CreatePlayListResourcesEvent.cs @@ -1,3 +1,5 @@ +using Musify.Application.Common; + namespace Musify.Application.Events { public record CreatePlayListResourcesEvent( diff --git a/Application/Events/CreateTrackResourcesEvent.cs b/Application/Events/CreateTrackResourcesEvent.cs index 987ce4c..2482d25 100644 --- a/Application/Events/CreateTrackResourcesEvent.cs +++ b/Application/Events/CreateTrackResourcesEvent.cs @@ -1,3 +1,5 @@ +using Musify.Application.Common; + namespace Musify.Application.Events { public record CreateTrackResourcesEvent( diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs index 3063a5e..2a3e760 100644 --- a/Application/Events/UpdatePlayListPictureEvent.cs +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -1,3 +1,5 @@ +using Musify.Application.Common; + namespace Musify.Application.Events { public record UpdatePlayListPictureEvent( diff --git a/Application/Events/UpdatePlayListPictureSourceEvent.cs b/Application/Events/UpdatePlayListPictureSourceEvent.cs index 2638c8d..fdc7cbd 100644 --- a/Application/Events/UpdatePlayListPictureSourceEvent.cs +++ b/Application/Events/UpdatePlayListPictureSourceEvent.cs @@ -1,3 +1,5 @@ +using Musify.Application.Common; + namespace Musify.Application.Events { public record UpdatePlayListPictureSourceEvent( diff --git a/Application/Events/UpdateTrackPictureEvent.cs b/Application/Events/UpdateTrackPictureEvent.cs index 2d8c80b..3fd0547 100644 --- a/Application/Events/UpdateTrackPictureEvent.cs +++ b/Application/Events/UpdateTrackPictureEvent.cs @@ -1,3 +1,5 @@ +using Musify.Application.Common; + namespace Musify.Application.Events { public record UpdateTrackPictureEvent( diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 85dd51b..1b7d894 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -3,13 +3,15 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Common; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index dbb6acb..a27da58 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -2,10 +2,11 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index 3c65440..b6881b6 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -1,9 +1,9 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using Mediator; +using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index b493509..2a0506b 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -1,11 +1,11 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; using Musify.Application.Pagination; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; using Mediator; +using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index 89b3b24..c1a1f6d 100644 --- a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -1,11 +1,11 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; using Musify.Application.Pagination; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Queries; using X.PagedList.EF; using Mediator; +using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index d1ccb96..4c0d25d 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -2,12 +2,13 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.Services; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index aeea8f1..45f01b8 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -3,13 +3,15 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Common; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers { diff --git a/Application/Services/UploadIntentValidator.cs b/Application/Services/UploadIntentValidator.cs index aeb43aa..380fbb7 100644 --- a/Application/Services/UploadIntentValidator.cs +++ b/Application/Services/UploadIntentValidator.cs @@ -1,8 +1,9 @@ using Ardalis.Result; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Application.Services; diff --git a/Application/Tracks/Handlers/CreateTrackCommandHandler.cs b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs index 7f4fbe9..b33ce1a 100644 --- a/Application/Tracks/Handlers/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs @@ -3,15 +3,17 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Common; using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; -namespace Musify.Application.Tracks.Handler +namespace Musify.Application.Tracks.Handlers { public class CreateTrackCommandHandler( IDatabase database, diff --git a/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs b/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs index 66a85c5..eb1ff77 100644 --- a/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs @@ -2,12 +2,13 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Commands; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; using Musify.Application.Events; +using Musify.Application.Contracts; -namespace Musify.Application.Tracks.Handler +namespace Musify.Application.Tracks.Handlers { public class DeleteTrackCommandHandler( IDatabase database, diff --git a/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs index df277c0..52e48c0 100644 --- a/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs @@ -1,11 +1,11 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; -namespace Musify.Application.Tracks.Handler +namespace Musify.Application.Tracks.Handlers { public class GetTrackByIdQueryHandler(IDatabase database) : IQueryHandler > diff --git a/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs index 6412908..bf76206 100644 --- a/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs @@ -2,12 +2,12 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Pagination; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; using X.PagedList.EF; +using Musify.Application.Contracts; -namespace Musify.Application.Tracks.Handler +namespace Musify.Application.Tracks.Handlers { public class GetTracksByUserIdQueryHandler(IDatabase database) : IQueryHandler> > diff --git a/Application/Tracks/Handlers/GetTracksQueryHandler.cs b/Application/Tracks/Handlers/GetTracksQueryHandler.cs index 2219fda..694b328 100644 --- a/Application/Tracks/Handlers/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTracksQueryHandler.cs @@ -2,12 +2,12 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Pagination; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Tracks.Queries; using Musify.Application.Tracks.Responses; using X.PagedList.EF; +using Musify.Application.Contracts; -namespace Musify.Application.Tracks.Handler +namespace Musify.Application.Tracks.Handlers { public class GetTracksQueryHandler(IDatabase database) : IQueryHandler> > diff --git a/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs index 22cf8a4..f20c9b3 100644 --- a/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs +++ b/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs @@ -2,14 +2,15 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Application.Services; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; -namespace Musify.Application.Tracks.Handler +namespace Musify.Application.Tracks.Handlers { public class RequestTrackUploadUrlsCommandHandler( IDatabase database, diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index 84a9c0e..fabcd17 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -2,7 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Application.Users.Commands; using Musify.Application.Users.Responses; diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index 1d14715..9abc9fc 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs index 10fb204..db8173b 100644 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs @@ -1,7 +1,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index 4688869..e6d9723 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -2,10 +2,10 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Musify.Application.Pagination; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Queries; using Musify.Application.Users.Responses; using X.PagedList.EF; +using Musify.Application.Contracts; namespace Musify.Application.Users.Handlers { diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index e01985f..6e6c7a8 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Musify.Domain.ValueObjects; + namespace Musify.Domain.Entities { #pragma warning disable CS8618 diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index 182b56b..c569b5a 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -2,6 +2,8 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; +using Musify.Domain.ValueObjects; + namespace Musify.Domain.Entities { #pragma warning disable CS8618 diff --git a/Domain/Entities/Upload.cs b/Domain/Entities/Upload.cs index 9e8c42b..1cb9cb7 100644 --- a/Domain/Entities/Upload.cs +++ b/Domain/Entities/Upload.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Musify.Domain.ValueObjects; + namespace Musify.Domain.Entities { [Table("Upload")] diff --git a/Domain/Entities/UploadIntent.cs b/Domain/Entities/UploadIntent.cs index 3502950..11a44f3 100644 --- a/Domain/Entities/UploadIntent.cs +++ b/Domain/Entities/UploadIntent.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Musify.Domain.ValueObjects; + namespace Musify.Domain.Entities { [Table("UploadIntents")] diff --git a/Domain/ValueObjects/LifeCycleStatus.cs b/Domain/ValueObjects/LifeCycleStatus.cs index bd4c7dc..bed197e 100644 --- a/Domain/ValueObjects/LifeCycleStatus.cs +++ b/Domain/ValueObjects/LifeCycleStatus.cs @@ -1,4 +1,4 @@ -namespace Musify.Domain.Entities +namespace Musify.Domain.ValueObjects { public enum LifeCycleStatus { diff --git a/Domain/ValueObjects/ProcessingStatus.cs b/Domain/ValueObjects/ProcessingStatus.cs index 583ac67..66aa32e 100644 --- a/Domain/ValueObjects/ProcessingStatus.cs +++ b/Domain/ValueObjects/ProcessingStatus.cs @@ -1,4 +1,4 @@ -namespace Musify.Domain.Entities +namespace Musify.Domain.ValueObjects { public enum ProcessingStatus { diff --git a/Domain/ValueObjects/UploadIntentPurpose.cs b/Domain/ValueObjects/UploadIntentPurpose.cs index f0a7f08..dc0339b 100644 --- a/Domain/ValueObjects/UploadIntentPurpose.cs +++ b/Domain/ValueObjects/UploadIntentPurpose.cs @@ -1,4 +1,4 @@ -namespace Musify.Domain.Entities +namespace Musify.Domain.ValueObjects { public enum UploadIntentPurpose { diff --git a/Domain/ValueObjects/UploadIntentStatus.cs b/Domain/ValueObjects/UploadIntentStatus.cs index be3956b..7a6e23f 100644 --- a/Domain/ValueObjects/UploadIntentStatus.cs +++ b/Domain/ValueObjects/UploadIntentStatus.cs @@ -1,4 +1,4 @@ -namespace Musify.Domain.Entities +namespace Musify.Domain.ValueObjects { public enum UploadIntentStatus { diff --git a/Domain/ValueObjects/UploadState.cs b/Domain/ValueObjects/UploadState.cs index 50a4dfc..5bcaf5b 100644 --- a/Domain/ValueObjects/UploadState.cs +++ b/Domain/ValueObjects/UploadState.cs @@ -1,4 +1,4 @@ -namespace Musify.Domain.Entities +namespace Musify.Domain.ValueObjects { public enum UploadState { diff --git a/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs b/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs index 24b71f4..d11924a 100644 --- a/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs +++ b/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs @@ -1,21 +1,21 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; +using Musify.Application.Contracts; namespace Musify.Infrastructure.Jobs { - public class TemporalUploadsCleanupJob( + public class TemporalUploadsCleanUpJob( IServiceScopeFactory scopeFactory, - ILogger logger, + ILogger logger, ApplicationStorageConfiguration storageConfiguration, UploadIntentConfiguration uploadIntentConfiguration) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - logger.LogInformation("TemporalUploadsCleanupJob started (interval: {Interval}s, prefix: {Prefix})", + logger.LogInformation("TemporalUploadsCleanUpJob started (interval: {Interval}s, prefix: {Prefix})", uploadIntentConfiguration.TempCleanupJobIntervalSeconds, uploadIntentConfiguration.TempRootPrefix); @@ -27,7 +27,7 @@ namespace Musify.Infrastructure.Jobs } catch (Exception exception) when (exception is not OperationCanceledException) { - logger.LogError(exception, "Error in TemporalUploadsCleanupJob"); + logger.LogError(exception, "Error in TemporalUploadsCleanUpJob"); } await Task.Delay(TimeSpan.FromSeconds(uploadIntentConfiguration.TempCleanupJobIntervalSeconds), stoppingToken); @@ -39,7 +39,7 @@ namespace Musify.Infrastructure.Jobs await using var scope = scopeFactory.CreateAsyncScope(); var storageService = scope.ServiceProvider.GetRequiredService(); - var cutoff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TempUploadsRetentionDays); + var cutOff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TempUploadsRetentionDays); var deleted = 0; var errors = 0; @@ -48,7 +48,7 @@ namespace Musify.Infrastructure.Jobs uploadIntentConfiguration.TempRootPrefix, cancellationToken)) { - if (lastModified >= cutoff) + if (lastModified >= cutOff) continue; try @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.Jobs } if (deleted > 0 || errors > 0) - logger.LogInformation("TemporalUploadsCleanupJob: deleted {Deleted} orphaned objects, {Errors} errors", deleted, errors); + logger.LogInformation("TemporalUploadsCleanUpJob: deleted {Deleted} orphaned objects, {Errors} errors", deleted, errors); } } } diff --git a/Infrastructure/Jobs/UploadIntentExpirationJob.cs b/Infrastructure/Jobs/UploadIntentExpirationJob.cs index ce74e86..2f21219 100644 --- a/Infrastructure/Jobs/UploadIntentExpirationJob.cs +++ b/Infrastructure/Jobs/UploadIntentExpirationJob.cs @@ -2,9 +2,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; -using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Infrastructure.Jobs { diff --git a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index b53337f..b404528 100644 --- a/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -1,9 +1,10 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Domain.Entities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.MassTransit.Arguments; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 4766ae5..3e8f6a8 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -1,10 +1,11 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Domain.Entities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index cf32e81..82d4d0b 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -1,8 +1,9 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs index 52ce5a7..22e061a 100644 --- a/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/CopyFileInBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs index f7d7db8..53803cf 100644 --- a/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/DownloadFileFromBucketActivity.cs @@ -1,8 +1,8 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities.Files { diff --git a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs index 911baf9..b148c2b 100644 --- a/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs @@ -1,9 +1,9 @@ using MassTransit; using Amazon.S3; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Arguments; using System.Net; +using Musify.Application.Contracts; namespace Musify.Infrastructure.MassTransit.Activities.Files { diff --git a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs index 5f6ceb8..8e1db84 100644 --- a/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/TransferFilesToBucketActivity.cs @@ -1,8 +1,8 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs index d3e18b6..62961bc 100644 --- a/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs +++ b/Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs index be2f675..f7a0c83 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/GeneratePictureWorkflowPathsActivity.cs @@ -1,6 +1,6 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Infrastructure.MassTransit.Activities.Arguments; +using Musify.Infrastructure.MassTransit.Arguments; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs index 941132b..193b100 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/ResizePictureActivity.cs @@ -1,8 +1,8 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index c6df282..63dd568 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -1,8 +1,8 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 3c807b8..55e9150 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,8 +1,8 @@ using MassTransit; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Infrastructure.MassTransit.Activities.Arguments; -using Musify.Infrastructure.MassTransit.Activities.Logs; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities { diff --git a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs index 8cff6f7..ec77a49 100644 --- a/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs +++ b/Infrastructure/MassTransit/Activities/PlayLists/DeletePlayListFromDbActivity.cs @@ -1,7 +1,8 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; namespace Musify.Infrastructure.MassTransit.Activities.PlayLists { diff --git a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs index 0f6084a..b68f23a 100644 --- a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs +++ b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs @@ -1,8 +1,10 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.MassTransit.Arguments; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Infrastructure.MassTransit.Activities.PlayLists { diff --git a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs index 5e3cbf3..28e4c15 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/DeleteTrackFromDbActivity.cs @@ -1,7 +1,8 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; +using Musify.Infrastructure.MassTransit.Arguments; namespace Musify.Infrastructure.MassTransit.Activities.Tracks { diff --git a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs index 9f07a62..2256f42 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs @@ -1,8 +1,10 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.MassTransit.Arguments; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Application.Contracts; namespace Musify.Infrastructure.MassTransit.Activities.Tracks { diff --git a/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs b/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs index 15cb3ef..2f33f2b 100644 --- a/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs +++ b/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs @@ -1,8 +1,9 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Arguments/DeletePlayListFromDbArguments.cs b/Infrastructure/MassTransit/Arguments/DeletePlayListFromDbArguments.cs index 9890c26..b455ed9 100644 --- a/Infrastructure/MassTransit/Arguments/DeletePlayListFromDbArguments.cs +++ b/Infrastructure/MassTransit/Arguments/DeletePlayListFromDbArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +namespace Musify.Infrastructure.MassTransit.Arguments { internal record DeletePlayListFromDbArguments( Guid PlayListId); diff --git a/Infrastructure/MassTransit/Arguments/DeleteTrackFromDbArguments.cs b/Infrastructure/MassTransit/Arguments/DeleteTrackFromDbArguments.cs index be5b3ae..982e1ae 100644 --- a/Infrastructure/MassTransit/Arguments/DeleteTrackFromDbArguments.cs +++ b/Infrastructure/MassTransit/Arguments/DeleteTrackFromDbArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Tracks +namespace Musify.Infrastructure.MassTransit.Arguments { internal record DeleteTrackFromDbArguments( Guid TrackId); diff --git a/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs index 996306d..1579a91 100644 --- a/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/DownloadFileFromBucketArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record DownloadFileFromBucketArguments( string Bucket, diff --git a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs index 2acd8f7..0786562 100644 --- a/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GenerateAudioWorkflowPathsArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record GenerateAudioWorkflowPathsArguments( Guid TrackId, diff --git a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs index e112a6d..330a3b8 100644 --- a/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/GeneratePictureWorkflowPathsArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record GeneratePictureWorkflowPathsArguments( string TemporaryRootDirectory, diff --git a/Infrastructure/MassTransit/Arguments/MarkPlayListAsRemovingArguments.cs b/Infrastructure/MassTransit/Arguments/MarkPlayListAsRemovingArguments.cs index fefbf99..8cd63f7 100644 --- a/Infrastructure/MassTransit/Arguments/MarkPlayListAsRemovingArguments.cs +++ b/Infrastructure/MassTransit/Arguments/MarkPlayListAsRemovingArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.PlayLists +namespace Musify.Infrastructure.MassTransit.Arguments { internal record MarkPlayListAsRemovingArguments( Guid PlayListId); diff --git a/Infrastructure/MassTransit/Arguments/MarkTrackAsRemovingArguments.cs b/Infrastructure/MassTransit/Arguments/MarkTrackAsRemovingArguments.cs index 70625ca..5731258 100644 --- a/Infrastructure/MassTransit/Arguments/MarkTrackAsRemovingArguments.cs +++ b/Infrastructure/MassTransit/Arguments/MarkTrackAsRemovingArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Tracks +namespace Musify.Infrastructure.MassTransit.Arguments { internal record MarkTrackAsRemovingArguments( Guid TrackId); diff --git a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs index 2b402ca..d3d6689 100644 --- a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs +++ b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs @@ -1,3 +1,4 @@ +using Musify.Application.Common; using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Arguments diff --git a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs index aebba17..6d4926f 100644 --- a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs @@ -1,3 +1,4 @@ +using Musify.Application.Common; using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Arguments diff --git a/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs index ca2bb1c..5f3e499 100644 --- a/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/RemoveFileFromBucketArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record RemoveFileFromBucketArguments( string Bucket, diff --git a/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs index 633d34e..022fc7c 100644 --- a/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs +++ b/Infrastructure/MassTransit/Arguments/ResizePictureLocalArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record ResizePictureLocalArguments( string SourceFilePathVariable, diff --git a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs index 379a14c..08f0cef 100644 --- a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record TranscodeDashAudioArguments( Guid TrackId, diff --git a/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs index d8b9313..48f804d 100644 --- a/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TransferFilesToBucketArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record TransferFilesToBucketArguments( string DestinationBucket, diff --git a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs index d47d31f..bb05ee6 100644 --- a/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdatePlayListPictureArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record UpdatePlayListPictureArguments( Guid PlayListId, diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs index 005c260..0c48ac3 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackPictureArguments.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { public record UpdateTrackPictureArguments( Guid TrackId, diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index 438b5e6..c700164 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -2,10 +2,8 @@ using MassTransit; using Microsoft.Extensions.DependencyInjection; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Audio; using Musify.Infrastructure.MassTransit.Activities.Files; -using Musify.Infrastructure.MassTransit.Activities.Logs; using Musify.Infrastructure.MassTransit.Activities.PlayLists; using Musify.Infrastructure.MassTransit.Activities.Tracks; using Musify.Infrastructure.MassTransit.Activities.UploadIntents; diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 1c497ce..684a9f6 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -1,7 +1,7 @@ using MassTransit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Persistence; diff --git a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs index b46f9a5..bb00b6e 100644 --- a/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/DownloadFileFromBucketLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record DownloadFileFromBucketLog( string Bucket, diff --git a/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs b/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs index 7c23cba..9a9c223 100644 --- a/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs +++ b/Infrastructure/MassTransit/Logs/GeneratePictureWorkflowPathsLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record GeneratePictureWorkflowPathsLog(string WorkingDirectory); } diff --git a/Infrastructure/MassTransit/Logs/ResizePictureLog.cs b/Infrastructure/MassTransit/Logs/ResizePictureLog.cs index bfd59a0..f255d51 100644 --- a/Infrastructure/MassTransit/Logs/ResizePictureLog.cs +++ b/Infrastructure/MassTransit/Logs/ResizePictureLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record ResizePictureLog(string DestinationFilePath); } diff --git a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs index 038ec0b..05c6a9f 100644 --- a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs +++ b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record TranscodeDashAudioLog(string WorkingDirectory); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs index 6eeee6c..5db2002 100644 --- a/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs +++ b/Infrastructure/MassTransit/Logs/TransferFilesToBucketLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record TransferFilesToBucketLog(string DestinationBucket, string[] UploadedKeys); } diff --git a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs index 069442f..8135ec8 100644 --- a/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdatePlayListPictureLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record UpdatePlayListPictureLog( Guid PlayListId, diff --git a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs index c6e7387..a634c2f 100644 --- a/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs +++ b/Infrastructure/MassTransit/Logs/UpdateTrackPictureLog.cs @@ -1,4 +1,4 @@ -namespace Musify.Infrastructure.MassTransit.Activities.Logs +namespace Musify.Infrastructure.MassTransit.Logs { public record UpdateTrackPictureLog( Guid TrackId, diff --git a/Infrastructure/MassTransit/MassTransitEventBus.cs b/Infrastructure/MassTransit/MassTransitEventBus.cs index 23f69de..beaea4f 100644 --- a/Infrastructure/MassTransit/MassTransitEventBus.cs +++ b/Infrastructure/MassTransit/MassTransitEventBus.cs @@ -1,5 +1,5 @@ using MassTransit; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; namespace Musify.Infrastructure.MassTransit { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index b5713b3..d186353 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -4,10 +4,9 @@ using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities.Audio; using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; -using Musify.Infrastructure.MassTransit.Activities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs index 1d6efa8..efe4837 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs @@ -1,12 +1,12 @@ using MassTransit; using MassTransit.Courier.Contracts; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Infrastructure.MassTransit.Activities.Files; -using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.PlayLists; +using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; +using Musify.Application.Contracts; namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index eb2e944..f6ed175 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -1,12 +1,12 @@ using MassTransit; using MassTransit.Courier.Contracts; using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; -using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.Tracks; +using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; +using Musify.Application.Contracts; namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index fec9322..712298e 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -1,9 +1,9 @@ using MassTransit; using MassTransit.Courier.Contracts; +using Musify.Application.Common; using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities; -using Musify.Infrastructure.MassTransit.Activities.Arguments; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Consumers; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs index 80cf5c2..7e186c9 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs @@ -1,5 +1,6 @@ using MassTransit; using MassTransit.Courier.Contracts; +using Musify.Application.Common; using Musify.Application.Events; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.PlayLists; diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index f74cf4f..98f1ea1 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -1,10 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using MassTransit; -using Musify.Application.Contracts.Infrastructure; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; -using AppIDatabase = Musify.Application.Contracts.Infrastructure.IDatabase; +using AppIDatabase = Musify.Application.Contracts.IDatabase; +using Musify.Application.Contracts; namespace Musify.Infrastructure.Persistence { diff --git a/Infrastructure/Persistence/DatabaseDependencyInjection.cs b/Infrastructure/Persistence/DatabaseDependencyInjection.cs index a5eb939..6d3a9d9 100644 --- a/Infrastructure/Persistence/DatabaseDependencyInjection.cs +++ b/Infrastructure/Persistence/DatabaseDependencyInjection.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Infrastructure.Configuration; namespace Musify.Infrastructure.Persistence diff --git a/Infrastructure/Services/AudioTranscoderService.cs b/Infrastructure/Services/AudioTranscoderService.cs index 1bec040..805f4fc 100644 --- a/Infrastructure/Services/AudioTranscoderService.cs +++ b/Infrastructure/Services/AudioTranscoderService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Infrastructure.Configuration; using System.Diagnostics; diff --git a/Infrastructure/Services/PictureService.cs b/Infrastructure/Services/PictureService.cs index f50f9ba..4237519 100644 --- a/Infrastructure/Services/PictureService.cs +++ b/Infrastructure/Services/PictureService.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index af6ed83..36d9b5c 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -2,11 +2,11 @@ using Amazon.S3; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Musify.Application.Contracts.Infrastructure; using Musify.Application.Configuration; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Jobs; using Musify.Application.Services; +using Musify.Application.Contracts; namespace Musify.Infrastructure.Services { @@ -66,7 +66,7 @@ namespace Musify.Infrastructure.Services serviceDescriptors.AddUploadIntentConfiguration(configuration); serviceDescriptors.AddHostedService(); - serviceDescriptors.AddHostedService(); + serviceDescriptors.AddHostedService(); return serviceDescriptors; } diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 757499c..7f11820 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -3,10 +3,11 @@ using Amazon.S3.Model; using Amazon.S3.Transfer; using Microsoft.Extensions.Logging; using MimeMapping; -using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Contracts; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; using Musify.Infrastructure.Configuration; -using ObjectMetadata = Musify.Application.Contracts.Infrastructure.ObjectMetadata; +using ObjectMetaData = Musify.Application.Contracts.ObjectMetaData; namespace Musify.Infrastructure.Services { @@ -70,12 +71,12 @@ namespace Musify.Infrastructure.Services return await amazonS3.GetPreSignedURLAsync(request); } - public async Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken) + public async Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken) { try { var response = await amazonS3.GetObjectMetadataAsync(bucket, key, cancellationToken); - return new ObjectMetadata(response.Headers.ContentType, response.ContentLength); + return new ObjectMetaData(response.Headers.ContentType, response.ContentLength); } catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) { From b8c2de705db54245b7d3576b221014ee2c22aa30 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 21:04:09 +0200 Subject: [PATCH 147/172] PreventOverwrite --- Application/Contracts/IStorageService.cs | 6 ++---- Infrastructure/Services/StorageService.cs | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Application/Contracts/IStorageService.cs b/Application/Contracts/IStorageService.cs index 388d680..2a29c15 100644 --- a/Application/Contracts/IStorageService.cs +++ b/Application/Contracts/IStorageService.cs @@ -4,12 +4,11 @@ namespace Musify.Application.Contracts { Task GetFileAsync(string bucket, string key, CancellationToken cancellationToken); - // Returns null if the object does not exist Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken); Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); - Task GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, CancellationToken cancellationToken); + Task GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, CancellationToken cancellationToken, bool preventOverwrite = true); Task UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken); @@ -23,8 +22,7 @@ namespace Musify.Application.Contracts CancellationToken cancellationToken); Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken); - - // Returns (key, lastModifiedUtc) for all objects under the given prefix + IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)> ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 7f11820..3ea9804 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -55,7 +55,8 @@ namespace Musify.Infrastructure.Services string key, string contentType, TimeSpan expirationTime, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + bool preventOverwrite = true) { var request = new GetPreSignedUrlRequest { @@ -67,6 +68,9 @@ namespace Musify.Infrastructure.Services ContentType = contentType }; + if (preventOverwrite) + request.Headers["If-None-Match"] = "*"; + logger.LogDebug("Generating pre-signed upload URL for {Bucket}/{Key}", bucket, key); return await amazonS3.GetPreSignedURLAsync(request); } From 59eb2435404863ec414b59a0d206e5e668ea8509 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 21:07:49 +0200 Subject: [PATCH 148/172] Change on files --- Application/Contracts/IStorageService.cs | 2 +- Application/Contracts/StorageObject.cs | 4 ++++ Application/Events/CreatePlayListResourcesEvent.cs | 2 +- Application/Events/CreateTrackResourcesEvent.cs | 2 +- Application/Events/UpdatePlayListPictureEvent.cs | 2 +- Application/Events/UpdatePlayListPictureSourceEvent.cs | 2 +- Application/Events/UpdateTrackPictureEvent.cs | 2 +- .../PlayLists/Handlers/CreatePlayListCommandHandler.cs | 2 +- .../PlayLists/Handlers/UpdatePlayListCommandHandler.cs | 2 +- Application/{Common => Shared}/ImageSize.cs | 2 +- Application/Tracks/Handlers/CreateTrackCommandHandler.cs | 2 +- .../PublishPlayListPictureProcessingEventArguments.cs | 2 +- .../Arguments/PublishTrackProcessingEventsArguments.cs | 2 +- .../RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs | 2 +- .../Builders/PlayListPictureSourceRoutingSlipBuilder.cs | 2 +- Infrastructure/Services/StorageService.cs | 4 ++-- 16 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 Application/Contracts/StorageObject.cs rename Application/{Common => Shared}/ImageSize.cs (71%) diff --git a/Application/Contracts/IStorageService.cs b/Application/Contracts/IStorageService.cs index 2a29c15..02fe1f1 100644 --- a/Application/Contracts/IStorageService.cs +++ b/Application/Contracts/IStorageService.cs @@ -23,6 +23,6 @@ namespace Musify.Application.Contracts Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken); - IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)> ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken); + IAsyncEnumerable ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Contracts/StorageObject.cs b/Application/Contracts/StorageObject.cs new file mode 100644 index 0000000..2ec4d45 --- /dev/null +++ b/Application/Contracts/StorageObject.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Contracts +{ + public record StorageObject(string Key, DateTime LastModifiedUtc); +} diff --git a/Application/Events/CreatePlayListResourcesEvent.cs b/Application/Events/CreatePlayListResourcesEvent.cs index 3954a90..80266cb 100644 --- a/Application/Events/CreatePlayListResourcesEvent.cs +++ b/Application/Events/CreatePlayListResourcesEvent.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; namespace Musify.Application.Events { diff --git a/Application/Events/CreateTrackResourcesEvent.cs b/Application/Events/CreateTrackResourcesEvent.cs index 2482d25..e7f08b5 100644 --- a/Application/Events/CreateTrackResourcesEvent.cs +++ b/Application/Events/CreateTrackResourcesEvent.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; namespace Musify.Application.Events { diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs index 2a3e760..d617c75 100644 --- a/Application/Events/UpdatePlayListPictureEvent.cs +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; namespace Musify.Application.Events { diff --git a/Application/Events/UpdatePlayListPictureSourceEvent.cs b/Application/Events/UpdatePlayListPictureSourceEvent.cs index fdc7cbd..24c7101 100644 --- a/Application/Events/UpdatePlayListPictureSourceEvent.cs +++ b/Application/Events/UpdatePlayListPictureSourceEvent.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; namespace Musify.Application.Events { diff --git a/Application/Events/UpdateTrackPictureEvent.cs b/Application/Events/UpdateTrackPictureEvent.cs index 3fd0547..8973e64 100644 --- a/Application/Events/UpdateTrackPictureEvent.cs +++ b/Application/Events/UpdateTrackPictureEvent.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; namespace Musify.Application.Events { diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 1b7d894..38f43c4 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -3,7 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 45f01b8..c099d5e 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -3,7 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; diff --git a/Application/Common/ImageSize.cs b/Application/Shared/ImageSize.cs similarity index 71% rename from Application/Common/ImageSize.cs rename to Application/Shared/ImageSize.cs index 17b104f..c6f27d2 100644 --- a/Application/Common/ImageSize.cs +++ b/Application/Shared/ImageSize.cs @@ -1,4 +1,4 @@ -namespace Musify.Application.Common +namespace Musify.Application.Shared { public record ImageSize( string SavePath, diff --git a/Application/Tracks/Handlers/CreateTrackCommandHandler.cs b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs index b33ce1a..04f4569 100644 --- a/Application/Tracks/Handlers/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs @@ -3,7 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; diff --git a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs index d3d6689..ce28509 100644 --- a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs +++ b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Arguments diff --git a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs index 6d4926f..1d10795 100644 --- a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs @@ -1,4 +1,4 @@ -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Arguments diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 712298e..0fcbf34 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -1,6 +1,6 @@ using MassTransit; using MassTransit.Courier.Contracts; -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Activities; diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs index 7e186c9..f863910 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/PlayListPictureSourceRoutingSlipBuilder.cs @@ -1,6 +1,6 @@ using MassTransit; using MassTransit.Courier.Contracts; -using Musify.Application.Common; +using Musify.Application.Shared; using Musify.Application.Events; using Musify.Infrastructure.MassTransit.Activities.Files; using Musify.Infrastructure.MassTransit.Activities.PlayLists; diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 3ea9804..76bbb5e 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -228,7 +228,7 @@ namespace Musify.Infrastructure.Services } } - public async IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)> ListObjectsAsync( + public async IAsyncEnumerable ListObjectsAsync( string bucket, string prefix, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { var listRequest = new ListObjectsV2Request @@ -242,7 +242,7 @@ namespace Musify.Infrastructure.Services { listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken); foreach (var obj in listResponse.S3Objects) - yield return (obj.Key, (obj.LastModified ?? DateTime.UtcNow).ToUniversalTime()); + yield return new StorageObject(obj.Key, (obj.LastModified ?? DateTime.UtcNow).ToUniversalTime()); listRequest.ContinuationToken = listResponse.NextContinuationToken; } From 9000e563c43a74ec80eca041a73d6c619743aa25 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 28 May 2026 23:16:35 +0200 Subject: [PATCH 149/172] Remove keycloak --- .../Handlers/GetUserByKeycloakQueryHandler.cs | 23 ------------------- .../Users/Queries/GetUserByKeycloakQuery.cs | 9 -------- 2 files changed, 32 deletions(-) delete mode 100644 Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs delete mode 100644 Application/Users/Queries/GetUserByKeycloakQuery.cs diff --git a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs b/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs deleted file mode 100644 index db8173b..0000000 --- a/Application/Users/Handlers/GetUserByKeycloakQueryHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Ardalis.Result; -using Mediator; -using Microsoft.EntityFrameworkCore; -using Musify.Application.Contracts; -using Musify.Application.Users.Queries; -using Musify.Application.Users.Responses; - -namespace Musify.Application.Users.Handlers -{ - public class GetUserByKeycloakQueryHandler(IDatabase database) - : IQueryHandler > - { - public async ValueTask> Handle(GetUserByKeycloakQuery request, CancellationToken cancellationToken) - { - var user = await database.Users - .AsNoTracking() - .Select(u => UserApplicationResponse.FromEntity(u)) - .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - - return user ?? Result.NotFound(); - } - } -} diff --git a/Application/Users/Queries/GetUserByKeycloakQuery.cs b/Application/Users/Queries/GetUserByKeycloakQuery.cs deleted file mode 100644 index f351590..0000000 --- a/Application/Users/Queries/GetUserByKeycloakQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ardalis.Result; -using Mediator; -using Musify.Application.Users.Responses; - -namespace Musify.Application.Users.Queries -{ - public record GetUserByKeycloakQuery(Guid Id) - : IQuery >; -} From 7894126f97298606b30e15d526946b93407ec62e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 01:48:46 +0200 Subject: [PATCH 150/172] Changes --- Application/Application.csproj | 12 +++++----- Infrastructure/Infrastructure.csproj | 22 +++++++++---------- .../OpenTelemetryDependencyInjection.cs | 4 ++++ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index a6c1f02..c64c6c1 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -13,17 +13,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 8599ea5..379581c 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -14,22 +14,22 @@ - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + - + - + diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs index 0ac4f0c..0a2a861 100644 --- a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Musify.Infrastructure.Configuration; using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -48,6 +49,9 @@ namespace Musify.Infrastructure.Observability .AddRuntimeInstrumentation() .AddProcessInstrumentation() .AddOtlpExporter() + ) + .WithLogging(logging => + logging.AddOtlpExporter() ); return serviceDescriptors; From 0d5a6df27b790cd7a009756f8e6ea543e9b2c42d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 01:57:27 +0200 Subject: [PATCH 151/172] Rename --- .../Observability/OpenTelemetryDependencyInjection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs index 0a2a861..75c4903 100644 --- a/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs +++ b/Infrastructure/Observability/OpenTelemetryDependencyInjection.cs @@ -12,7 +12,7 @@ namespace Musify.Infrastructure.Observability { public static class OpenTelemetryDependencyInjection { - public static IServiceCollection AddInfrastructureOpenTelemetry(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddObservability(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors .AddOptionsWithValidateOnStart() From 51dd877a4401ffb7f6c581edb719f475f05e0b67 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 13:49:44 +0200 Subject: [PATCH 152/172] Added tests --- .../Handlers/GetPlayListByIdQueryHandler.cs | 7 +- .../Handlers/GetTrackByIdQueryHandler.cs | 7 +- .../Users/Handlers/GetUserByIdQueryHandler.cs | 7 +- MusifyBackend.slnx | 4 + .../Configuration/RoutesTests.cs | 86 ++++++ .../Domain/TrackTests.cs | 89 ++++++ .../Extensions/ResultExtensionsTests.cs | 74 +++++ .../CreatePlayListCommandHandlerTests.cs | 105 +++++++ .../CreateTrackCommandHandlerTests.cs | 141 ++++++++++ .../Handlers/CreateUserCommandHandlerTests.cs | 44 +++ .../DeletePlayListCommandHandlerTests.cs | 88 ++++++ .../DeleteTrackCommandHandlerTests.cs | 108 ++++++++ .../Handlers/GetByIdQueryHandlerTests.cs | 93 +++++++ .../GetTracksByUserIdQueryHandlerTests.cs | 83 ++++++ .../Handlers/ListQueryHandlerTests.cs | 135 +++++++++ ...layListPictureUploadCommandHandlerTests.cs | 129 +++++++++ ...questTrackUploadUrlsCommandHandlerTests.cs | 134 +++++++++ .../UpdatePlayListCommandHandlerTests.cs | 144 ++++++++++ .../Infrastructure/SchemaSmokeTests.cs | 42 +++ .../Infrastructure/SqliteTestContext.cs | 44 +++ .../Infrastructure/TestData.cs | 75 +++++ .../Infrastructure/TestDatabase.cs | 39 +++ .../Musify.Application.Tests.csproj | 29 ++ .../Pagination/PaginatedResponseTests.cs | 38 +++ .../Services/UploadIntentValidatorTests.cs | 256 ++++++++++++++++++ .../JobTestHarness.cs | 32 +++ .../Musify.Infrastructure.Tests.csproj | 29 ++ .../TemporalUploadsCleanUpJobTests.cs | 72 +++++ .../UploadIntentExpirationJobTests.cs | 84 ++++++ 29 files changed, 2212 insertions(+), 6 deletions(-) create mode 100644 Tests/Musify.Application.Tests/Configuration/RoutesTests.cs create mode 100644 Tests/Musify.Application.Tests/Domain/TrackTests.cs create mode 100644 Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs create mode 100644 Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs create mode 100644 Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs create mode 100644 Tests/Musify.Application.Tests/Infrastructure/TestData.cs create mode 100644 Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs create mode 100644 Tests/Musify.Application.Tests/Musify.Application.Tests.csproj create mode 100644 Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs create mode 100644 Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs create mode 100644 Tests/Musify.Infrastructure.Tests/JobTestHarness.cs create mode 100644 Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj create mode 100644 Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs create mode 100644 Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs diff --git a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs index b6881b6..5fc3439 100644 --- a/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListByIdQueryHandler.cs @@ -14,10 +14,13 @@ namespace Musify.Application.PlayLists.Handlers { var playList = await database.PlayLists .AsNoTracking() + .Where(p => p.Id == request.Id) .Select(p => PlayListApplicationResponse.FromEntity(p)) - .SingleOrDefaultAsync(p => p.Id == request.Id, cancellationToken); + .SingleOrDefaultAsync(cancellationToken); - return playList ?? Result.NotFound(); + return playList is null + ? Result.NotFound() + : Result.Success(playList); } } } \ No newline at end of file diff --git a/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs b/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs index 52e48c0..8fa199b 100644 --- a/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTrackByIdQueryHandler.cs @@ -13,10 +13,13 @@ namespace Musify.Application.Tracks.Handlers public async ValueTask> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken) { var track = await database.Tracks.AsNoTracking() + .Where(t => t.Id == request.TrackId) .Select(t => TrackApplicationResponse.FromEntity(t)) - .SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken); + .SingleOrDefaultAsync(cancellationToken); - return track ?? Result.NotFound(); + return track is null + ? Result.NotFound() + : Result.Success(track); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/GetUserByIdQueryHandler.cs b/Application/Users/Handlers/GetUserByIdQueryHandler.cs index 9abc9fc..c99c799 100644 --- a/Application/Users/Handlers/GetUserByIdQueryHandler.cs +++ b/Application/Users/Handlers/GetUserByIdQueryHandler.cs @@ -14,10 +14,13 @@ namespace Musify.Application.Users.Handlers { var user = await database.Users .AsNoTracking() + .Where(u => u.Id == request.Id) .Select(u => UserApplicationResponse.FromEntity(u)) - .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); + .SingleOrDefaultAsync(cancellationToken); - return user ?? Result.NotFound(); + return user is null + ? Result.NotFound() + : Result.Success(user); } } } \ No newline at end of file diff --git a/MusifyBackend.slnx b/MusifyBackend.slnx index c92b66b..386f50d 100644 --- a/MusifyBackend.slnx +++ b/MusifyBackend.slnx @@ -1,4 +1,8 @@ + + + + diff --git a/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs b/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs new file mode 100644 index 0000000..a0c0cbf --- /dev/null +++ b/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs @@ -0,0 +1,86 @@ +using FluentAssertions; +using Musify.Application.Configuration; +using Xunit; + +namespace Musify.Application.Tests.Configuration; + +public sealed class RoutesTests +{ + private static readonly Guid UserId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + + [Fact] + public void TrackRoutes_default_picture_and_audio_paths() + { + var routes = new TrackRoutes(); + + routes.OriginalPicturesPath.Should().Be("Tracks/OriginalPictures"); + routes.OriginalAudiosPath.Should().Be("Tracks/OriginalAudios"); + routes.ProcessedAudiosPath.Should().Be("Tracks/ProcessedAudios"); + } + + [Fact] + public void TrackRoutes_BuildOriginalPicturePath_with_user_nests_under_uploads() + { + var routes = new TrackRoutes(); + + routes.BuildOriginalPicturePath(UserId, "cover.webp") + .Should().Be($"uploads/{UserId}/Tracks/OriginalPictures/cover.webp"); + } + + [Fact] + public void TrackRoutes_BuildOriginalAudioPath_with_user_nests_under_uploads() + { + var routes = new TrackRoutes(); + + routes.BuildOriginalAudioPath(UserId, "song.mp3") + .Should().Be($"uploads/{UserId}/Tracks/OriginalAudios/song.mp3"); + } + + [Fact] + public void TrackRoutes_BuildTempPicturePath_uses_prefix_user_and_parent_folder() + { + var routes = new TrackRoutes(); + + routes.BuildTempPicturePath("temp", UserId, "obj.png") + .Should().Be($"temp/{UserId}/Tracks/obj.png"); + } + + [Fact] + public void CombineKey_trims_slashes_and_whitespace_per_segment() + { + var routes = new TrackRoutes(); + + var result = routes.BuildTempPicturePath(" /temp/ ", UserId, "/obj.png\\"); + + result.Should().Be($"temp/{UserId}/Tracks/obj.png"); + } + + [Fact] + public void CombineKey_drops_empty_and_whitespace_segments() + { + var routes = new TrackRoutes(); + + var result = routes.BuildTempPicturePath(" ", UserId, "obj.png"); + + result.Should().Be($"{UserId}/Tracks/obj.png"); + } + + [Fact] + public void PlayListRoutes_BuildOriginalPicturePath_with_user_nests_under_uploads() + { + var routes = new PlayListRoutes(); + + routes.BuildOriginalPicturePath(UserId, "cover.webp") + .Should().Be($"uploads/{UserId}/PlayLists/OriginalPictures/cover.webp"); + } + + [Fact] + public void PlayListRoutes_size_paths_use_parent_folder() + { + var routes = new PlayListRoutes(); + + routes.SmallPicturesPath.Should().Be("PlayLists/SmallPictures"); + routes.MediumPicturesPath.Should().Be("PlayLists/MediumPictures"); + routes.LargePicturesPath.Should().Be("PlayLists/LargePictures"); + } +} diff --git a/Tests/Musify.Application.Tests/Domain/TrackTests.cs b/Tests/Musify.Application.Tests/Domain/TrackTests.cs new file mode 100644 index 0000000..f56505d --- /dev/null +++ b/Tests/Musify.Application.Tests/Domain/TrackTests.cs @@ -0,0 +1,89 @@ +using FluentAssertions; +using Musify.Domain.ValueObjects; +using Musify.Application.Tests.Infrastructure; +using Xunit; + +namespace Musify.Application.Tests.Domain; + +public sealed class TrackTests +{ + [Fact] + public void IsPicturesProcessed_is_true_only_when_completed() + { + var track = TestData.Track(); + + track.PicturesProcessingStatus = ProcessingStatus.Completed; + track.IsPicturesProcessed.Should().BeTrue(); + + track.PicturesProcessingStatus = ProcessingStatus.Processing; + track.IsPicturesProcessed.Should().BeFalse(); + + track.PicturesProcessingStatus = ProcessingStatus.Failed; + track.IsPicturesProcessed.Should().BeFalse(); + } + + [Fact] + public void IsAudioProcessed_is_true_only_when_completed() + { + var track = TestData.Track(); + + track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; + track.IsAudioProcessed.Should().BeTrue(); + + track.AudioTranscodeProcessingStatus = ProcessingStatus.Pending; + track.IsAudioProcessed.Should().BeFalse(); + } + + [Fact] + public void Defaults_are_active_pending_and_zero_retries() + { + var track = new Musify.Domain.Entities.Track + { + Title = "t", + NormalizedTitle = "T", + OriginalPictureName = "p.webp", + OriginalAudioName = "a.mp3", + }; + + track.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); + track.PicturesProcessingStatus.Should().Be(ProcessingStatus.Pending); + track.AudioTranscodeProcessingStatus.Should().Be(ProcessingStatus.Pending); + track.RetryCount.Should().Be(0); + track.Id.Should().NotBe(Guid.Empty); + } +} + +public sealed class EnumValueRegressionTests +{ + [Fact] + public void ProcessingStatus_values_are_stable() + { + ((int)ProcessingStatus.Pending).Should().Be(0); + ((int)ProcessingStatus.Processing).Should().Be(1); + ((int)ProcessingStatus.Completed).Should().Be(2); + ((int)ProcessingStatus.Failed).Should().Be(3); + } + + [Fact] + public void LifeCycleStatus_values_are_stable() + { + ((int)LifeCycleStatus.Active).Should().Be(0); + ((int)LifeCycleStatus.Removing).Should().Be(1); + } + + [Fact] + public void UploadIntentStatus_values_are_stable() + { + ((int)UploadIntentStatus.Issued).Should().Be(0); + ((int)UploadIntentStatus.Consumed).Should().Be(1); + ((int)UploadIntentStatus.Expired).Should().Be(2); + } + + [Fact] + public void UploadIntentPurpose_values_are_stable() + { + ((int)UploadIntentPurpose.PlayListPicture).Should().Be(0); + ((int)UploadIntentPurpose.TrackPicture).Should().Be(1); + ((int)UploadIntentPurpose.TrackAudio).Should().Be(2); + } +} diff --git a/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs b/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs new file mode 100644 index 0000000..72ef974 --- /dev/null +++ b/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs @@ -0,0 +1,74 @@ +using Ardalis.Result; +using FluentAssertions; +using Musify.Application.Extensions; +using Xunit; + +namespace Musify.Application.Tests.Extensions; + +public sealed class ResultExtensionsTests +{ + [Fact] + public void NotFound_maps_to_NotFound_and_preserves_errors() + { + var source = Result.NotFound("missing"); + + var mapped = source.As(); + + mapped.Status.Should().Be(ResultStatus.NotFound); + mapped.Errors.Should().Contain("missing"); + } + + [Fact] + public void Conflict_maps_to_Conflict() + { + Result.Conflict("dup").As().Status.Should().Be(ResultStatus.Conflict); + } + + [Fact] + public void Unauthorized_maps_to_Unauthorized() + { + Result.Unauthorized().As().Status.Should().Be(ResultStatus.Unauthorized); + } + + [Fact] + public void Forbidden_maps_to_Forbidden() + { + Result.Forbidden().As().Status.Should().Be(ResultStatus.Forbidden); + } + + [Fact] + public void Invalid_maps_to_Invalid_and_preserves_validation_errors() + { + var source = Result.Invalid(new ValidationError("field", "bad")); + + var mapped = source.As(); + + mapped.Status.Should().Be(ResultStatus.Invalid); + mapped.ValidationErrors.Should().ContainSingle(e => e.ErrorMessage == "bad"); + } + + [Fact] + public void Error_maps_to_Error() + { + Result.Error("boom").As().Status.Should().Be(ResultStatus.Error); + } + + [Theory] + [InlineData(ResultStatus.Ok)] + [InlineData(ResultStatus.Created)] + [InlineData(ResultStatus.NoContent)] + public void Successful_results_throw_when_converted(ResultStatus status) + { + Result source = status switch + { + ResultStatus.Ok => Result.Success(1), + ResultStatus.Created => Result.Created(1), + ResultStatus.NoContent => Result.NoContent(), + _ => throw new InvalidOperationException() + }; + + var act = () => source.As(); + + act.Should().Throw(); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs new file mode 100644 index 0000000..6f60132 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs @@ -0,0 +1,105 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Events; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Handlers; +using Musify.Application.Services; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class CreatePlayListCommandHandlerTests +{ + private readonly IEventBus _eventBus = Substitute.For(); + private readonly IStorageService _storage = Substitute.For(); + + private CreatePlayListCommandHandler CreateSut(TestDatabase db) => + new( + _eventBus, + db, + new UploadIntentValidator(db, _storage), + NullLogger.Instance, + TestData.StorageConfig(), + TestData.PlayListConfig(), + TestData.UploadIntentConfig()); + + [Fact] + public async Task When_user_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle( + new CreatePlayListCommand(Guid.NewGuid(), "List", "desc", null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task Without_picture_intent_uses_preset_and_does_not_publish() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreatePlayListCommand(user.Id, " Chill Vibes ", "desc", null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Created); + await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); + + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id); + persisted.OriginalPictureName.Should().Be(TestData.PlayListConfig().Routes.PresetOriginalPicture); + persisted.NormalizedName.Should().Be("CHILL VIBES"); + } + + [Fact] + public async Task With_valid_picture_intent_publishes_event_with_intent_object_name() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + var intent = TestData.UploadIntent(user.Id, UploadIntentPurpose.PlayListPicture, objectName: "cover.webp"); + ctx.Database.Users.Add(user); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) + .Returns(new ObjectMetaData("image/webp", 500)); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreatePlayListCommand(user.Id, "List", "desc", intent.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Created); + await _eventBus.Received(1).PublishAsync( + Arg.Is(e => e.PictureIntentId == intent.Id), + Arg.Any()); + + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id); + persisted.OriginalPictureName.Should().Be("cover.webp"); + } + + [Fact] + public async Task With_invalid_picture_intent_propagates_error_and_does_not_persist() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreatePlayListCommand(user.Id, "List", "desc", Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + (await ctx.NewContext().PlayLists.CountAsync()).Should().Be(0); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs new file mode 100644 index 0000000..640b45b --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs @@ -0,0 +1,141 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Events; +using Musify.Application.Services; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Tracks.Commands; +using Musify.Application.Tracks.Handlers; +using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class CreateTrackCommandHandlerTests +{ + private readonly IEventBus _eventBus = Substitute.For(); + private readonly IStorageService _storage = Substitute.For(); + + private CreateTrackCommandHandler CreateSut(TestDatabase db) => + new( + db, + _eventBus, + new UploadIntentValidator(db, _storage), + NullLogger.Instance, + TestData.StorageConfig(), + TestData.TrackConfig(), + TestData.UploadIntentConfig()); + + private void StorageHasObject() => + _storage.HeadObjectAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new ObjectMetaData("application/octet-stream", 500)); + + private static async Task<(UploadIntent picture, UploadIntent audio)> SeedIntentsAsync( + SqliteTestContext ctx, Guid userId) + { + var picture = TestData.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{userId}/pic.png"); + var audio = TestData.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", key: $"temp/{userId}/song.mp3"); + ctx.Database.UploadIntents.AddRange(picture, audio); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + return (picture, audio); + } + + [Fact] + public async Task When_user_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle( + new CreateTrackCommand(Guid.NewGuid(), "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task When_picture_intent_invalid_propagates_and_creates_no_track() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreateTrackCommand(user.Id, "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + (await ctx.NewContext().Tracks.CountAsync()).Should().Be(0); + await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task When_audio_intent_invalid_propagates() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + var picture = TestData.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{user.Id}/pic.png"); + ctx.Database.Users.Add(user); + ctx.Database.UploadIntents.Add(picture); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + StorageHasObject(); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreateTrackCommand(user.Id, "Song", picture.Id, Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task Happy_path_creates_track_publishes_event_and_normalizes_title() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + var (picture, audio) = await SeedIntentsAsync(ctx, user.Id); + StorageHasObject(); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Created); + await _eventBus.Received(1).PublishAsync( + Arg.Is(e => + e.PictureIntentId == picture.Id && e.AudioIntentId == audio.Id), + Arg.Any()); + + var persisted = await ctx.NewContext().Tracks.SingleAsync(); + persisted.NormalizedTitle.Should().Be("MY SONG"); + persisted.OriginalPictureName.Should().Be("pic.png"); + persisted.OriginalAudioName.Should().Be("song.mp3"); + } + + [Fact] + public async Task When_publish_throws_returns_error_and_does_not_persist() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + var (picture, audio) = await SeedIntentsAsync(ctx, user.Id); + StorageHasObject(); + + _eventBus.PublishAsync(Arg.Any(), Arg.Any()) + .Returns(_ => throw new InvalidOperationException("bus down")); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Error); + (await ctx.NewContext().Tracks.CountAsync()).Should().Be(0); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs new file mode 100644 index 0000000..789a6dc --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs @@ -0,0 +1,44 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Users.Commands; +using Musify.Application.Users.Handlers; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class CreateUserCommandHandlerTests +{ + [Fact] + public async Task When_user_already_exists_returns_conflict() + { + await using var ctx = new SqliteTestContext(); + var existing = TestData.User(); + ctx.Database.Users.Add(existing); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new CreateUserCommandHandler(ctx.Database, NullLogger.Instance); + var result = await sut.Handle(new CreateUserCommand(existing.Id, "Bob", null, null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Conflict); + } + + [Fact] + public async Task Happy_path_creates_user_and_persists() + { + await using var ctx = new SqliteTestContext(); + var id = Guid.NewGuid(); + + var sut = new CreateUserCommandHandler(ctx.Database, NullLogger.Instance); + var result = await sut.Handle(new CreateUserCommand(id, "Bob", "Robert", "Smith"), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Created); + result.Value.Name.Should().Be("Bob"); + + var persisted = await ctx.NewContext().Users.SingleAsync(u => u.Id == id); + persisted.NormalizedName.Should().Be("BOB"); + persisted.FirstName.Should().Be("Robert"); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs new file mode 100644 index 0000000..9b69142 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs @@ -0,0 +1,88 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Events; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Handlers; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class DeletePlayListCommandHandlerTests +{ + private readonly IEventBus _eventBus = Substitute.For(); + + private DeletePlayListCommandHandler CreateSut(TestDatabase db) => + new(db, _eventBus, NullLogger.Instance); + + [Fact] + public async Task When_playlist_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task When_user_not_owner_returns_unauthorized() + { + await using var ctx = new SqliteTestContext(); + var playList = TestData.PlayList(Guid.NewGuid()); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), playList.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Unauthorized); + } + + [Fact] + public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var playList = TestData.PlayList(userId); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NoContent); + await _eventBus.Received(1).PublishAsync( + Arg.Is(e => e.PlayListId == playList.Id && e.UserId == userId), + Arg.Any()); + + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); + persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing); + } + + [Fact] + public async Task When_publish_throws_returns_error_and_does_not_persist() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var playList = TestData.PlayList(userId); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _eventBus.PublishAsync(Arg.Any(), Arg.Any()) + .Returns(_ => throw new InvalidOperationException("bus down")); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Error); + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); + persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs new file mode 100644 index 0000000..e81a555 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs @@ -0,0 +1,108 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Events; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Tracks.Commands; +using Musify.Application.Tracks.Handlers; +using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class DeleteTrackCommandHandlerTests +{ + private readonly IEventBus _eventBus = Substitute.For(); + + private DeleteTrackCommandHandler CreateSut(TestDatabase db) => + new(db, _eventBus, NullLogger.Instance); + + private static async Task<(Guid trackId, Guid userId)> SeedOwnedTrackAsync( + SqliteTestContext ctx, ProcessingStatus audioStatus) + { + var userId = Guid.NewGuid(); + var track = TestData.Track(audioStatus: audioStatus); + ctx.Database.Tracks.Add(track); + ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id }); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + return (track.Id, userId); + } + + [Fact] + public async Task When_track_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task When_user_not_owner_returns_unauthorized() + { + await using var ctx = new SqliteTestContext(); + var track = TestData.Track(); + ctx.Database.Tracks.Add(track); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), track.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Unauthorized); + } + + [Theory] + [InlineData(ProcessingStatus.Pending)] + [InlineData(ProcessingStatus.Processing)] + public async Task When_audio_still_processing_returns_conflict(ProcessingStatus status) + { + await using var ctx = new SqliteTestContext(); + var (trackId, userId) = await SeedOwnedTrackAsync(ctx, status); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Conflict); + await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content() + { + await using var ctx = new SqliteTestContext(); + var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NoContent); + await _eventBus.Received(1).PublishAsync( + Arg.Is(e => e.TrackId == trackId), Arg.Any()); + + var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId); + persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing); + } + + [Fact] + public async Task When_publish_throws_returns_error_and_does_not_persist() + { + await using var ctx = new SqliteTestContext(); + var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed); + + _eventBus.PublishAsync(Arg.Any(), Arg.Any()) + .Returns(_ => throw new InvalidOperationException("bus down")); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Error); + var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId); + persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs new file mode 100644 index 0000000..7c29582 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs @@ -0,0 +1,93 @@ +using Ardalis.Result; +using FluentAssertions; +using Musify.Application.PlayLists.Handlers; +using Musify.Application.PlayLists.Queries; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Tracks.Handlers; +using Musify.Application.Tracks.Queries; +using Musify.Application.Users.Handlers; +using Musify.Application.Users.Queries; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class GetByIdQueryHandlerTests +{ + [Fact] + public async Task GetTrackById_returns_track_when_found() + { + await using var ctx = new SqliteTestContext(); + var track = TestData.Track(title: "Found"); + ctx.Database.Tracks.Add(track); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetTrackByIdQueryHandler(ctx.Database); + var result = await sut.Handle(new GetTrackByIdQuery(track.Id), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.Title.Should().Be("Found"); + } + + [Fact] + public async Task GetTrackById_returns_not_found_when_missing() + { + await using var ctx = new SqliteTestContext(); + var sut = new GetTrackByIdQueryHandler(ctx.Database); + + var result = await sut.Handle(new GetTrackByIdQuery(Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task GetPlayListById_returns_playlist_when_found() + { + await using var ctx = new SqliteTestContext(); + var playList = TestData.PlayList(Guid.NewGuid(), name: "MyList"); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetPlayListByIdQueryHandler(ctx.Database); + var result = await sut.Handle(new GetPlayListByIdQuery(playList.Id), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("MyList"); + } + + [Fact] + public async Task GetPlayListById_returns_not_found_when_missing() + { + await using var ctx = new SqliteTestContext(); + var sut = new GetPlayListByIdQueryHandler(ctx.Database); + + var result = await sut.Handle(new GetPlayListByIdQuery(Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task GetUserById_returns_user_when_found() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(name: "Carol"); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetUserByIdQueryHandler(ctx.Database); + var result = await sut.Handle(new GetUserByIdQuery(user.Id), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("Carol"); + } + + [Fact] + public async Task GetUserById_returns_not_found_when_missing() + { + await using var ctx = new SqliteTestContext(); + var sut = new GetUserByIdQueryHandler(ctx.Database); + + var result = await sut.Handle(new GetUserByIdQuery(Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs new file mode 100644 index 0000000..0352e47 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs @@ -0,0 +1,83 @@ +using FluentAssertions; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Tracks.Handlers; +using Musify.Application.Tracks.Queries; +using Musify.Domain.Entities; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class GetTracksByUserIdQueryHandlerTests +{ + private static async Task SeedTrackForUserAsync(SqliteTestContext ctx, Guid userId, string title, DateTime created) + { + var track = TestData.Track(title: title); + track.CreatedDate = created; + ctx.Database.Tracks.Add(track); + ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id }); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + } + + [Fact] + public async Task Returns_only_tracks_owned_by_the_user_ordered_by_created_date() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var otherUser = Guid.NewGuid(); + await SeedTrackForUserAsync(ctx, userId, "Beta", new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc)); + await SeedTrackForUserAsync(ctx, userId, "Alpha", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + await SeedTrackForUserAsync(ctx, otherUser, "Gamma", new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc)); + + var sut = new GetTracksByUserIdQueryHandler(ctx.Database); + var result = await sut.Handle(new GetTracksByUserIdQuery(userId), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalItemCount.Should().Be(2); + result.Value.Items.Select(t => t.Title).Should().Equal("Alpha", "Beta"); + } + + [Fact] + public async Task Filters_by_normalized_name_contains() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + await SeedTrackForUserAsync(ctx, userId, "Summer Hit", DateTime.UtcNow); + await SeedTrackForUserAsync(ctx, userId, "Winter Song", DateTime.UtcNow); + + var sut = new GetTracksByUserIdQueryHandler(ctx.Database); + var result = await sut.Handle(new GetTracksByUserIdQuery(userId, Name: "summer"), CancellationToken.None); + + result.Value.TotalItemCount.Should().Be(1); + result.Value.Items.Single().Title.Should().Be("Summer Hit"); + } + + [Fact] + public async Task Normalizes_invalid_paging_arguments_to_defaults() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + for (var i = 0; i < 3; i++) + await SeedTrackForUserAsync(ctx, userId, $"Track {i}", DateTime.UtcNow.AddSeconds(i)); + + var sut = new GetTracksByUserIdQueryHandler(ctx.Database); + var result = await sut.Handle( + new GetTracksByUserIdQuery(userId, PageNumber: 0, PageSize: -5), CancellationToken.None); + + result.Value.PageNumber.Should().Be(1); + result.Value.PageSize.Should().Be(10); + result.Value.TotalItemCount.Should().Be(3); + } + + [Fact] + public async Task Returns_empty_page_when_user_has_no_tracks() + { + await using var ctx = new SqliteTestContext(); + + var sut = new GetTracksByUserIdQueryHandler(ctx.Database); + var result = await sut.Handle(new GetTracksByUserIdQuery(Guid.NewGuid()), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalItemCount.Should().Be(0); + result.Value.Items.Should().BeEmpty(); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs new file mode 100644 index 0000000..326a9fe --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs @@ -0,0 +1,135 @@ +using FluentAssertions; +using Musify.Application.PlayLists.Handlers; +using Musify.Application.PlayLists.Queries; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Tracks.Handlers; +using Musify.Application.Tracks.Queries; +using Musify.Application.Users.Handlers; +using Musify.Application.Users.Queries; +using Musify.Domain.Entities; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class ListQueryHandlerTests +{ + [Fact] + public async Task GetTracks_paginates_across_all_users() + { + await using var ctx = new SqliteTestContext(); + for (var i = 0; i < 15; i++) + { + var track = TestData.Track(title: $"Track {i:D2}"); + ctx.Database.Tracks.Add(track); + ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id }); + } + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetTracksQueryHandler(ctx.Database); + var result = await sut.Handle(new GetTracksQuery(PageNumber: 2, PageSize: 10), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalItemCount.Should().Be(15); + result.Value.PageNumber.Should().Be(2); + result.Value.Items.Should().HaveCount(5); + result.Value.HasPreviousPage.Should().BeTrue(); + result.Value.HasNextPage.Should().BeFalse(); + } + + [Fact] + public async Task GetTracks_normalizes_invalid_paging_to_defaults() + { + await using var ctx = new SqliteTestContext(); + var track = TestData.Track(); + ctx.Database.Tracks.Add(track); + ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id }); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetTracksQueryHandler(ctx.Database); + var result = await sut.Handle(new GetTracksQuery(PageNumber: 0, PageSize: -3), CancellationToken.None); + + result.Value.PageNumber.Should().Be(1); + result.Value.PageSize.Should().Be(10); + result.Value.TotalItemCount.Should().Be(1); + } + + [Fact] + public async Task GetPlayLists_returns_all_playlists_paginated() + { + await using var ctx = new SqliteTestContext(); + for (var i = 0; i < 3; i++) + ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: $"PL {i}")); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetPlayListsQueryHandler(ctx.Database); + var result = await sut.Handle(new GetPlayListsQuery(), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalItemCount.Should().Be(3); + result.Value.Items.Should().HaveCount(3); + } + + [Fact] + public async Task GetPlayListsByUserId_filters_by_user_and_name() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Summer Mix")); + ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Winter Mix")); + ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: "Summer Other")); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database); + + var all = await sut.Handle(new GetPlayListsByUserIdQuery(userId), CancellationToken.None); + all.Value.TotalItemCount.Should().Be(2); + + var filtered = await sut.Handle(new GetPlayListsByUserIdQuery(userId, Name: "summer"), CancellationToken.None); + filtered.Value.TotalItemCount.Should().Be(1); + filtered.Value.Items.Single().Name.Should().Be("Summer Mix"); + } + + [Fact] + public async Task GetPlayListsByUserId_returns_empty_for_unknown_user() + { + await using var ctx = new SqliteTestContext(); + var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database); + + var result = await sut.Handle(new GetPlayListsByUserIdQuery(Guid.NewGuid()), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalItemCount.Should().Be(0); + } + + [Fact] + public async Task GetUsers_filters_by_username_search_case_insensitively() + { + await using var ctx = new SqliteTestContext(); + ctx.Database.Users.Add(TestData.User(name: "Alice")); + ctx.Database.Users.Add(TestData.User(name: "Bob")); + ctx.Database.Users.Add(TestData.User(name: "Alicia")); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetUsersQueryHandler(ctx.Database); + var result = await sut.Handle(new GetUsersQuery(1, 10, UsernameSearch: "ali"), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalItemCount.Should().Be(2); + result.Value.Items.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Alicia"); + } + + [Fact] + public async Task GetUsers_normalizes_invalid_paging_to_defaults() + { + await using var ctx = new SqliteTestContext(); + ctx.Database.Users.Add(TestData.User(name: "Solo")); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = new GetUsersQueryHandler(ctx.Database); + var result = await sut.Handle(new GetUsersQuery(0, -1), CancellationToken.None); + + result.Value.PageNumber.Should().Be(1); + result.Value.PageSize.Should().Be(10); + result.Value.TotalItemCount.Should().Be(1); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs new file mode 100644 index 0000000..2268920 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs @@ -0,0 +1,129 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Configuration; +using Musify.Application.Contracts; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Handlers; +using Musify.Application.Services; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class RequestPlayListPictureUploadCommandHandlerTests +{ + private readonly IStorageService _storage = Substitute.For(); + + private RequestPlayListPictureUploadCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) => + new( + db, + new UploadIntentValidator(db, _storage), + _storage, + NullLogger.Instance, + TestData.StorageConfig(), + TestData.PlayListConfig(), + config ?? TestData.UploadIntentConfig()); + + private static RequestPlayListPictureUploadCommand Command(Guid userId, long? size = null) => + new(userId, ".WEBP", "image/webp", size); + + [Fact] + public async Task When_user_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task Happy_path_creates_single_intent_and_returns_url() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns("https://signed.example/upload"); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(Command(user.Id), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.UploadUrl.Should().Be("https://signed.example/upload"); + + var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); + intents.Should().ContainSingle(); + intents[0].Purpose.Should().Be(UploadIntentPurpose.PlayListPicture); + intents[0].ObjectName.Should().EndWith(".webp"); + } + + [Fact] + public async Task When_quota_exceeded_returns_invalid_and_creates_no_intent() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns("https://signed.example/upload"); + + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadIntentsPerUser = 0; + + var sut = CreateSut(ctx.Database, config); + var result = await sut.Handle(Command(user.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); + } + + [Fact] + public async Task Uses_default_picture_size_when_not_provided() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns("https://signed.example/upload"); + + var config = TestData.UploadIntentConfig(); + var sut = CreateSut(ctx.Database, config); + await sut.Handle(Command(user.Id), CancellationToken.None); + + var intent = await ctx.NewContext().UploadIntents.SingleAsync(i => i.UserId == user.Id); + intent.ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes); + } + + [Fact] + public async Task When_storage_throws_returns_error() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(_ => throw new InvalidOperationException("s3 down")); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(Command(user.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Error); + (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs new file mode 100644 index 0000000..c116dd4 --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs @@ -0,0 +1,134 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Configuration; +using Musify.Application.Contracts; +using Musify.Application.Services; +using Musify.Application.Tests.Infrastructure; +using Musify.Application.Tracks.Commands; +using Musify.Application.Tracks.Handlers; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class RequestTrackUploadUrlsCommandHandlerTests +{ + private readonly IStorageService _storage = Substitute.For(); + + private RequestTrackUploadUrlsCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) => + new( + db, + new UploadIntentValidator(db, _storage), + _storage, + NullLogger.Instance, + TestData.StorageConfig(), + TestData.TrackConfig(), + config ?? TestData.UploadIntentConfig()); + + private static RequestTrackUploadUrlsCommand Command(Guid userId, long? pic = null, long? audio = null) => + new(userId, ".PNG", "image/png", "mp3", "audio/mpeg", pic, audio); + + [Fact] + public async Task When_user_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task Happy_path_creates_two_intents_and_returns_urls() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns("https://signed.example/upload"); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(Command(user.Id), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.PictureUploadUrl.Should().Be("https://signed.example/upload"); + + var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); + intents.Should().HaveCount(2); + intents.Select(i => i.Purpose).Should().BeEquivalentTo( + new[] { UploadIntentPurpose.TrackPicture, UploadIntentPurpose.TrackAudio }); + intents.Should().Contain(i => i.ObjectName.EndsWith(".png")); + intents.Should().Contain(i => i.ObjectName.EndsWith(".mp3")); + } + + [Fact] + public async Task When_quota_exceeded_returns_invalid_and_creates_no_intents() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns("https://signed.example/upload"); + + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadIntentsPerUser = 1; + + var sut = CreateSut(ctx.Database, config); + var result = await sut.Handle(Command(user.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); + } + + [Fact] + public async Task Uses_default_sizes_when_not_provided() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns("https://signed.example/upload"); + + var config = TestData.UploadIntentConfig(); + var sut = CreateSut(ctx.Database, config); + await sut.Handle(Command(user.Id), CancellationToken.None); + + var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); + intents.Single(i => i.Purpose == UploadIntentPurpose.TrackPicture) + .ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes); + intents.Single(i => i.Purpose == UploadIntentPurpose.TrackAudio) + .ExpectedSizeBytes.Should().Be(config.DefaultExpectedAudioSizeBytes); + } + + [Fact] + public async Task When_storage_throws_returns_error() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.GetUploadUrlAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(_ => throw new InvalidOperationException("s3 down")); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle(Command(user.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Error); + (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); + } +} diff --git a/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs new file mode 100644 index 0000000..52957bc --- /dev/null +++ b/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs @@ -0,0 +1,144 @@ +using Ardalis.Result; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Events; +using Musify.Application.PlayLists.Commands; +using Musify.Application.PlayLists.Handlers; +using Musify.Application.Services; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Handlers; + +public sealed class UpdatePlayListCommandHandlerTests +{ + private readonly IEventBus _eventBus = Substitute.For(); + private readonly IStorageService _storage = Substitute.For(); + + private UpdatePlayListCommandHandler CreateSut(TestDatabase db) => + new( + _eventBus, + db, + new UploadIntentValidator(db, _storage), + NullLogger.Instance, + TestData.StorageConfig(), + TestData.PlayListConfig(), + TestData.UploadIntentConfig()); + + [Fact] + public async Task When_playlist_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.Handle( + new UpdatePlayListCommand(Guid.NewGuid(), Guid.NewGuid(), "n", null, null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task When_user_not_owner_returns_unauthorized() + { + await using var ctx = new SqliteTestContext(); + var playList = TestData.PlayList(Guid.NewGuid()); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new UpdatePlayListCommand(Guid.NewGuid(), playList.Id, "n", null, null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Unauthorized); + } + + [Fact] + public async Task Updates_name_and_description_without_publishing() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var playList = TestData.PlayList(userId, name: "Old"); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new UpdatePlayListCommand(userId, playList.Id, " New Name ", "new desc", null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Ok); + await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); + + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); + persisted.Name.Should().Be(" New Name "); + persisted.NormalizedName.Should().Be("NEW NAME"); + persisted.Description.Should().Be("new desc"); + } + + [Fact] + public async Task When_only_description_provided_name_is_unchanged() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var playList = TestData.PlayList(userId, name: "Keep"); + ctx.Database.PlayLists.Add(playList); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new UpdatePlayListCommand(userId, playList.Id, null, "only desc", null), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Ok); + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); + persisted.Name.Should().Be("Keep"); + persisted.Description.Should().Be("only desc"); + } + + [Fact] + public async Task With_valid_new_picture_intent_publishes_source_event_and_updates_picture_name() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var playList = TestData.PlayList(userId); + var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, objectName: "newcover.webp"); + ctx.Database.PlayLists.Add(playList); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) + .Returns(new ObjectMetaData("image/webp", 500)); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Ok); + await _eventBus.Received(1).PublishAsync( + Arg.Is(e => e.PlayListId == playList.Id && e.PictureIntentId == intent.Id), + Arg.Any()); + + var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); + persisted.OriginalPictureName.Should().Be("newcover.webp"); + } + + [Fact] + public async Task With_invalid_new_picture_intent_propagates_error() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var playList = TestData.PlayList(userId); + var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, status: UploadIntentStatus.Consumed); + ctx.Database.PlayLists.Add(playList); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.Handle( + new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Conflict); + } +} diff --git a/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs b/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs new file mode 100644 index 0000000..5544404 --- /dev/null +++ b/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Musify.Application.Tests.Infrastructure; + +public sealed class SchemaSmokeTests +{ + [Fact] + public async Task Schema_creates_and_persists_all_core_entities() + { + await using var ctx = new SqliteTestContext(); + + var user = TestData.User(); + ctx.Database.Users.Add(user); + ctx.Database.UploadIntents.Add(TestData.UploadIntent(user.Id)); + ctx.Database.PlayLists.Add(TestData.PlayList(user.Id)); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var verify = ctx.NewContext(); + (await verify.Users.CountAsync()).Should().Be(1); + (await verify.UploadIntents.CountAsync()).Should().Be(1); + (await verify.PlayLists.CountAsync()).Should().Be(1); + } + + [Fact] + public async Task Transactions_are_supported_under_sqlite() + { + await using var ctx = new SqliteTestContext(); + var user = TestData.User(); + + await using (var tx = await ctx.Database.BeginTransactionAsync( + System.Data.IsolationLevel.Serializable, CancellationToken.None)) + { + ctx.Database.Users.Add(user); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + await tx.CommitAsync(CancellationToken.None); + } + + (await ctx.NewContext().Users.CountAsync()).Should().Be(1); + } +} diff --git a/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs b/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs new file mode 100644 index 0000000..ced7b5b --- /dev/null +++ b/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs @@ -0,0 +1,44 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Musify.Application.Tests.Infrastructure; + +public sealed class SqliteTestContext : IAsyncDisposable, IDisposable +{ + private readonly SqliteConnection _connection; + + public TestDatabase Database { get; } + + public SqliteTestContext() + { + _connection = new SqliteConnection("DataSource=:memory:;Foreign Keys=False"); + _connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + + Database = new TestDatabase(options); + Database.Database.EnsureCreated(); + } + + public TestDatabase NewContext() + { + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + return new TestDatabase(options); + } + + public void Dispose() + { + Database.Dispose(); + _connection.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await Database.DisposeAsync(); + await _connection.DisposeAsync(); + } +} diff --git a/Tests/Musify.Application.Tests/Infrastructure/TestData.cs b/Tests/Musify.Application.Tests/Infrastructure/TestData.cs new file mode 100644 index 0000000..2e8ef62 --- /dev/null +++ b/Tests/Musify.Application.Tests/Infrastructure/TestData.cs @@ -0,0 +1,75 @@ +using Musify.Application.Configuration; +using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; + +namespace Musify.Application.Tests.Infrastructure; + +public static class TestData +{ + public const string Bucket = "musify-test"; + + public static ApplicationStorageConfiguration StorageConfig() => new() { Bucket = Bucket }; + + public static UploadIntentConfiguration UploadIntentConfig() => new(); + + public static TrackConfiguration TrackConfig() => new(); + + public static PlayListConfiguration PlayListConfig() => new(); + + public static User User(Guid? id = null, string name = "Alice") => new() + { + Id = id ?? Guid.NewGuid(), + Name = name, + NormalizedName = name.ToUpperInvariant(), + }; + + public static UploadIntent UploadIntent( + Guid userId, + UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture, + UploadIntentStatus status = UploadIntentStatus.Issued, + DateTime? expiresAt = null, + long? expectedSizeBytes = null, + string objectName = "object.webp", + string? key = null) => new() + { + UserId = userId, + Bucket = Bucket, + Key = key ?? $"temp/{userId}/Tracks/{objectName}", + ObjectName = objectName, + ContentType = "image/webp", + ExpectedSizeBytes = expectedSizeBytes, + Purpose = purpose, + Status = status, + ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5), + }; + + public static Track Track( + string title = "Song", + ProcessingStatus audioStatus = ProcessingStatus.Completed, + LifeCycleStatus lifeCycle = LifeCycleStatus.Active) => new() + { + Title = title, + NormalizedTitle = title.ToUpperInvariant(), + OriginalPictureName = "pic.webp", + OriginalAudioName = "audio.mp3", + AudioTranscodeProcessingStatus = audioStatus, + PicturesProcessingStatus = ProcessingStatus.Completed, + LifeCycleStatus = lifeCycle, + }; + + public static PlayList PlayList( + Guid userId, + string name = "My PlayList", + LifeCycleStatus lifeCycle = LifeCycleStatus.Active) => new() + { + UserId = userId, + Name = name, + NormalizedName = name.ToUpperInvariant(), + Description = "desc", + OriginalPictureName = "orig.webp", + SmallPictureName = "small.webp", + MediumPictureName = "medium.webp", + LargePictureName = "large.webp", + LifeCycleStatus = lifeCycle, + }; +} diff --git a/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs b/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs new file mode 100644 index 0000000..0d70d13 --- /dev/null +++ b/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Musify.Domain.Entities; +using AppIDatabase = Musify.Application.Contracts.IDatabase; +using IDatabaseTransaction = Musify.Application.Contracts.IDatabaseTransaction; + +namespace Musify.Application.Tests.Infrastructure; + +public sealed class TestDatabase(DbContextOptions options) : DbContext(options), AppIDatabase +{ + public DbSet Users => Set(); + + public DbSet Tracks => Set(); + + public DbSet PlayLists => Set(); + + public DbSet UserHasTracks => Set(); + + public DbSet PlayListHasTracks => Set(); + + public DbSet Uploads => Set(); + + public DbSet UploadIntents => Set(); + + public async Task BeginTransactionAsync( + System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) + { + var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); + return new TestDatabaseTransaction(transaction); + } + + private sealed class TestDatabaseTransaction(IDbContextTransaction inner) : IDatabaseTransaction + { + public Task CommitAsync(CancellationToken cancellationToken = default) => + inner.CommitAsync(cancellationToken); + + public ValueTask DisposeAsync() => inner.DisposeAsync(); + } +} diff --git a/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj b/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj new file mode 100644 index 0000000..67ef5a4 --- /dev/null +++ b/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + Musify.Application.Tests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs b/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs new file mode 100644 index 0000000..4cfc561 --- /dev/null +++ b/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using Musify.Application.Pagination; +using X.PagedList; +using Xunit; + +namespace Musify.Application.Tests.Pagination; + +public sealed class PaginatedResponseTests +{ + [Fact] + public void FromPagedList_maps_all_fields_for_a_middle_page() + { + var source = Enumerable.Range(1, 25).ToList(); + var paged = new PagedList(source, pageNumber: 2, pageSize: 10); + + var response = PaginatedResponse.FromPagedList(paged); + + response.PageNumber.Should().Be(2); + response.PageSize.Should().Be(10); + response.PageCount.Should().Be(3); + response.TotalItemCount.Should().Be(25); + response.HasNextPage.Should().BeTrue(); + response.HasPreviousPage.Should().BeTrue(); + response.Items.Should().Equal(11, 12, 13, 14, 15, 16, 17, 18, 19, 20); + } + + [Fact] + public void FromPagedList_first_page_has_no_previous_page() + { + var paged = new PagedList(Enumerable.Range(1, 5).ToList(), pageNumber: 1, pageSize: 10); + + var response = PaginatedResponse.FromPagedList(paged); + + response.HasPreviousPage.Should().BeFalse(); + response.HasNextPage.Should().BeFalse(); + response.TotalItemCount.Should().Be(5); + } +} diff --git a/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs b/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs new file mode 100644 index 0000000..791d888 --- /dev/null +++ b/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs @@ -0,0 +1,256 @@ +using Ardalis.Result; +using FluentAssertions; +using Musify.Application.Contracts; +using Musify.Application.Services; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Services; + +public sealed class UploadIntentValidatorTests +{ + private readonly IStorageService _storage = Substitute.For(); + + private UploadIntentValidator CreateSut(TestDatabase database) => new(database, _storage); + + [Fact] + public async Task CheckQuota_under_limits_returns_success() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + var sut = CreateSut(ctx.Database); + + var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1_000, requiredIntentCount: 1, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task CheckQuota_when_intent_count_would_exceed_max_returns_invalid() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadIntentsPerUser = 2; + + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1, requiredIntentCount: 1, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + } + + [Fact] + public async Task CheckQuota_exactly_at_intent_limit_is_allowed() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadIntentsPerUser = 2; + + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1, requiredIntentCount: 1, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task CheckQuota_when_byte_quota_exceeded_returns_invalid() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadBytesPerUser = 1_000; + + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 800)); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 300, requiredIntentCount: 1, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + } + + [Fact] + public async Task CheckQuota_uses_default_picture_size_for_intents_without_expected_size() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadBytesPerUser = 1_000; + config.DefaultExpectedPictureSizeBytes = 900; + + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: null)); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 200, requiredIntentCount: 1, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + } + + [Fact] + public async Task CheckQuota_ignores_other_users_and_non_issued_intents() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var otherUser = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + config.MaxActiveUploadIntentsPerUser = 2; + config.MaxActiveUploadBytesPerUser = 1_000; + + ctx.Database.UploadIntents.Add(TestData.UploadIntent(otherUser, expectedSizeBytes: 5_000)); + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, status: UploadIntentStatus.Consumed, expectedSizeBytes: 5_000)); + ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, status: UploadIntentStatus.Expired, expectedSizeBytes: 5_000)); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 100, requiredIntentCount: 1, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task ValidateAndLoad_when_intent_missing_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var sut = CreateSut(ctx.Database); + + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task ValidateAndLoad_when_intent_belongs_to_other_user_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var ownerId = Guid.NewGuid(); + var intent = TestData.UploadIntent(ownerId); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), intent.Id, Guid.NewGuid(), CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task ValidateAndLoad_when_consumed_returns_conflict() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Consumed); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Conflict); + } + + [Fact] + public async Task ValidateAndLoad_when_status_expired_returns_invalid() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Expired); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + } + + [Fact] + public async Task ValidateAndLoad_when_expired_by_timestamp_returns_invalid() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1)); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + } + + [Fact] + public async Task ValidateAndLoad_when_object_missing_in_storage_returns_not_found() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var intent = TestData.UploadIntent(userId); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) + .Returns((ObjectMetaData?)null); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.NotFound); + } + + [Fact] + public async Task ValidateAndLoad_when_object_exceeds_max_size_returns_invalid() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var config = TestData.UploadIntentConfig(); + config.MaxUploadBytes = 1_000; + var intent = TestData.UploadIntent(userId); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) + .Returns(new ObjectMetaData("image/webp", 2_000)); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync(config, intent.Id, userId, CancellationToken.None); + + result.Status.Should().Be(ResultStatus.Invalid); + } + + [Fact] + public async Task ValidateAndLoad_happy_path_returns_loaded_intent() + { + await using var ctx = new SqliteTestContext(); + var userId = Guid.NewGuid(); + var intent = TestData.UploadIntent(userId); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) + .Returns(new ObjectMetaData("image/webp", 500)); + + var sut = CreateSut(ctx.Database); + var result = await sut.ValidateAndLoadAsync( + TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.Id.Should().Be(intent.Id); + } +} diff --git a/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs b/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs new file mode 100644 index 0000000..2e5fe4a --- /dev/null +++ b/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Musify.Application.Contracts; +using Musify.Application.Tests.Infrastructure; + +namespace Musify.Infrastructure.Tests; + +internal static class JobTestHarness +{ + public static IServiceScopeFactory ScopeFactoryWith(Action configure) + { + var services = new ServiceCollection(); + configure(services); + return services.BuildServiceProvider().GetRequiredService(); + } + + public static IServiceScopeFactory WithDatabase(IDatabase database) => + ScopeFactoryWith(s => s.AddSingleton(database)); + + public static IServiceScopeFactory WithStorage(IStorageService storage) => + ScopeFactoryWith(s => s.AddSingleton(storage)); + + public static Task RunAsync(object job, CancellationToken cancellationToken = default) + { + var method = job.GetType().GetMethod( + "RunAsync", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + ?? throw new InvalidOperationException("RunAsync not found on job."); + + return (Task)method.Invoke(job, [cancellationToken])!; + } +} diff --git a/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj b/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj new file mode 100644 index 0000000..820228f --- /dev/null +++ b/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + Musify.Infrastructure.Tests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs b/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs new file mode 100644 index 0000000..2b255d3 --- /dev/null +++ b/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs @@ -0,0 +1,72 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Tests.Infrastructure; +using Musify.Infrastructure.Jobs; +using NSubstitute; +using Xunit; + +namespace Musify.Infrastructure.Tests; + +public sealed class TemporalUploadsCleanUpJobTests +{ + private readonly IStorageService _storage = Substitute.For(); + private readonly Application.Configuration.UploadIntentConfiguration _config = TestData.UploadIntentConfig(); + + private TemporalUploadsCleanUpJob CreateJob() => + new( + JobTestHarness.WithStorage(_storage), + NullLogger.Instance, + TestData.StorageConfig(), + _config); + + private static async IAsyncEnumerable AsAsync(params StorageObject[] items) + { + foreach (var item in items) + yield return item; + await Task.CompletedTask; + } + + [Fact] + public async Task Deletes_only_objects_older_than_retention_window() + { + var cutoffDays = _config.TempUploadsRetentionDays; + var stale = new StorageObject("temp/u1/old.webp", DateTime.UtcNow.AddDays(-(cutoffDays + 2))); + var fresh = new StorageObject("temp/u1/new.webp", DateTime.UtcNow.AddHours(-1)); + + _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) + .Returns(AsAsync(stale, fresh)); + + await JobTestHarness.RunAsync(CreateJob()); + + await _storage.Received(1).RemoveFileAsync(TestData.Bucket, stale.Key, Arg.Any()); + await _storage.DidNotReceive().RemoveFileAsync(TestData.Bucket, fresh.Key, Arg.Any()); + } + + [Fact] + public async Task Continues_when_a_delete_fails() + { + var first = new StorageObject("temp/u1/a.webp", DateTime.UtcNow.AddDays(-10)); + var second = new StorageObject("temp/u1/b.webp", DateTime.UtcNow.AddDays(-10)); + + _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) + .Returns(AsAsync(first, second)); + _storage.RemoveFileAsync(TestData.Bucket, first.Key, Arg.Any()) + .Returns(_ => throw new InvalidOperationException("s3 down")); + + await JobTestHarness.RunAsync(CreateJob()); + + await _storage.Received(1).RemoveFileAsync(TestData.Bucket, second.Key, Arg.Any()); + } + + [Fact] + public async Task Does_nothing_when_no_objects_listed() + { + _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) + .Returns(AsAsync()); + + await JobTestHarness.RunAsync(CreateJob()); + + await _storage.DidNotReceive().RemoveFileAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } +} diff --git a/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs b/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs new file mode 100644 index 0000000..cca62fb --- /dev/null +++ b/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs @@ -0,0 +1,84 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.Jobs; +using Xunit; + +namespace Musify.Infrastructure.Tests; + +public sealed class UploadIntentExpirationJobTests +{ + private static UploadIntentExpirationJob CreateJob(SqliteTestContext ctx, Action? configure = null) + { + var config = TestData.UploadIntentConfig(); + configure?.Invoke(config); + return new UploadIntentExpirationJob( + JobTestHarness.WithDatabase(ctx.Database), + NullLogger.Instance, + config); + } + + [Fact] + public async Task Marks_issued_intents_past_expiry_as_expired() + { + await using var ctx = new SqliteTestContext(); + var expired = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-5)); + var active = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(30)); + ctx.Database.UploadIntents.AddRange(expired, active); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + await JobTestHarness.RunAsync(CreateJob(ctx)); + + var verify = ctx.NewContext(); + (await verify.UploadIntents.SingleAsync(i => i.Id == expired.Id)).Status.Should().Be(UploadIntentStatus.Expired); + (await verify.UploadIntents.SingleAsync(i => i.Id == active.Id)).Status.Should().Be(UploadIntentStatus.Issued); + } + + [Fact] + public async Task Deletes_expired_intents_past_retention_window() + { + await using var ctx = new SqliteTestContext(); + var config = TestData.UploadIntentConfig(); + var old = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired, + expiresAt: DateTime.UtcNow.AddDays(-(config.ExpiredIntentsRetentionDays + 1))); + var recent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired, + expiresAt: DateTime.UtcNow.AddHours(-1)); + ctx.Database.UploadIntents.AddRange(old, recent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + await JobTestHarness.RunAsync(CreateJob(ctx)); + + var verify = ctx.NewContext(); + (await verify.UploadIntents.AnyAsync(i => i.Id == old.Id)).Should().BeFalse(); + (await verify.UploadIntents.AnyAsync(i => i.Id == recent.Id)).Should().BeTrue(); + } + + [Fact] + public async Task With_zero_retention_just_expired_intents_are_marked_then_deleted() + { + await using var ctx = new SqliteTestContext(); + var intent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1)); + ctx.Database.UploadIntents.Add(intent); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + await JobTestHarness.RunAsync(CreateJob(ctx, c => c.ExpiredIntentsRetentionDays = 0)); + + (await ctx.NewContext().UploadIntents.AnyAsync(i => i.Id == intent.Id)).Should().BeFalse(); + } + + [Fact] + public async Task Leaves_consumed_intents_untouched() + { + await using var ctx = new SqliteTestContext(); + var consumed = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Consumed, expiresAt: DateTime.UtcNow.AddDays(-10)); + ctx.Database.UploadIntents.Add(consumed); + await ctx.Database.SaveChangesAsync(CancellationToken.None); + + await JobTestHarness.RunAsync(CreateJob(ctx)); + + (await ctx.NewContext().UploadIntents.SingleAsync(i => i.Id == consumed.Id)).Status + .Should().Be(UploadIntentStatus.Consumed); + } +} From acec0977735a294e526dff1d1009f7f9a3302c3d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 15:44:20 +0200 Subject: [PATCH 153/172] Remove tests --- MusifyBackend.slnx | 4 - ...ify.Infrastructure.IntegrationTests.csproj | 28 ++++ .../SeaweedFsFixture.cs | 67 ++++++++ .../StorageServiceTests.cs | 145 ++++++++++++++++++ 4 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj create mode 100644 Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs create mode 100644 Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs diff --git a/MusifyBackend.slnx b/MusifyBackend.slnx index 386f50d..c92b66b 100644 --- a/MusifyBackend.slnx +++ b/MusifyBackend.slnx @@ -1,8 +1,4 @@ - - - - diff --git a/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj b/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj new file mode 100644 index 0000000..beb1cda --- /dev/null +++ b/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + Musify.Infrastructure.IntegrationTests + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs b/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs new file mode 100644 index 0000000..fcca425 --- /dev/null +++ b/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs @@ -0,0 +1,67 @@ +using Amazon.S3; +using Amazon.S3.Model; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Musify.Infrastructure.Configuration; +using Xunit; + +namespace Musify.Infrastructure.IntegrationTests; + +public sealed class SeaweedFsFixture : IAsyncLifetime +{ + private const int S3Port = 8333; + private const string AccessKey = "admin_access_key"; + private const string SecretKey = "admin_secret_key"; + + public const string Bucket = "webapi-storage"; + + private readonly IContainer _container = new ContainerBuilder() + .WithImage("chrislusf/seaweedfs:latest") + .WithCommand("server", "-s3", "-dir=/data", "-ip.bind=0.0.0.0") + .WithPortBinding(S3Port, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Start Seaweed S3 API Server")) + .Build(); + + public string ServiceUrl { get; private set; } = string.Empty; + + public IAmazonS3 CreateClient() + { + var config = new AmazonS3Config + { + ServiceURL = ServiceUrl, + ForcePathStyle = true, + UseHttp = true, + AuthenticationRegion = "us-east-1", + }; + return new AmazonS3Client(AccessKey, SecretKey, config); + } + + public InfrastructureStorageConfiguration StorageConfig() => new() + { + Address = ServiceUrl, + AccessKey = AccessKey, + SecretAccessKey = SecretKey, + ForcePathStyle = true, + UseHttp = true, + }; + + public async Task InitializeAsync() + { + await _container.StartAsync(); + ServiceUrl = $"http://{_container.Hostname}:{_container.GetMappedPublicPort(S3Port)}"; + + using var client = CreateClient(); + await client.PutBucketAsync(new PutBucketRequest { BucketName = Bucket }); + } + + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } +} + +[CollectionDefinition(SeaweedFsCollection.Name)] +public sealed class SeaweedFsCollection : ICollectionFixture +{ + public const string Name = "SeaweedFS"; +} diff --git a/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs b/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs new file mode 100644 index 0000000..e3bbbbb --- /dev/null +++ b/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs @@ -0,0 +1,145 @@ +using System.Text; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Musify.Application.Contracts; +using Musify.Application.Tests.Infrastructure; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.Services; +using Xunit; + +namespace Musify.Infrastructure.IntegrationTests; + +[Collection(SeaweedFsCollection.Name)] +public sealed class StorageServiceTests(SeaweedFsFixture seaweedFs) +{ + private const string Bucket = SeaweedFsFixture.Bucket; + + private (StorageService service, SqliteTestContext db) CreateService() + { + var db = new SqliteTestContext(); + var service = new StorageService( + db.Database, + seaweedFs.CreateClient(), + NullLogger.Instance, + seaweedFs.StorageConfig()); + return (service, db); + } + + private static MemoryStream TextStream(string content) => new(Encoding.UTF8.GetBytes(content)); + + [Fact] + public async Task Upload_then_head_and_get_roundtrips_content() + { + var (service, db) = CreateService(); + using var _ = db; + var key = $"tests/{Guid.NewGuid()}.txt"; + + await service.UploadFileAsync(TextStream("hello seaweed"), "text/plain", Bucket, key, CancellationToken.None); + + var metadata = await service.HeadObjectAsync(Bucket, key, CancellationToken.None); + metadata.Should().NotBeNull(); + metadata!.ContentLength.Should().Be("hello seaweed".Length); + + using var stream = await service.GetFileAsync(Bucket, key, CancellationToken.None); + using var reader = new StreamReader(stream); + (await reader.ReadToEndAsync()).Should().Be("hello seaweed"); + } + + [Fact] + public async Task Upload_persists_a_successful_upload_record() + { + var (service, db) = CreateService(); + using var _ = db; + var key = $"tests/{Guid.NewGuid()}.txt"; + + await service.UploadFileAsync(TextStream("data"), "text/plain", Bucket, key, CancellationToken.None); + + var upload = db.NewContext().Uploads.Single(u => u.Key == key); + upload.State.Should().Be(UploadState.Successful); + upload.Bucket.Should().Be(Bucket); + } + + [Fact] + public async Task HeadObject_returns_null_for_missing_object() + { + var (service, db) = CreateService(); + using var _ = db; + + var metadata = await service.HeadObjectAsync(Bucket, $"tests/missing-{Guid.NewGuid()}", CancellationToken.None); + + metadata.Should().BeNull(); + } + + [Fact] + public async Task CopyFile_duplicates_the_object() + { + var (service, db) = CreateService(); + using var _ = db; + var source = $"tests/{Guid.NewGuid()}-src.txt"; + var destination = $"tests/{Guid.NewGuid()}-dst.txt"; + await service.UploadFileAsync(TextStream("copy me"), "text/plain", Bucket, source, CancellationToken.None); + + await service.CopyFileAsync(Bucket, source, Bucket, destination, CancellationToken.None); + + (await service.HeadObjectAsync(Bucket, destination, CancellationToken.None)).Should().NotBeNull(); + (await service.HeadObjectAsync(Bucket, source, CancellationToken.None)).Should().NotBeNull(); + } + + [Fact] + public async Task RemoveFile_deletes_the_object() + { + var (service, db) = CreateService(); + using var _ = db; + var key = $"tests/{Guid.NewGuid()}.txt"; + await service.UploadFileAsync(TextStream("delete me"), "text/plain", Bucket, key, CancellationToken.None); + + await service.RemoveFileAsync(Bucket, key, CancellationToken.None); + + (await service.HeadObjectAsync(Bucket, key, CancellationToken.None)).Should().BeNull(); + } + + [Fact] + public async Task ListObjects_returns_objects_under_prefix() + { + var (service, db) = CreateService(); + using var _ = db; + var prefix = $"listing/{Guid.NewGuid()}"; + await service.UploadFileAsync(TextStream("a"), "text/plain", Bucket, $"{prefix}/a.txt", CancellationToken.None); + await service.UploadFileAsync(TextStream("b"), "text/plain", Bucket, $"{prefix}/b.txt", CancellationToken.None); + + var keys = new List(); + await foreach (var obj in service.ListObjectsAsync(Bucket, prefix, CancellationToken.None)) + keys.Add(obj.Key); + + keys.Should().Contain($"{prefix}/a.txt").And.Contain($"{prefix}/b.txt"); + } + + [Fact] + public async Task RemoveFolder_deletes_every_object_under_prefix() + { + var (service, db) = CreateService(); + using var _ = db; + var prefix = $"folder/{Guid.NewGuid()}"; + await service.UploadFileAsync(TextStream("a"), "text/plain", Bucket, $"{prefix}/a.txt", CancellationToken.None); + await service.UploadFileAsync(TextStream("b"), "text/plain", Bucket, $"{prefix}/nested/b.txt", CancellationToken.None); + + await service.RemoveFolderAsync(Bucket, prefix, CancellationToken.None); + + var remaining = new List(); + await foreach (var obj in service.ListObjectsAsync(Bucket, prefix, CancellationToken.None)) + remaining.Add(obj); + remaining.Should().BeEmpty(); + } + + [Fact] + public async Task GetUploadUrl_returns_a_presigned_url() + { + var (service, db) = CreateService(); + using var _ = db; + + var url = await service.GetUploadUrlAsync( + Bucket, $"tests/{Guid.NewGuid()}.txt", "text/plain", TimeSpan.FromMinutes(5), CancellationToken.None); + + url.Should().StartWith(seaweedFs.ServiceUrl); + } +} From 098f97473d27f247051e01f8b5a8a667b8a6809f Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 15:45:31 +0200 Subject: [PATCH 154/172] Remove tests --- .../Configuration/RoutesTests.cs | 86 ------ .../Domain/TrackTests.cs | 89 ------ .../Extensions/ResultExtensionsTests.cs | 74 ----- .../CreatePlayListCommandHandlerTests.cs | 105 ------- .../CreateTrackCommandHandlerTests.cs | 141 ---------- .../Handlers/CreateUserCommandHandlerTests.cs | 44 --- .../DeletePlayListCommandHandlerTests.cs | 88 ------ .../DeleteTrackCommandHandlerTests.cs | 108 -------- .../Handlers/GetByIdQueryHandlerTests.cs | 93 ------- .../GetTracksByUserIdQueryHandlerTests.cs | 83 ------ .../Handlers/ListQueryHandlerTests.cs | 135 --------- ...layListPictureUploadCommandHandlerTests.cs | 129 --------- ...questTrackUploadUrlsCommandHandlerTests.cs | 134 --------- .../UpdatePlayListCommandHandlerTests.cs | 144 ---------- .../Infrastructure/SchemaSmokeTests.cs | 42 --- .../Infrastructure/SqliteTestContext.cs | 44 --- .../Infrastructure/TestData.cs | 75 ----- .../Infrastructure/TestDatabase.cs | 39 --- .../Musify.Application.Tests.csproj | 29 -- .../Pagination/PaginatedResponseTests.cs | 38 --- .../Services/UploadIntentValidatorTests.cs | 256 ------------------ ...ify.Infrastructure.IntegrationTests.csproj | 28 -- .../SeaweedFsFixture.cs | 67 ----- .../StorageServiceTests.cs | 145 ---------- .../JobTestHarness.cs | 32 --- .../Musify.Infrastructure.Tests.csproj | 29 -- .../TemporalUploadsCleanUpJobTests.cs | 72 ----- .../UploadIntentExpirationJobTests.cs | 84 ------ 28 files changed, 2433 deletions(-) delete mode 100644 Tests/Musify.Application.Tests/Configuration/RoutesTests.cs delete mode 100644 Tests/Musify.Application.Tests/Domain/TrackTests.cs delete mode 100644 Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/TestData.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs delete mode 100644 Tests/Musify.Application.Tests/Musify.Application.Tests.csproj delete mode 100644 Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs delete mode 100644 Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs delete mode 100644 Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj delete mode 100644 Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs delete mode 100644 Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs delete mode 100644 Tests/Musify.Infrastructure.Tests/JobTestHarness.cs delete mode 100644 Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj delete mode 100644 Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs delete mode 100644 Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs diff --git a/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs b/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs deleted file mode 100644 index a0c0cbf..0000000 --- a/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using FluentAssertions; -using Musify.Application.Configuration; -using Xunit; - -namespace Musify.Application.Tests.Configuration; - -public sealed class RoutesTests -{ - private static readonly Guid UserId = Guid.Parse("11111111-1111-1111-1111-111111111111"); - - [Fact] - public void TrackRoutes_default_picture_and_audio_paths() - { - var routes = new TrackRoutes(); - - routes.OriginalPicturesPath.Should().Be("Tracks/OriginalPictures"); - routes.OriginalAudiosPath.Should().Be("Tracks/OriginalAudios"); - routes.ProcessedAudiosPath.Should().Be("Tracks/ProcessedAudios"); - } - - [Fact] - public void TrackRoutes_BuildOriginalPicturePath_with_user_nests_under_uploads() - { - var routes = new TrackRoutes(); - - routes.BuildOriginalPicturePath(UserId, "cover.webp") - .Should().Be($"uploads/{UserId}/Tracks/OriginalPictures/cover.webp"); - } - - [Fact] - public void TrackRoutes_BuildOriginalAudioPath_with_user_nests_under_uploads() - { - var routes = new TrackRoutes(); - - routes.BuildOriginalAudioPath(UserId, "song.mp3") - .Should().Be($"uploads/{UserId}/Tracks/OriginalAudios/song.mp3"); - } - - [Fact] - public void TrackRoutes_BuildTempPicturePath_uses_prefix_user_and_parent_folder() - { - var routes = new TrackRoutes(); - - routes.BuildTempPicturePath("temp", UserId, "obj.png") - .Should().Be($"temp/{UserId}/Tracks/obj.png"); - } - - [Fact] - public void CombineKey_trims_slashes_and_whitespace_per_segment() - { - var routes = new TrackRoutes(); - - var result = routes.BuildTempPicturePath(" /temp/ ", UserId, "/obj.png\\"); - - result.Should().Be($"temp/{UserId}/Tracks/obj.png"); - } - - [Fact] - public void CombineKey_drops_empty_and_whitespace_segments() - { - var routes = new TrackRoutes(); - - var result = routes.BuildTempPicturePath(" ", UserId, "obj.png"); - - result.Should().Be($"{UserId}/Tracks/obj.png"); - } - - [Fact] - public void PlayListRoutes_BuildOriginalPicturePath_with_user_nests_under_uploads() - { - var routes = new PlayListRoutes(); - - routes.BuildOriginalPicturePath(UserId, "cover.webp") - .Should().Be($"uploads/{UserId}/PlayLists/OriginalPictures/cover.webp"); - } - - [Fact] - public void PlayListRoutes_size_paths_use_parent_folder() - { - var routes = new PlayListRoutes(); - - routes.SmallPicturesPath.Should().Be("PlayLists/SmallPictures"); - routes.MediumPicturesPath.Should().Be("PlayLists/MediumPictures"); - routes.LargePicturesPath.Should().Be("PlayLists/LargePictures"); - } -} diff --git a/Tests/Musify.Application.Tests/Domain/TrackTests.cs b/Tests/Musify.Application.Tests/Domain/TrackTests.cs deleted file mode 100644 index f56505d..0000000 --- a/Tests/Musify.Application.Tests/Domain/TrackTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using FluentAssertions; -using Musify.Domain.ValueObjects; -using Musify.Application.Tests.Infrastructure; -using Xunit; - -namespace Musify.Application.Tests.Domain; - -public sealed class TrackTests -{ - [Fact] - public void IsPicturesProcessed_is_true_only_when_completed() - { - var track = TestData.Track(); - - track.PicturesProcessingStatus = ProcessingStatus.Completed; - track.IsPicturesProcessed.Should().BeTrue(); - - track.PicturesProcessingStatus = ProcessingStatus.Processing; - track.IsPicturesProcessed.Should().BeFalse(); - - track.PicturesProcessingStatus = ProcessingStatus.Failed; - track.IsPicturesProcessed.Should().BeFalse(); - } - - [Fact] - public void IsAudioProcessed_is_true_only_when_completed() - { - var track = TestData.Track(); - - track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; - track.IsAudioProcessed.Should().BeTrue(); - - track.AudioTranscodeProcessingStatus = ProcessingStatus.Pending; - track.IsAudioProcessed.Should().BeFalse(); - } - - [Fact] - public void Defaults_are_active_pending_and_zero_retries() - { - var track = new Musify.Domain.Entities.Track - { - Title = "t", - NormalizedTitle = "T", - OriginalPictureName = "p.webp", - OriginalAudioName = "a.mp3", - }; - - track.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); - track.PicturesProcessingStatus.Should().Be(ProcessingStatus.Pending); - track.AudioTranscodeProcessingStatus.Should().Be(ProcessingStatus.Pending); - track.RetryCount.Should().Be(0); - track.Id.Should().NotBe(Guid.Empty); - } -} - -public sealed class EnumValueRegressionTests -{ - [Fact] - public void ProcessingStatus_values_are_stable() - { - ((int)ProcessingStatus.Pending).Should().Be(0); - ((int)ProcessingStatus.Processing).Should().Be(1); - ((int)ProcessingStatus.Completed).Should().Be(2); - ((int)ProcessingStatus.Failed).Should().Be(3); - } - - [Fact] - public void LifeCycleStatus_values_are_stable() - { - ((int)LifeCycleStatus.Active).Should().Be(0); - ((int)LifeCycleStatus.Removing).Should().Be(1); - } - - [Fact] - public void UploadIntentStatus_values_are_stable() - { - ((int)UploadIntentStatus.Issued).Should().Be(0); - ((int)UploadIntentStatus.Consumed).Should().Be(1); - ((int)UploadIntentStatus.Expired).Should().Be(2); - } - - [Fact] - public void UploadIntentPurpose_values_are_stable() - { - ((int)UploadIntentPurpose.PlayListPicture).Should().Be(0); - ((int)UploadIntentPurpose.TrackPicture).Should().Be(1); - ((int)UploadIntentPurpose.TrackAudio).Should().Be(2); - } -} diff --git a/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs b/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs deleted file mode 100644 index 72ef974..0000000 --- a/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Musify.Application.Extensions; -using Xunit; - -namespace Musify.Application.Tests.Extensions; - -public sealed class ResultExtensionsTests -{ - [Fact] - public void NotFound_maps_to_NotFound_and_preserves_errors() - { - var source = Result.NotFound("missing"); - - var mapped = source.As(); - - mapped.Status.Should().Be(ResultStatus.NotFound); - mapped.Errors.Should().Contain("missing"); - } - - [Fact] - public void Conflict_maps_to_Conflict() - { - Result.Conflict("dup").As().Status.Should().Be(ResultStatus.Conflict); - } - - [Fact] - public void Unauthorized_maps_to_Unauthorized() - { - Result.Unauthorized().As().Status.Should().Be(ResultStatus.Unauthorized); - } - - [Fact] - public void Forbidden_maps_to_Forbidden() - { - Result.Forbidden().As().Status.Should().Be(ResultStatus.Forbidden); - } - - [Fact] - public void Invalid_maps_to_Invalid_and_preserves_validation_errors() - { - var source = Result.Invalid(new ValidationError("field", "bad")); - - var mapped = source.As(); - - mapped.Status.Should().Be(ResultStatus.Invalid); - mapped.ValidationErrors.Should().ContainSingle(e => e.ErrorMessage == "bad"); - } - - [Fact] - public void Error_maps_to_Error() - { - Result.Error("boom").As().Status.Should().Be(ResultStatus.Error); - } - - [Theory] - [InlineData(ResultStatus.Ok)] - [InlineData(ResultStatus.Created)] - [InlineData(ResultStatus.NoContent)] - public void Successful_results_throw_when_converted(ResultStatus status) - { - Result source = status switch - { - ResultStatus.Ok => Result.Success(1), - ResultStatus.Created => Result.Created(1), - ResultStatus.NoContent => Result.NoContent(), - _ => throw new InvalidOperationException() - }; - - var act = () => source.As(); - - act.Should().Throw(); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs deleted file mode 100644 index 6f60132..0000000 --- a/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class CreatePlayListCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - private readonly IStorageService _storage = Substitute.For(); - - private CreatePlayListCommandHandler CreateSut(TestDatabase db) => - new( - _eventBus, - db, - new UploadIntentValidator(db, _storage), - NullLogger.Instance, - TestData.StorageConfig(), - TestData.PlayListConfig(), - TestData.UploadIntentConfig()); - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle( - new CreatePlayListCommand(Guid.NewGuid(), "List", "desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Without_picture_intent_uses_preset_and_does_not_publish() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreatePlayListCommand(user.Id, " Chill Vibes ", "desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id); - persisted.OriginalPictureName.Should().Be(TestData.PlayListConfig().Routes.PresetOriginalPicture); - persisted.NormalizedName.Should().Be("CHILL VIBES"); - } - - [Fact] - public async Task With_valid_picture_intent_publishes_event_with_intent_object_name() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - var intent = TestData.UploadIntent(user.Id, UploadIntentPurpose.PlayListPicture, objectName: "cover.webp"); - ctx.Database.Users.Add(user); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 500)); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreatePlayListCommand(user.Id, "List", "desc", intent.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.PictureIntentId == intent.Id), - Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id); - persisted.OriginalPictureName.Should().Be("cover.webp"); - } - - [Fact] - public async Task With_invalid_picture_intent_propagates_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreatePlayListCommand(user.Id, "List", "desc", Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - (await ctx.NewContext().PlayLists.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs deleted file mode 100644 index 640b45b..0000000 --- a/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Commands; -using Musify.Application.Tracks.Handlers; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class CreateTrackCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - private readonly IStorageService _storage = Substitute.For(); - - private CreateTrackCommandHandler CreateSut(TestDatabase db) => - new( - db, - _eventBus, - new UploadIntentValidator(db, _storage), - NullLogger.Instance, - TestData.StorageConfig(), - TestData.TrackConfig(), - TestData.UploadIntentConfig()); - - private void StorageHasObject() => - _storage.HeadObjectAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(new ObjectMetaData("application/octet-stream", 500)); - - private static async Task<(UploadIntent picture, UploadIntent audio)> SeedIntentsAsync( - SqliteTestContext ctx, Guid userId) - { - var picture = TestData.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{userId}/pic.png"); - var audio = TestData.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", key: $"temp/{userId}/song.mp3"); - ctx.Database.UploadIntents.AddRange(picture, audio); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - return (picture, audio); - } - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle( - new CreateTrackCommand(Guid.NewGuid(), "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_picture_intent_invalid_propagates_and_creates_no_track() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - (await ctx.NewContext().Tracks.CountAsync()).Should().Be(0); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task When_audio_intent_invalid_propagates() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - var picture = TestData.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{user.Id}/pic.png"); - ctx.Database.Users.Add(user); - ctx.Database.UploadIntents.Add(picture); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - StorageHasObject(); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "Song", picture.Id, Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Happy_path_creates_track_publishes_event_and_normalizes_title() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - var (picture, audio) = await SeedIntentsAsync(ctx, user.Id); - StorageHasObject(); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => - e.PictureIntentId == picture.Id && e.AudioIntentId == audio.Id), - Arg.Any()); - - var persisted = await ctx.NewContext().Tracks.SingleAsync(); - persisted.NormalizedTitle.Should().Be("MY SONG"); - persisted.OriginalPictureName.Should().Be("pic.png"); - persisted.OriginalAudioName.Should().Be("song.mp3"); - } - - [Fact] - public async Task When_publish_throws_returns_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - var (picture, audio) = await SeedIntentsAsync(ctx, user.Id); - StorageHasObject(); - - _eventBus.PublishAsync(Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("bus down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - (await ctx.NewContext().Tracks.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs deleted file mode 100644 index 789a6dc..0000000 --- a/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Users.Commands; -using Musify.Application.Users.Handlers; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class CreateUserCommandHandlerTests -{ - [Fact] - public async Task When_user_already_exists_returns_conflict() - { - await using var ctx = new SqliteTestContext(); - var existing = TestData.User(); - ctx.Database.Users.Add(existing); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new CreateUserCommandHandler(ctx.Database, NullLogger.Instance); - var result = await sut.Handle(new CreateUserCommand(existing.Id, "Bob", null, null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - } - - [Fact] - public async Task Happy_path_creates_user_and_persists() - { - await using var ctx = new SqliteTestContext(); - var id = Guid.NewGuid(); - - var sut = new CreateUserCommandHandler(ctx.Database, NullLogger.Instance); - var result = await sut.Handle(new CreateUserCommand(id, "Bob", "Robert", "Smith"), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - result.Value.Name.Should().Be("Bob"); - - var persisted = await ctx.NewContext().Users.SingleAsync(u => u.Id == id); - persisted.NormalizedName.Should().Be("BOB"); - persisted.FirstName.Should().Be("Robert"); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs deleted file mode 100644 index 9b69142..0000000 --- a/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class DeletePlayListCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - - private DeletePlayListCommandHandler CreateSut(TestDatabase db) => - new(db, _eventBus, NullLogger.Instance); - - [Fact] - public async Task When_playlist_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_user_not_owner_returns_unauthorized() - { - await using var ctx = new SqliteTestContext(); - var playList = TestData.PlayList(Guid.NewGuid()); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), playList.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Unauthorized); - } - - [Fact] - public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NoContent); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.PlayListId == playList.Id && e.UserId == userId), - Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing); - } - - [Fact] - public async Task When_publish_throws_returns_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _eventBus.PublishAsync(Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("bus down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs deleted file mode 100644 index e81a555..0000000 --- a/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Commands; -using Musify.Application.Tracks.Handlers; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class DeleteTrackCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - - private DeleteTrackCommandHandler CreateSut(TestDatabase db) => - new(db, _eventBus, NullLogger.Instance); - - private static async Task<(Guid trackId, Guid userId)> SeedOwnedTrackAsync( - SqliteTestContext ctx, ProcessingStatus audioStatus) - { - var userId = Guid.NewGuid(); - var track = TestData.Track(audioStatus: audioStatus); - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id }); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - return (track.Id, userId); - } - - [Fact] - public async Task When_track_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_user_not_owner_returns_unauthorized() - { - await using var ctx = new SqliteTestContext(); - var track = TestData.Track(); - ctx.Database.Tracks.Add(track); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), track.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Unauthorized); - } - - [Theory] - [InlineData(ProcessingStatus.Pending)] - [InlineData(ProcessingStatus.Processing)] - public async Task When_audio_still_processing_returns_conflict(ProcessingStatus status) - { - await using var ctx = new SqliteTestContext(); - var (trackId, userId) = await SeedOwnedTrackAsync(ctx, status); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content() - { - await using var ctx = new SqliteTestContext(); - var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NoContent); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.TrackId == trackId), Arg.Any()); - - var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing); - } - - [Fact] - public async Task When_publish_throws_returns_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed); - - _eventBus.PublishAsync(Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("bus down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs deleted file mode 100644 index 7c29582..0000000 --- a/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.PlayLists.Queries; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Handlers; -using Musify.Application.Tracks.Queries; -using Musify.Application.Users.Handlers; -using Musify.Application.Users.Queries; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class GetByIdQueryHandlerTests -{ - [Fact] - public async Task GetTrackById_returns_track_when_found() - { - await using var ctx = new SqliteTestContext(); - var track = TestData.Track(title: "Found"); - ctx.Database.Tracks.Add(track); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetTrackByIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTrackByIdQuery(track.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Title.Should().Be("Found"); - } - - [Fact] - public async Task GetTrackById_returns_not_found_when_missing() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetTrackByIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetTrackByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task GetPlayListById_returns_playlist_when_found() - { - await using var ctx = new SqliteTestContext(); - var playList = TestData.PlayList(Guid.NewGuid(), name: "MyList"); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetPlayListByIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetPlayListByIdQuery(playList.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Name.Should().Be("MyList"); - } - - [Fact] - public async Task GetPlayListById_returns_not_found_when_missing() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetPlayListByIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetPlayListByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task GetUserById_returns_user_when_found() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(name: "Carol"); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetUserByIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetUserByIdQuery(user.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Name.Should().Be("Carol"); - } - - [Fact] - public async Task GetUserById_returns_not_found_when_missing() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetUserByIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetUserByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs deleted file mode 100644 index 0352e47..0000000 --- a/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using FluentAssertions; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Handlers; -using Musify.Application.Tracks.Queries; -using Musify.Domain.Entities; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class GetTracksByUserIdQueryHandlerTests -{ - private static async Task SeedTrackForUserAsync(SqliteTestContext ctx, Guid userId, string title, DateTime created) - { - var track = TestData.Track(title: title); - track.CreatedDate = created; - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id }); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - } - - [Fact] - public async Task Returns_only_tracks_owned_by_the_user_ordered_by_created_date() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var otherUser = Guid.NewGuid(); - await SeedTrackForUserAsync(ctx, userId, "Beta", new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc)); - await SeedTrackForUserAsync(ctx, userId, "Alpha", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - await SeedTrackForUserAsync(ctx, otherUser, "Gamma", new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc)); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksByUserIdQuery(userId), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(2); - result.Value.Items.Select(t => t.Title).Should().Equal("Alpha", "Beta"); - } - - [Fact] - public async Task Filters_by_normalized_name_contains() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - await SeedTrackForUserAsync(ctx, userId, "Summer Hit", DateTime.UtcNow); - await SeedTrackForUserAsync(ctx, userId, "Winter Song", DateTime.UtcNow); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksByUserIdQuery(userId, Name: "summer"), CancellationToken.None); - - result.Value.TotalItemCount.Should().Be(1); - result.Value.Items.Single().Title.Should().Be("Summer Hit"); - } - - [Fact] - public async Task Normalizes_invalid_paging_arguments_to_defaults() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - for (var i = 0; i < 3; i++) - await SeedTrackForUserAsync(ctx, userId, $"Track {i}", DateTime.UtcNow.AddSeconds(i)); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle( - new GetTracksByUserIdQuery(userId, PageNumber: 0, PageSize: -5), CancellationToken.None); - - result.Value.PageNumber.Should().Be(1); - result.Value.PageSize.Should().Be(10); - result.Value.TotalItemCount.Should().Be(3); - } - - [Fact] - public async Task Returns_empty_page_when_user_has_no_tracks() - { - await using var ctx = new SqliteTestContext(); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksByUserIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(0); - result.Value.Items.Should().BeEmpty(); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs deleted file mode 100644 index 326a9fe..0000000 --- a/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using FluentAssertions; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.PlayLists.Queries; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Handlers; -using Musify.Application.Tracks.Queries; -using Musify.Application.Users.Handlers; -using Musify.Application.Users.Queries; -using Musify.Domain.Entities; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class ListQueryHandlerTests -{ - [Fact] - public async Task GetTracks_paginates_across_all_users() - { - await using var ctx = new SqliteTestContext(); - for (var i = 0; i < 15; i++) - { - var track = TestData.Track(title: $"Track {i:D2}"); - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id }); - } - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetTracksQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksQuery(PageNumber: 2, PageSize: 10), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(15); - result.Value.PageNumber.Should().Be(2); - result.Value.Items.Should().HaveCount(5); - result.Value.HasPreviousPage.Should().BeTrue(); - result.Value.HasNextPage.Should().BeFalse(); - } - - [Fact] - public async Task GetTracks_normalizes_invalid_paging_to_defaults() - { - await using var ctx = new SqliteTestContext(); - var track = TestData.Track(); - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id }); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetTracksQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksQuery(PageNumber: 0, PageSize: -3), CancellationToken.None); - - result.Value.PageNumber.Should().Be(1); - result.Value.PageSize.Should().Be(10); - result.Value.TotalItemCount.Should().Be(1); - } - - [Fact] - public async Task GetPlayLists_returns_all_playlists_paginated() - { - await using var ctx = new SqliteTestContext(); - for (var i = 0; i < 3; i++) - ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: $"PL {i}")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetPlayListsQueryHandler(ctx.Database); - var result = await sut.Handle(new GetPlayListsQuery(), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(3); - result.Value.Items.Should().HaveCount(3); - } - - [Fact] - public async Task GetPlayListsByUserId_filters_by_user_and_name() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Summer Mix")); - ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Winter Mix")); - ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: "Summer Other")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database); - - var all = await sut.Handle(new GetPlayListsByUserIdQuery(userId), CancellationToken.None); - all.Value.TotalItemCount.Should().Be(2); - - var filtered = await sut.Handle(new GetPlayListsByUserIdQuery(userId, Name: "summer"), CancellationToken.None); - filtered.Value.TotalItemCount.Should().Be(1); - filtered.Value.Items.Single().Name.Should().Be("Summer Mix"); - } - - [Fact] - public async Task GetPlayListsByUserId_returns_empty_for_unknown_user() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetPlayListsByUserIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(0); - } - - [Fact] - public async Task GetUsers_filters_by_username_search_case_insensitively() - { - await using var ctx = new SqliteTestContext(); - ctx.Database.Users.Add(TestData.User(name: "Alice")); - ctx.Database.Users.Add(TestData.User(name: "Bob")); - ctx.Database.Users.Add(TestData.User(name: "Alicia")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetUsersQueryHandler(ctx.Database); - var result = await sut.Handle(new GetUsersQuery(1, 10, UsernameSearch: "ali"), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(2); - result.Value.Items.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Alicia"); - } - - [Fact] - public async Task GetUsers_normalizes_invalid_paging_to_defaults() - { - await using var ctx = new SqliteTestContext(); - ctx.Database.Users.Add(TestData.User(name: "Solo")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetUsersQueryHandler(ctx.Database); - var result = await sut.Handle(new GetUsersQuery(0, -1), CancellationToken.None); - - result.Value.PageNumber.Should().Be(1); - result.Value.PageSize.Should().Be(10); - result.Value.TotalItemCount.Should().Be(1); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs deleted file mode 100644 index 2268920..0000000 --- a/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Configuration; -using Musify.Application.Contracts; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class RequestPlayListPictureUploadCommandHandlerTests -{ - private readonly IStorageService _storage = Substitute.For(); - - private RequestPlayListPictureUploadCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) => - new( - db, - new UploadIntentValidator(db, _storage), - _storage, - NullLogger.Instance, - TestData.StorageConfig(), - TestData.PlayListConfig(), - config ?? TestData.UploadIntentConfig()); - - private static RequestPlayListPictureUploadCommand Command(Guid userId, long? size = null) => - new(userId, ".WEBP", "image/webp", size); - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Happy_path_creates_single_intent_and_returns_url() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.UploadUrl.Should().Be("https://signed.example/upload"); - - var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); - intents.Should().ContainSingle(); - intents[0].Purpose.Should().Be(UploadIntentPurpose.PlayListPicture); - intents[0].ObjectName.Should().EndWith(".webp"); - } - - [Fact] - public async Task When_quota_exceeded_returns_invalid_and_creates_no_intent() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 0; - - var sut = CreateSut(ctx.Database, config); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } - - [Fact] - public async Task Uses_default_picture_size_when_not_provided() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - var sut = CreateSut(ctx.Database, config); - await sut.Handle(Command(user.Id), CancellationToken.None); - - var intent = await ctx.NewContext().UploadIntents.SingleAsync(i => i.UserId == user.Id); - intent.ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes); - } - - [Fact] - public async Task When_storage_throws_returns_error() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("s3 down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs deleted file mode 100644 index c116dd4..0000000 --- a/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Configuration; -using Musify.Application.Contracts; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Commands; -using Musify.Application.Tracks.Handlers; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class RequestTrackUploadUrlsCommandHandlerTests -{ - private readonly IStorageService _storage = Substitute.For(); - - private RequestTrackUploadUrlsCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) => - new( - db, - new UploadIntentValidator(db, _storage), - _storage, - NullLogger.Instance, - TestData.StorageConfig(), - TestData.TrackConfig(), - config ?? TestData.UploadIntentConfig()); - - private static RequestTrackUploadUrlsCommand Command(Guid userId, long? pic = null, long? audio = null) => - new(userId, ".PNG", "image/png", "mp3", "audio/mpeg", pic, audio); - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Happy_path_creates_two_intents_and_returns_urls() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.PictureUploadUrl.Should().Be("https://signed.example/upload"); - - var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); - intents.Should().HaveCount(2); - intents.Select(i => i.Purpose).Should().BeEquivalentTo( - new[] { UploadIntentPurpose.TrackPicture, UploadIntentPurpose.TrackAudio }); - intents.Should().Contain(i => i.ObjectName.EndsWith(".png")); - intents.Should().Contain(i => i.ObjectName.EndsWith(".mp3")); - } - - [Fact] - public async Task When_quota_exceeded_returns_invalid_and_creates_no_intents() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 1; - - var sut = CreateSut(ctx.Database, config); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } - - [Fact] - public async Task Uses_default_sizes_when_not_provided() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - var sut = CreateSut(ctx.Database, config); - await sut.Handle(Command(user.Id), CancellationToken.None); - - var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); - intents.Single(i => i.Purpose == UploadIntentPurpose.TrackPicture) - .ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes); - intents.Single(i => i.Purpose == UploadIntentPurpose.TrackAudio) - .ExpectedSizeBytes.Should().Be(config.DefaultExpectedAudioSizeBytes); - } - - [Fact] - public async Task When_storage_throws_returns_error() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("s3 down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs deleted file mode 100644 index 52957bc..0000000 --- a/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class UpdatePlayListCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - private readonly IStorageService _storage = Substitute.For(); - - private UpdatePlayListCommandHandler CreateSut(TestDatabase db) => - new( - _eventBus, - db, - new UploadIntentValidator(db, _storage), - NullLogger.Instance, - TestData.StorageConfig(), - TestData.PlayListConfig(), - TestData.UploadIntentConfig()); - - [Fact] - public async Task When_playlist_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle( - new UpdatePlayListCommand(Guid.NewGuid(), Guid.NewGuid(), "n", null, null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_user_not_owner_returns_unauthorized() - { - await using var ctx = new SqliteTestContext(); - var playList = TestData.PlayList(Guid.NewGuid()); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(Guid.NewGuid(), playList.Id, "n", null, null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Unauthorized); - } - - [Fact] - public async Task Updates_name_and_description_without_publishing() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId, name: "Old"); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, " New Name ", "new desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Ok); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.Name.Should().Be(" New Name "); - persisted.NormalizedName.Should().Be("NEW NAME"); - persisted.Description.Should().Be("new desc"); - } - - [Fact] - public async Task When_only_description_provided_name_is_unchanged() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId, name: "Keep"); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, null, "only desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Ok); - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.Name.Should().Be("Keep"); - persisted.Description.Should().Be("only desc"); - } - - [Fact] - public async Task With_valid_new_picture_intent_publishes_source_event_and_updates_picture_name() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, objectName: "newcover.webp"); - ctx.Database.PlayLists.Add(playList); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 500)); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Ok); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.PlayListId == playList.Id && e.PictureIntentId == intent.Id), - Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.OriginalPictureName.Should().Be("newcover.webp"); - } - - [Fact] - public async Task With_invalid_new_picture_intent_propagates_error() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, status: UploadIntentStatus.Consumed); - ctx.Database.PlayLists.Add(playList); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - } -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs b/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs deleted file mode 100644 index 5544404..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace Musify.Application.Tests.Infrastructure; - -public sealed class SchemaSmokeTests -{ - [Fact] - public async Task Schema_creates_and_persists_all_core_entities() - { - await using var ctx = new SqliteTestContext(); - - var user = TestData.User(); - ctx.Database.Users.Add(user); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(user.Id)); - ctx.Database.PlayLists.Add(TestData.PlayList(user.Id)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var verify = ctx.NewContext(); - (await verify.Users.CountAsync()).Should().Be(1); - (await verify.UploadIntents.CountAsync()).Should().Be(1); - (await verify.PlayLists.CountAsync()).Should().Be(1); - } - - [Fact] - public async Task Transactions_are_supported_under_sqlite() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - - await using (var tx = await ctx.Database.BeginTransactionAsync( - System.Data.IsolationLevel.Serializable, CancellationToken.None)) - { - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - await tx.CommitAsync(CancellationToken.None); - } - - (await ctx.NewContext().Users.CountAsync()).Should().Be(1); - } -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs b/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs deleted file mode 100644 index ced7b5b..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; - -namespace Musify.Application.Tests.Infrastructure; - -public sealed class SqliteTestContext : IAsyncDisposable, IDisposable -{ - private readonly SqliteConnection _connection; - - public TestDatabase Database { get; } - - public SqliteTestContext() - { - _connection = new SqliteConnection("DataSource=:memory:;Foreign Keys=False"); - _connection.Open(); - - var options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - - Database = new TestDatabase(options); - Database.Database.EnsureCreated(); - } - - public TestDatabase NewContext() - { - var options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - return new TestDatabase(options); - } - - public void Dispose() - { - Database.Dispose(); - _connection.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await Database.DisposeAsync(); - await _connection.DisposeAsync(); - } -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/TestData.cs b/Tests/Musify.Application.Tests/Infrastructure/TestData.cs deleted file mode 100644 index 2e8ef62..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/TestData.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Musify.Application.Configuration; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; - -namespace Musify.Application.Tests.Infrastructure; - -public static class TestData -{ - public const string Bucket = "musify-test"; - - public static ApplicationStorageConfiguration StorageConfig() => new() { Bucket = Bucket }; - - public static UploadIntentConfiguration UploadIntentConfig() => new(); - - public static TrackConfiguration TrackConfig() => new(); - - public static PlayListConfiguration PlayListConfig() => new(); - - public static User User(Guid? id = null, string name = "Alice") => new() - { - Id = id ?? Guid.NewGuid(), - Name = name, - NormalizedName = name.ToUpperInvariant(), - }; - - public static UploadIntent UploadIntent( - Guid userId, - UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture, - UploadIntentStatus status = UploadIntentStatus.Issued, - DateTime? expiresAt = null, - long? expectedSizeBytes = null, - string objectName = "object.webp", - string? key = null) => new() - { - UserId = userId, - Bucket = Bucket, - Key = key ?? $"temp/{userId}/Tracks/{objectName}", - ObjectName = objectName, - ContentType = "image/webp", - ExpectedSizeBytes = expectedSizeBytes, - Purpose = purpose, - Status = status, - ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5), - }; - - public static Track Track( - string title = "Song", - ProcessingStatus audioStatus = ProcessingStatus.Completed, - LifeCycleStatus lifeCycle = LifeCycleStatus.Active) => new() - { - Title = title, - NormalizedTitle = title.ToUpperInvariant(), - OriginalPictureName = "pic.webp", - OriginalAudioName = "audio.mp3", - AudioTranscodeProcessingStatus = audioStatus, - PicturesProcessingStatus = ProcessingStatus.Completed, - LifeCycleStatus = lifeCycle, - }; - - public static PlayList PlayList( - Guid userId, - string name = "My PlayList", - LifeCycleStatus lifeCycle = LifeCycleStatus.Active) => new() - { - UserId = userId, - Name = name, - NormalizedName = name.ToUpperInvariant(), - Description = "desc", - OriginalPictureName = "orig.webp", - SmallPictureName = "small.webp", - MediumPictureName = "medium.webp", - LargePictureName = "large.webp", - LifeCycleStatus = lifeCycle, - }; -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs b/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs deleted file mode 100644 index 0d70d13..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using Musify.Domain.Entities; -using AppIDatabase = Musify.Application.Contracts.IDatabase; -using IDatabaseTransaction = Musify.Application.Contracts.IDatabaseTransaction; - -namespace Musify.Application.Tests.Infrastructure; - -public sealed class TestDatabase(DbContextOptions options) : DbContext(options), AppIDatabase -{ - public DbSet Users => Set(); - - public DbSet Tracks => Set(); - - public DbSet PlayLists => Set(); - - public DbSet UserHasTracks => Set(); - - public DbSet PlayListHasTracks => Set(); - - public DbSet Uploads => Set(); - - public DbSet UploadIntents => Set(); - - public async Task BeginTransactionAsync( - System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) - { - var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); - return new TestDatabaseTransaction(transaction); - } - - private sealed class TestDatabaseTransaction(IDbContextTransaction inner) : IDatabaseTransaction - { - public Task CommitAsync(CancellationToken cancellationToken = default) => - inner.CommitAsync(cancellationToken); - - public ValueTask DisposeAsync() => inner.DisposeAsync(); - } -} diff --git a/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj b/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj deleted file mode 100644 index 67ef5a4..0000000 --- a/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net10.0 - enable - enable - false - Musify.Application.Tests - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - diff --git a/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs b/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs deleted file mode 100644 index 4cfc561..0000000 --- a/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using FluentAssertions; -using Musify.Application.Pagination; -using X.PagedList; -using Xunit; - -namespace Musify.Application.Tests.Pagination; - -public sealed class PaginatedResponseTests -{ - [Fact] - public void FromPagedList_maps_all_fields_for_a_middle_page() - { - var source = Enumerable.Range(1, 25).ToList(); - var paged = new PagedList(source, pageNumber: 2, pageSize: 10); - - var response = PaginatedResponse.FromPagedList(paged); - - response.PageNumber.Should().Be(2); - response.PageSize.Should().Be(10); - response.PageCount.Should().Be(3); - response.TotalItemCount.Should().Be(25); - response.HasNextPage.Should().BeTrue(); - response.HasPreviousPage.Should().BeTrue(); - response.Items.Should().Equal(11, 12, 13, 14, 15, 16, 17, 18, 19, 20); - } - - [Fact] - public void FromPagedList_first_page_has_no_previous_page() - { - var paged = new PagedList(Enumerable.Range(1, 5).ToList(), pageNumber: 1, pageSize: 10); - - var response = PaginatedResponse.FromPagedList(paged); - - response.HasPreviousPage.Should().BeFalse(); - response.HasNextPage.Should().BeFalse(); - response.TotalItemCount.Should().Be(5); - } -} diff --git a/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs b/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs deleted file mode 100644 index 791d888..0000000 --- a/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Musify.Application.Contracts; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Services; - -public sealed class UploadIntentValidatorTests -{ - private readonly IStorageService _storage = Substitute.For(); - - private UploadIntentValidator CreateSut(TestDatabase database) => new(database, _storage); - - [Fact] - public async Task CheckQuota_under_limits_returns_success() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - var sut = CreateSut(ctx.Database); - - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1_000, requiredIntentCount: 1, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task CheckQuota_when_intent_count_would_exceed_max_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 2; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1, requiredIntentCount: 1, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task CheckQuota_exactly_at_intent_limit_is_allowed() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 2; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1, requiredIntentCount: 1, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task CheckQuota_when_byte_quota_exceeded_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadBytesPerUser = 1_000; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 800)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 300, requiredIntentCount: 1, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task CheckQuota_uses_default_picture_size_for_intents_without_expected_size() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadBytesPerUser = 1_000; - config.DefaultExpectedPictureSizeBytes = 900; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: null)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 200, requiredIntentCount: 1, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task CheckQuota_ignores_other_users_and_non_issued_intents() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var otherUser = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 2; - config.MaxActiveUploadBytesPerUser = 1_000; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(otherUser, expectedSizeBytes: 5_000)); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, status: UploadIntentStatus.Consumed, expectedSizeBytes: 5_000)); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, status: UploadIntentStatus.Expired, expectedSizeBytes: 5_000)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 100, requiredIntentCount: 1, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task ValidateAndLoad_when_intent_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task ValidateAndLoad_when_intent_belongs_to_other_user_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var ownerId = Guid.NewGuid(); - var intent = TestData.UploadIntent(ownerId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, Guid.NewGuid(), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task ValidateAndLoad_when_consumed_returns_conflict() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Consumed); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - } - - [Fact] - public async Task ValidateAndLoad_when_status_expired_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Expired); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task ValidateAndLoad_when_expired_by_timestamp_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1)); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task ValidateAndLoad_when_object_missing_in_storage_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns((ObjectMetaData?)null); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task ValidateAndLoad_when_object_exceeds_max_size_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxUploadBytes = 1_000; - var intent = TestData.UploadIntent(userId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 2_000)); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync(config, intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task ValidateAndLoad_happy_path_returns_loaded_intent() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 500)); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Id.Should().Be(intent.Id); - } -} diff --git a/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj b/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj deleted file mode 100644 index beb1cda..0000000 --- a/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net10.0 - enable - enable - false - Musify.Infrastructure.IntegrationTests - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs b/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs deleted file mode 100644 index fcca425..0000000 --- a/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Amazon.S3; -using Amazon.S3.Model; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; -using Musify.Infrastructure.Configuration; -using Xunit; - -namespace Musify.Infrastructure.IntegrationTests; - -public sealed class SeaweedFsFixture : IAsyncLifetime -{ - private const int S3Port = 8333; - private const string AccessKey = "admin_access_key"; - private const string SecretKey = "admin_secret_key"; - - public const string Bucket = "webapi-storage"; - - private readonly IContainer _container = new ContainerBuilder() - .WithImage("chrislusf/seaweedfs:latest") - .WithCommand("server", "-s3", "-dir=/data", "-ip.bind=0.0.0.0") - .WithPortBinding(S3Port, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Start Seaweed S3 API Server")) - .Build(); - - public string ServiceUrl { get; private set; } = string.Empty; - - public IAmazonS3 CreateClient() - { - var config = new AmazonS3Config - { - ServiceURL = ServiceUrl, - ForcePathStyle = true, - UseHttp = true, - AuthenticationRegion = "us-east-1", - }; - return new AmazonS3Client(AccessKey, SecretKey, config); - } - - public InfrastructureStorageConfiguration StorageConfig() => new() - { - Address = ServiceUrl, - AccessKey = AccessKey, - SecretAccessKey = SecretKey, - ForcePathStyle = true, - UseHttp = true, - }; - - public async Task InitializeAsync() - { - await _container.StartAsync(); - ServiceUrl = $"http://{_container.Hostname}:{_container.GetMappedPublicPort(S3Port)}"; - - using var client = CreateClient(); - await client.PutBucketAsync(new PutBucketRequest { BucketName = Bucket }); - } - - public async Task DisposeAsync() - { - await _container.DisposeAsync(); - } -} - -[CollectionDefinition(SeaweedFsCollection.Name)] -public sealed class SeaweedFsCollection : ICollectionFixture -{ - public const string Name = "SeaweedFS"; -} diff --git a/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs b/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs deleted file mode 100644 index e3bbbbb..0000000 --- a/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Text; -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using Musify.Infrastructure.Services; -using Xunit; - -namespace Musify.Infrastructure.IntegrationTests; - -[Collection(SeaweedFsCollection.Name)] -public sealed class StorageServiceTests(SeaweedFsFixture seaweedFs) -{ - private const string Bucket = SeaweedFsFixture.Bucket; - - private (StorageService service, SqliteTestContext db) CreateService() - { - var db = new SqliteTestContext(); - var service = new StorageService( - db.Database, - seaweedFs.CreateClient(), - NullLogger.Instance, - seaweedFs.StorageConfig()); - return (service, db); - } - - private static MemoryStream TextStream(string content) => new(Encoding.UTF8.GetBytes(content)); - - [Fact] - public async Task Upload_then_head_and_get_roundtrips_content() - { - var (service, db) = CreateService(); - using var _ = db; - var key = $"tests/{Guid.NewGuid()}.txt"; - - await service.UploadFileAsync(TextStream("hello seaweed"), "text/plain", Bucket, key, CancellationToken.None); - - var metadata = await service.HeadObjectAsync(Bucket, key, CancellationToken.None); - metadata.Should().NotBeNull(); - metadata!.ContentLength.Should().Be("hello seaweed".Length); - - using var stream = await service.GetFileAsync(Bucket, key, CancellationToken.None); - using var reader = new StreamReader(stream); - (await reader.ReadToEndAsync()).Should().Be("hello seaweed"); - } - - [Fact] - public async Task Upload_persists_a_successful_upload_record() - { - var (service, db) = CreateService(); - using var _ = db; - var key = $"tests/{Guid.NewGuid()}.txt"; - - await service.UploadFileAsync(TextStream("data"), "text/plain", Bucket, key, CancellationToken.None); - - var upload = db.NewContext().Uploads.Single(u => u.Key == key); - upload.State.Should().Be(UploadState.Successful); - upload.Bucket.Should().Be(Bucket); - } - - [Fact] - public async Task HeadObject_returns_null_for_missing_object() - { - var (service, db) = CreateService(); - using var _ = db; - - var metadata = await service.HeadObjectAsync(Bucket, $"tests/missing-{Guid.NewGuid()}", CancellationToken.None); - - metadata.Should().BeNull(); - } - - [Fact] - public async Task CopyFile_duplicates_the_object() - { - var (service, db) = CreateService(); - using var _ = db; - var source = $"tests/{Guid.NewGuid()}-src.txt"; - var destination = $"tests/{Guid.NewGuid()}-dst.txt"; - await service.UploadFileAsync(TextStream("copy me"), "text/plain", Bucket, source, CancellationToken.None); - - await service.CopyFileAsync(Bucket, source, Bucket, destination, CancellationToken.None); - - (await service.HeadObjectAsync(Bucket, destination, CancellationToken.None)).Should().NotBeNull(); - (await service.HeadObjectAsync(Bucket, source, CancellationToken.None)).Should().NotBeNull(); - } - - [Fact] - public async Task RemoveFile_deletes_the_object() - { - var (service, db) = CreateService(); - using var _ = db; - var key = $"tests/{Guid.NewGuid()}.txt"; - await service.UploadFileAsync(TextStream("delete me"), "text/plain", Bucket, key, CancellationToken.None); - - await service.RemoveFileAsync(Bucket, key, CancellationToken.None); - - (await service.HeadObjectAsync(Bucket, key, CancellationToken.None)).Should().BeNull(); - } - - [Fact] - public async Task ListObjects_returns_objects_under_prefix() - { - var (service, db) = CreateService(); - using var _ = db; - var prefix = $"listing/{Guid.NewGuid()}"; - await service.UploadFileAsync(TextStream("a"), "text/plain", Bucket, $"{prefix}/a.txt", CancellationToken.None); - await service.UploadFileAsync(TextStream("b"), "text/plain", Bucket, $"{prefix}/b.txt", CancellationToken.None); - - var keys = new List(); - await foreach (var obj in service.ListObjectsAsync(Bucket, prefix, CancellationToken.None)) - keys.Add(obj.Key); - - keys.Should().Contain($"{prefix}/a.txt").And.Contain($"{prefix}/b.txt"); - } - - [Fact] - public async Task RemoveFolder_deletes_every_object_under_prefix() - { - var (service, db) = CreateService(); - using var _ = db; - var prefix = $"folder/{Guid.NewGuid()}"; - await service.UploadFileAsync(TextStream("a"), "text/plain", Bucket, $"{prefix}/a.txt", CancellationToken.None); - await service.UploadFileAsync(TextStream("b"), "text/plain", Bucket, $"{prefix}/nested/b.txt", CancellationToken.None); - - await service.RemoveFolderAsync(Bucket, prefix, CancellationToken.None); - - var remaining = new List(); - await foreach (var obj in service.ListObjectsAsync(Bucket, prefix, CancellationToken.None)) - remaining.Add(obj); - remaining.Should().BeEmpty(); - } - - [Fact] - public async Task GetUploadUrl_returns_a_presigned_url() - { - var (service, db) = CreateService(); - using var _ = db; - - var url = await service.GetUploadUrlAsync( - Bucket, $"tests/{Guid.NewGuid()}.txt", "text/plain", TimeSpan.FromMinutes(5), CancellationToken.None); - - url.Should().StartWith(seaweedFs.ServiceUrl); - } -} diff --git a/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs b/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs deleted file mode 100644 index 2e5fe4a..0000000 --- a/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Contracts; -using Musify.Application.Tests.Infrastructure; - -namespace Musify.Infrastructure.Tests; - -internal static class JobTestHarness -{ - public static IServiceScopeFactory ScopeFactoryWith(Action configure) - { - var services = new ServiceCollection(); - configure(services); - return services.BuildServiceProvider().GetRequiredService(); - } - - public static IServiceScopeFactory WithDatabase(IDatabase database) => - ScopeFactoryWith(s => s.AddSingleton(database)); - - public static IServiceScopeFactory WithStorage(IStorageService storage) => - ScopeFactoryWith(s => s.AddSingleton(storage)); - - public static Task RunAsync(object job, CancellationToken cancellationToken = default) - { - var method = job.GetType().GetMethod( - "RunAsync", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) - ?? throw new InvalidOperationException("RunAsync not found on job."); - - return (Task)method.Invoke(job, [cancellationToken])!; - } -} diff --git a/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj b/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj deleted file mode 100644 index 820228f..0000000 --- a/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net10.0 - enable - enable - false - Musify.Infrastructure.Tests - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - diff --git a/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs b/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs deleted file mode 100644 index 2b255d3..0000000 --- a/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Tests.Infrastructure; -using Musify.Infrastructure.Jobs; -using NSubstitute; -using Xunit; - -namespace Musify.Infrastructure.Tests; - -public sealed class TemporalUploadsCleanUpJobTests -{ - private readonly IStorageService _storage = Substitute.For(); - private readonly Application.Configuration.UploadIntentConfiguration _config = TestData.UploadIntentConfig(); - - private TemporalUploadsCleanUpJob CreateJob() => - new( - JobTestHarness.WithStorage(_storage), - NullLogger.Instance, - TestData.StorageConfig(), - _config); - - private static async IAsyncEnumerable AsAsync(params StorageObject[] items) - { - foreach (var item in items) - yield return item; - await Task.CompletedTask; - } - - [Fact] - public async Task Deletes_only_objects_older_than_retention_window() - { - var cutoffDays = _config.TempUploadsRetentionDays; - var stale = new StorageObject("temp/u1/old.webp", DateTime.UtcNow.AddDays(-(cutoffDays + 2))); - var fresh = new StorageObject("temp/u1/new.webp", DateTime.UtcNow.AddHours(-1)); - - _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) - .Returns(AsAsync(stale, fresh)); - - await JobTestHarness.RunAsync(CreateJob()); - - await _storage.Received(1).RemoveFileAsync(TestData.Bucket, stale.Key, Arg.Any()); - await _storage.DidNotReceive().RemoveFileAsync(TestData.Bucket, fresh.Key, Arg.Any()); - } - - [Fact] - public async Task Continues_when_a_delete_fails() - { - var first = new StorageObject("temp/u1/a.webp", DateTime.UtcNow.AddDays(-10)); - var second = new StorageObject("temp/u1/b.webp", DateTime.UtcNow.AddDays(-10)); - - _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) - .Returns(AsAsync(first, second)); - _storage.RemoveFileAsync(TestData.Bucket, first.Key, Arg.Any()) - .Returns(_ => throw new InvalidOperationException("s3 down")); - - await JobTestHarness.RunAsync(CreateJob()); - - await _storage.Received(1).RemoveFileAsync(TestData.Bucket, second.Key, Arg.Any()); - } - - [Fact] - public async Task Does_nothing_when_no_objects_listed() - { - _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) - .Returns(AsAsync()); - - await JobTestHarness.RunAsync(CreateJob()); - - await _storage.DidNotReceive().RemoveFileAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } -} diff --git a/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs b/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs deleted file mode 100644 index cca62fb..0000000 --- a/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using Musify.Infrastructure.Jobs; -using Xunit; - -namespace Musify.Infrastructure.Tests; - -public sealed class UploadIntentExpirationJobTests -{ - private static UploadIntentExpirationJob CreateJob(SqliteTestContext ctx, Action? configure = null) - { - var config = TestData.UploadIntentConfig(); - configure?.Invoke(config); - return new UploadIntentExpirationJob( - JobTestHarness.WithDatabase(ctx.Database), - NullLogger.Instance, - config); - } - - [Fact] - public async Task Marks_issued_intents_past_expiry_as_expired() - { - await using var ctx = new SqliteTestContext(); - var expired = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-5)); - var active = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(30)); - ctx.Database.UploadIntents.AddRange(expired, active); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx)); - - var verify = ctx.NewContext(); - (await verify.UploadIntents.SingleAsync(i => i.Id == expired.Id)).Status.Should().Be(UploadIntentStatus.Expired); - (await verify.UploadIntents.SingleAsync(i => i.Id == active.Id)).Status.Should().Be(UploadIntentStatus.Issued); - } - - [Fact] - public async Task Deletes_expired_intents_past_retention_window() - { - await using var ctx = new SqliteTestContext(); - var config = TestData.UploadIntentConfig(); - var old = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired, - expiresAt: DateTime.UtcNow.AddDays(-(config.ExpiredIntentsRetentionDays + 1))); - var recent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired, - expiresAt: DateTime.UtcNow.AddHours(-1)); - ctx.Database.UploadIntents.AddRange(old, recent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx)); - - var verify = ctx.NewContext(); - (await verify.UploadIntents.AnyAsync(i => i.Id == old.Id)).Should().BeFalse(); - (await verify.UploadIntents.AnyAsync(i => i.Id == recent.Id)).Should().BeTrue(); - } - - [Fact] - public async Task With_zero_retention_just_expired_intents_are_marked_then_deleted() - { - await using var ctx = new SqliteTestContext(); - var intent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1)); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx, c => c.ExpiredIntentsRetentionDays = 0)); - - (await ctx.NewContext().UploadIntents.AnyAsync(i => i.Id == intent.Id)).Should().BeFalse(); - } - - [Fact] - public async Task Leaves_consumed_intents_untouched() - { - await using var ctx = new SqliteTestContext(); - var consumed = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Consumed, expiresAt: DateTime.UtcNow.AddDays(-10)); - ctx.Database.UploadIntents.Add(consumed); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx)); - - (await ctx.NewContext().UploadIntents.SingleAsync(i => i.Id == consumed.Id)).Status - .Should().Be(UploadIntentStatus.Consumed); - } -} From 53067358dfd467efa79bf402e4b57b29fb10ca10 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 15:45:53 +0200 Subject: [PATCH 155/172] Remove docs and agents --- AGENTS.md | 41 ---- docs/changes-summary.md | 333 ----------------------------- docs/delete-workflows-plan.md | 205 ------------------ docs/presigned-upload-hardening.md | 263 ----------------------- docs/tech-stack.md | 186 ---------------- 5 files changed, 1028 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 docs/changes-summary.md delete mode 100644 docs/delete-workflows-plan.md delete mode 100644 docs/presigned-upload-hardening.md delete mode 100644 docs/tech-stack.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 41a2ff6..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,41 +0,0 @@ -# Contexto del proyecto para agentes de IA - -## Resumen rápido -- Proyecto backend en `.NET 10`. -- Estructura principal en capas: `Domain`, `Application`, `Infrastructure`. -- Patrón arquitectónico: `Clean Architecture` con enfoque `CQRS`. - -## Arquitectura -- **Domain (`Domain`)**: entidades de negocio puras. -- **Application (`Application`)**: - - Casos de uso (comandos/queries y handlers). - - Contratos (`Application.Contracts`). - - Configuración de aplicación (`Application.Configuration`). - - Mensajería orientada a eventos a través de `IEventBus`. -- **Infrastructure (`Infrastructure`)**: - - Persistencia y acceso a datos. - - Integraciones externas (Keycloak, almacenamiento, procesamiento de imágenes). - - Implementación de mensajería y consumidores. - -## CQRS / Mediación -- Se usa **DispatchR** (paquete `DispatchR.Mediator`) como variante de MediatR para orquestar comandos y consultas. -- Los casos de uso están organizados por feature con handlers dedicados (`Commands`, `Queries`). - -## Mensajería y workers -- Se usa **MassTransit** para publicación/consumo de eventos. -- Broker actual: **RabbitMQ** (`MassTransit.RabbitMQ`). -- Hay modo de registro para workers con consumidores en `Infrastructure.Messaging.Consumers`. -- Actualmente existen consumidores como: - - `ResizePictureConsumer` - - `RemoveFileConsumer` - -## Persistencia e integraciones -- `Entity Framework Core` + proveedor `Npgsql` (PostgreSQL). -- Integración de identidad con `Keycloak`. -- Almacenamiento con `AWS S3`. - -## Notas para agentes -- Mantener separación de responsabilidades por capa. -- Evitar lógica de infraestructura dentro de `Application` y `Domain`. -- Agregar nuevos casos de uso siguiendo el patrón CQRS existente (comando/query + handler). -- Para nuevas integraciones asíncronas, reutilizar el pipeline de `MassTransit` y la abstracción `IEventBus`. diff --git a/docs/changes-summary.md b/docs/changes-summary.md deleted file mode 100644 index c25c997..0000000 --- a/docs/changes-summary.md +++ /dev/null @@ -1,333 +0,0 @@ -# MusifyBackend — Resumen de cambios (2026-05-14) - -Este documento resume **todos los cambios aplicados hasta ahora** en la rama `master` durante esta iteración. - -> Nota: El proyecto compila en `Release` al cierre de estos cambios. - ---- - -## 1) Mensajería robusta: MassTransit + EF Outbox - -**Objetivo**: hacer la publicación de eventos confiable y transaccional (DB + mensajes) y tolerar redelivery en consumidores. - -### Cambios principales -- Se integró **MassTransit EntityFramework Outbox** con PostgreSQL (`UsePostgres` + `UseBusOutbox`). -- Se añadieron entidades/tablas de **Inbox/Outbox** en el `DbContext`. -- Se generó y añadió una **migración** para las tablas de outbox/inbox. -- Se ajustó la configuración de endpoints para aplicar: - - **Retry** (exponential backoff) - - **Idempotencia** en consumo usando `UseEntityFrameworkOutbox()` -- Se ajustó el orden de operaciones para el patrón outbox: **publicar evento y luego `SaveChanges`**. - -### Archivos involucrados (principalmente) -- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` -- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs` -- `Infrastructure/Persistence/Database.cs` -- `Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs` -- `Application/Abstractions/Infrastructure/IEventBus.cs` -- `Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs` - ---- - -## 2) Workflows: Courier RoutingSlip para imágenes y audio - -**Objetivo**: mantener el pipeline asíncrono (descarga, resize/transcode, upload, update de entidades) con reintentos e idempotencia. - -### Fix importante en workflow de imágenes -- Se corrigió el nombre de actividad del paso de **Upload del tamaño Medium** (antes duplicaba el nombre de `ResizeMedium`). - -Archivos: -- `Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs` -- `Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs` - -### Fix importante en compensación de uploads -- `UploadFileToBucketActivity` ahora registra correctamente el **object key completo** (ruta + filename) para que la compensación borre el objeto correcto. - -Archivo: -- `Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs` - ---- - -## 3) Uploads (Opción B): Presigned PUT URL + Create/Update con nombres - -**Objetivo**: separar la subida de binarios (S3) de la operación de dominio (Create/Update), evitando rollbacks complejos y haciendo el flujo más robusto. - -### Endpoint/Commands añadidos (request presigned URL) -- Playlist picture: - - `Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs` - - `Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs` - - `Application/PlayLists/Responses/PlayListPictureUploadResponse.cs` -- Track picture + audio: - - `Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs` - - `Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs` - - `Application/Tracks/Responses/TrackUploadUrlsResponse.cs` - -### Storage: presigned PUT -- Se añadió soporte para generar **presigned PUT URLs**: - - `Application/Abstractions/Infrastructure/IStorageService.cs` - - `Infrastructure/Services/StorageService.cs` (`GetUploadUrlAsync`) - ---- - -## 4) Limpieza: eliminar V1 y normalizar nombres (V2 → “normal”) - -**Objetivo**: eliminar comandos/handlers “V1” (subida directa por stream/multipart) y dejar un único set de comandos “normales” con el contrato final. - -### Resultado -- `CreatePlayListCommand`, `UpdatePlayListCommand` y `CreateTrackCommand` quedaron con el contrato nuevo (nombres/metadata, no streams). -- Se eliminaron archivos `*V2*` ya innecesarios. - -Archivos clave: -- `Application/PlayLists/Commands/CreatePlayListCommand.cs` -- `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` -- `Application/PlayLists/Commands/UpdatePlayListCommand.cs` -- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` -- `Application/Tracks/Commands/CreateTrackCommand.cs` -- `Application/Tracks/Handler/CreateTrackCommandHandler.cs` - ---- - -## 5) Prefijo de seguridad: `uploads/{userId}/...` para originales - -**Objetivo**: reducir superficie (y colisiones) haciendo que los objetos originales queden naturalmente “namespaced” por usuario. - -### Cambios -- Las **keys de originales** (playlist original picture, track original picture, track original audio) ahora se generan bajo: - - `uploads/{userId}/...` -- Se añadieron overloads de rutas para construir keys con `userId`. -- Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`. -- Los eventos de workflows (sourceKey) se publican con la key prefijada. -- El borrado de playlist y track usan keys prefijadas dentro de sus workflows de delete. - -Archivos: -- `Application/Configuration/PlayListConfiguration.cs` -- `Application/Configuration/TrackConfiguration.cs` -- `Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs` -- `Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs` -- `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` -- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` -- `Application/Tracks/Handler/CreateTrackCommandHandler.cs` -- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` - - ---- - -## 6) Normalización de separadores en keys S3 (evitar `\\` en Windows) - -**Objetivo**: evitar que `Path.Combine` introduzca separadores `\\` en keys S3 (lo cual es incorrecto/inesperado en S3, y rompe interoperabilidad). - -### Cambios -- Se reemplazaron combinaciones de keys que usaban `Path.Combine` por joins con `/` en puntos críticos. - -Archivos: -- `Application/Configuration/PlayListConfiguration.cs` -- `Application/Configuration/TrackConfiguration.cs` -- `Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs` -- `Infrastructure/Services/StorageService.cs` - ---- - -## 7) Validaciones en Application eliminadas (delegadas a API) - -**Objetivo**: evitar duplicar validaciones de formato (extensiones/strings) si la capa API ya se encarga. - -### Cambios -- Se eliminó el helper de validación creado inicialmente y los checks en handlers. - -Archivos: -- Eliminado: `Application/Validation/UploadObjectNameValidator.cs` -- Ajustados: - - `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` - - `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` - - `Application/Tracks/Handler/CreateTrackCommandHandler.cs` - ---- - -## 8) Documento de mitigaciones (presigned uploads) - -Se añadió documentación de hardening/mitigaciones contra abuso/spam en endpoints presigned. - -Archivo: -- `docs/presigned-upload-hardening.md` - ---- - -## 9) Notas de compatibilidad / impacto - -- Este cambio **rompe compatibilidad** con la API anterior si existían endpoints que aceptaban multipart/streams para Create/Update (ahora se espera el flujo presigned y luego Create/Update con nombres). -- Los objetos originales ahora viven bajo `uploads/{userId}/...`. Objetos antiguos fuera de ese prefijo pueden quedar huérfanos si ya existían de antes. - ---- - -## 10) Estado final (iteración anterior) - -- `dotnet build MusifyBackend.slnx -c Release` ✅ - ---- - -## 11) Deletes unificados con Workflows (RoutingSlip) - -**Objetivo**: homogeneizar deletes de Track/PlayList usando Courier RoutingSlip con actividades granulares e idempotentes. - -### Cambios -- Se añadió `LifeCycleStatus` (`Active`/`Removing`) a `Track` y `PlayList` y se generó migración. -- Track delete: - - Se eliminó la activity monolítica y el slip ahora encadena: marcar como removing → borrar N ficheros (condicional) → borrar en DB. -- PlayList delete: - - Se añadió `DeletePlayListEvent` + consumer + routing slip builder + activities para marcar como removing, borrar pictures (si no son preset) y borrar en DB. -- Se endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado. -- Se eliminaron `RemoveFileEvent`/`RemoveFileConsumer` por quedar sin usos. - -Archivos clave: -- `Domain/Entities/LifeCycleStatus.cs` -- `Domain/Entities/Track.cs` -- `Domain/Entities/PlayList.cs` -- `Application/Events/DeletePlayListEvent.cs` -- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` -- `Application/Tracks/Handler/DeleteTrackCommandHandler.cs` -- `Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs` -- `Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs` -- `Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs` -- `Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs` -- `Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs` -- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs` -- `Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs` - ---- - -## 12) Hardening de pre-signed uploads (Fases 1 y 2) - -**Objetivo**: evitar abuso de coste/almacenamiento cuando usuarios hacen spam de endpoints presigned sin crear la entidad final (objetos huérfanos en S3). - -Referencia: `docs/presigned-upload-hardening.md` - ---- - -### Fase 1 — Upload Intents en DB - -#### Nuevas entidades (Domain) -- `Domain/Entities/UploadIntent.cs` — tabla `UploadIntents` con campos: `Id`, `UserId`, `Bucket`, `Key`, `ObjectName`, `ContentType`, `ExpectedSizeBytes`, `Purpose`, `Status`, `ExpiresAt`, `CreatedAt`. -- `Domain/Entities/UploadIntentStatus.cs` — enum: `Issued`, `Consumed`, `Expired`. -- `Domain/Entities/UploadIntentPurpose.cs` — enum: `PlayListPicture`, `TrackPicture`, `TrackAudio`. - -#### Nueva configuración (Application) -- `Application/Configuration/UploadIntentConfiguration.cs` — sección `UploadIntent`: - - `UploadUrlExpiresInSeconds` (default: 120) - - `MaxActiveUploadIntentsPerUser` (default: 5) - - `MaxActiveUploadBytesPerUser` (default: 200 MB) - - `MaxUploadBytes` (default: 100 MB, para validación HEAD) - - `DefaultExpectedPictureSizeBytes` / `DefaultExpectedAudioSizeBytes` (fallback cuando el caller no envía tamaño) - - `ExpirationJobIntervalSeconds` / `ExpiredIntentsRetentionDays` - - `TempRootPrefix` / `TempUploadsRetentionDays` / `TempCleanupJobIntervalSeconds` (Fase 2) - -#### Interfaces actualizadas -- `Application/Abstractions/Infrastructure/IDatabase.cs` — añadido `DbSet UploadIntents`. -- `Application/Abstractions/Infrastructure/IStorageService.cs` — añadidos: - - `HeadObjectAsync(bucket, key)` → `ObjectMetadata?` (para validación antes de consumir). - - `ListObjectsAsync(bucket, prefix)` → `IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)>` (para el cleanup job). -- `Application/Abstractions/Infrastructure/ObjectMetadata.cs` — record `(ContentType, ContentLength)`. - -#### Infrastructure actualizada -- `Infrastructure/Persistence/Database.cs` — añadido `DbSet`. -- `Infrastructure/Services/StorageService.cs` — implementaciones de `HeadObjectAsync` y `ListObjectsAsync`. - -#### Commands actualizados (cambio de contrato) -- `RequestPlayListPictureUploadCommand` — nuevo campo opcional `ExpectedSizeBytes?`. -- `RequestTrackUploadUrlsCommand` — nuevos campos opcionales `ExpectedPictureSizeBytes?`, `ExpectedAudioSizeBytes?`. -- `CreatePlayListCommand` — `OriginalPictureName?` → `PictureIntentId?` (Guid). -- `CreateTrackCommand` — `OriginalPictureName` + `OriginalAudioName` → `PictureIntentId` + `AudioIntentId` (Guid). -- `UpdatePlayListCommand` — `NewOriginalPictureName?` → `NewPictureIntentId?` (Guid). - -#### Responses actualizadas -- `PlayListPictureUploadResponse` — añadido `IntentId` (Guid). -- `TrackUploadUrlsResponse` — añadidos `PictureIntentId` + `AudioIntentId` (Guid). - -#### Handlers actualizados - -**Request upload** (`RequestPlayListPictureUploadCommandHandler`, `RequestTrackUploadUrlsCommandHandler`): -- Comprueban cuota de usuario (conteo de intents activos + suma de bytes) antes de generar la URL. -- Crean un registro `UploadIntent` (Status = `Issued`) en la misma llamada. -- TTL reducido a `UploadUrlExpiresInSeconds` (configurable, default 120 s vs. los anteriores 600 s hardcodeados). - -**Create/Update** (`CreatePlayListCommandHandler`, `CreateTrackCommandHandler`, `UpdatePlayListCommandHandler`): -- Reciben `IntentId` en lugar del nombre de fichero. -- Validan el intent: existe, pertenece al usuario, Status = `Issued`, no caducado. -- Realizan `HEAD` del objeto en S3: debe existir y tamaño ≤ `MaxUploadBytes`. -- Marcan el intent como `Consumed` y guardan todo en el mismo `SaveChangesAsync`. - -#### Job de expiración -- `Infrastructure/Jobs/UploadIntentExpirationJob.cs` — `BackgroundService` que: - - Marca como `Expired` los intents `Issued` con `ExpiresAt < now`. - - Elimina intents `Expired` cuya fecha de expiración supera la ventana de retención configurable. - -#### Migración -- `Infrastructure/Persistence/Migrations/*_AddUploadIntentTable.cs` - ---- - -### Fase 2 — Prefijo temporal + cleanup de S3 - -#### Rutas temporales -- `TrackRoutes.BuildTempPicturePath` / `BuildTempAudioPath` — construyen `{TempRootPrefix}/{userId}/{ParentFolder}/{objectName}`. -- `PlayListRoutes.BuildTempPicturePath` — ídem para playlists. - -#### Handlers de request upload -- La presigned URL ahora apunta a `temp/{userId}/...` (prefijo temporal) en lugar de la ruta final. -- El intent almacena la temp key. - -#### Handlers de create/update (consume) -- Al consumir un intent, se llama `CopyFileAsync(tempKey → finalKey)` antes de publicar el evento de procesado. -- El objeto temporal queda en S3 y es eliminado posteriormente por el cleanup job. - -#### Job de cleanup de temporales -- `Infrastructure/Jobs/TempUploadsCleanupJob.cs` — `BackgroundService` que: - - Lista objetos bajo `TempRootPrefix/` usando `ListObjectsAsync`. - - Borra los que superan `TempUploadsRetentionDays` (default 3 días). - - Tolerante a errores por objeto (continúa si un borrado falla). - -#### DI -- `Infrastructure/Services/ServicesDependencyInjection.AddUploadIntentJobs()` — registra `UploadIntentConfiguration` + ambos background jobs. - ---- - -## 13) Refactor: IntentValidationResult (eliminar tuplas) - -**Objetivo**: evitar retornar tuplas `(Result?, UploadIntent?)` desde métodos privados — patrón no idiomático en C#. - -### Cambios -- Creado `Application/UploadIntents/IntentValidationResult.cs` — `internal sealed record` con: - - `Result? Error` / `UploadIntent? Intent` - - `bool IsSuccess` - - Factory methods `Ok(intent)` / `Fail(error)` -- Los tres handlers (`CreatePlayListCommandHandler`, `CreateTrackCommandHandler`, `UpdatePlayListCommandHandler`) ahora usan `IntentValidationResult` en `ValidateAndLoadIntentAsync`. -- El tipo es `internal` (no `private`) porque es compartido entre 3 handlers de distintas carpetas. - ---- - -## 14) Fix: race condition en cuota de intents - -**Objetivo**: garantizar que el check de cuota + insert del intent sean atómicos y no admitan concurrent bypass. - -### Cambios -- Añadida `IDatabaseTransaction` en `Application/Abstractions/Infrastructure/IDatabaseTransaction.cs` — interfaz limpia (`CommitAsync`, `DisposeAsync`) que no expone tipos EF. -- Añadido `BeginTransactionAsync(IsolationLevel, CancellationToken)` a `IDatabase`. -- Implementado en `Infrastructure/Persistence/Database.cs` con un `private sealed class DatabaseTransaction` que envuelve `IDbContextTransaction` de EF. -- `RequestPlayListPictureUploadCommandHandler` y `RequestTrackUploadUrlsCommandHandler`: el bloque quota check + insert ahora se ejecuta dentro de una transacción `SERIALIZABLE`, eliminando la ventana de race condition entre el check y el commit. - ---- - -## 15) Fix: separación AddUploadIntentConfiguration / AddUploadIntentJobs - -**Objetivo**: `UploadIntentConfiguration` es necesaria tanto en el host del API (para los handlers) como en el worker (para los jobs). El método anterior mezclaba config + jobs en uno solo, haciendo imposible registrar la config sin registrar los `BackgroundService`. - -### Cambios -- `AddUploadIntentConfiguration(services, configuration)` — registra solo la config. Lo llama el host del API **y** el worker. -- `AddUploadIntentJobs(services, configuration)` — llama internamente a `AddUploadIntentConfiguration` y además registra `UploadIntentExpirationJob` + `TempUploadsCleanupJob`. Solo lo llama el worker. - -Archivo: `Infrastructure/Services/ServicesDependencyInjection.cs` - ---- - -## 16) Estado final - -- `dotnet build` ✅ — 0 errores, 0 advertencias. diff --git a/docs/delete-workflows-plan.md b/docs/delete-workflows-plan.md deleted file mode 100644 index 22205d7..0000000 --- a/docs/delete-workflows-plan.md +++ /dev/null @@ -1,205 +0,0 @@ -# Plan — Unificar Deletes con Workflows (RoutingSlip) - -## Contexto - -Actualmente hay dos problemas de diseño en los deletes: - -**PlayLists:** el borrado disparaba múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego eliminaba la entidad en DB. Sin workflow, sin trazabilidad. - -**Tracks:** existía un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hacía **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales. - -La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes. - ---- - -## Objetivos - -- **Un único camino** de borrado por aggregate (Track / PlayList), basado en **RoutingSlip con actividades granulares**. -- El handler de comando hace lo mínimo: validar ownership/reglas, cambiar estado a `Removing`, y publicar el evento de borrado (outbox). -- El workflow ejecuta pasos **ordenados, reutilizables y reiniciables** individualmente. -- **Reutilizar** `RemoveFileFromBucketActivity` (ya existente) para cada archivo a borrar, en lugar de lógica ad-hoc dentro de una activity monolítica. -- Eliminar `RemoveFileEvent`, `RemoveFileConsumer` y su registration si no tienen otros usos. - -No objetivos (por ahora): -- No introducir observabilidad extra (tracing/telemetría) fuera de lo ya existente. - ---- - -## Problemas del enfoque actual - -### PlayLists -- El handler implementa orquestación (decide qué archivos borrar y cómo) → acoplamiento alto. -- Publica múltiples `RemoveFileEvent` sueltos, sin agruparlos en un proceso único. -- No hay log de RoutingSlip que permita seguir el progreso/estado. - -### Tracks ← **nuevo problema identificado** -- `DeleteTrackActivity` es monolítica: carga entidad, borra pictures, borra audios, elimina de DB, todo en un solo `Execute`. -- No reutiliza `RemoveFileFromBucketActivity` ya existente. -- Si falla a mitad, no hay granularidad de reintento (no se sabe qué paso falló). -- El RoutingSlip tiene un único nodo → no es realmente un "slip" con pasos. - ---- - -## Diseño propuesto - -## Implementación final (nombres reales) - -Este plan se implementó con los siguientes nombres finales (para facilitar traceo/debug): - -### Colas (consumers) -- `delete-track` (`DeleteTrackConsumer`) -- `delete-playList` (`DeletePlayListConsumer`) - -### Execute endpoints (activities) -MassTransit crea las colas de execute como `_execute`. - -- `remove-file-from-bucket` → cola `remove-file-from-bucket_execute` -- `mark-track-as-removing` → cola `mark-track-as-removing_execute` -- `delete-track-from-db` → cola `delete-track-from-db_execute` -- `mark-playList-as-removing` → cola `mark-playList-as-removing_execute` -- `delete-playList-from-db` → cola `delete-playList-from-db_execute` - -### Estado de borrado (DB) -- Se introdujo `LifeCycleStatus` en `Track` y `PlayList` con valores `Active` y `Removing`. -- El handler marca `Removing` antes de publicar el evento (y persiste con `SaveChanges`). - -> Nota: se eliminó el enfoque de “estado Removing” implícito en `ProcessingStatus` para deletes. El tracking de delete queda en `LifeCycleStatus`. - -### Actividades compartidas / reutilizables - -Ya existe (y se mantiene): -- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip. - -Se añaden actividades de DB por aggregate: -- `MarkTrackAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste. -- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones). -- `MarkPlayListAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste. -- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones). - -### Estructura del RoutingSlip para Track - -``` -DeleteTrackEvent - └─► DeleteTrackConsumer (queue: `delete-track`) - └─► DeleteTrackRoutingSlipBuilder.BuildAsync(...) - ├─ MarkTrackAsRemovingActivity (endpoint: `mark-track-as-removing`) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original picture - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small (si IsPicturesProcessed) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium (si IsPicturesProcessed) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large (si IsPicturesProcessed) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original audio - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — processed audio (si IsAudioProcessed) - └─ DeleteTrackFromDbActivity (endpoint: `delete-track-from-db`) -``` - -> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue). - -### Estructura del RoutingSlip para PlayList - -``` -DeletePlayListEvent - └─► DeletePlayListConsumer (queue: `delete-playList`) - └─► DeletePlayListRoutingSlipBuilder.BuildAsync(...) - ├─ MarkPlayListAsRemovingActivity (endpoint: `mark-playList-as-removing`) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original (uploads/{userId}/...) ← solo si no es preset - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small ← solo si no es preset - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium ← solo si no es preset - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large ← solo si no es preset - └─ DeletePlayListFromDbActivity (endpoint: `delete-playList-from-db`) -``` - -### Handler de comando (ambos aggregates) - -```csharp -// DeletePlayListCommandHandler — ANTES -await PublishRemoveFileEventsAsync(...); // 4 eventos sueltos -database.PlayLists.Remove(playlist); -await database.SaveChangesAsync(); - -// DESPUÉS -playlist.LifeCycleStatus = LifeCycleStatus.Removing; -await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId)); -await database.SaveChangesAsync(); -// El workflow se encarga de borrar ficheros y eliminar en DB. -``` - -```csharp -// DeleteTrackCommandHandler — ANTES -// (ya publicaba DeleteTrackEvent, se mantiene igual) - -// DESPUÉS -track.LifeCycleStatus = LifeCycleStatus.Removing; -await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId)); -await database.SaveChangesAsync(); - -// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica. -``` - ---- - -## Reglas de negocio a preservar - -- Ownership se valida en el handler del comando (igual que ahora), antes de publicar el evento. -- PlayList: solo borrar original/sizes si no son presets. -- PlayList: el original está bajo `uploads/{userId}/...` (cambio reciente). -- Track: respetar estados de procesamiento — si audio está `Pending/Processing`, el handler ya bloquea el delete antes de llegar al workflow. -- EF cascade elimina `PlayListHasTrack` al borrar la PlayList → no hace falta activity explícita para ello. - ---- - -## Idempotencia / reintentos - -- RoutingSlip + reintentos de endpoint ya configurados en MassTransit. -- Operaciones S3: idempotentes por diseño. Si el key no existe (`NoSuchKey`/`NotFound`) → log `Information` y `Completed()` (no throw). -- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora). -- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip. - ---- - -## Plan de implementación (paso a paso) - -### 1. Inventario -- Confirmar todos los usos de `RemoveFileEvent` / `RemoveFileConsumer` en la solución. -- Si no tienen otros usos fuera de playlist delete → marcarlos para eliminación en el paso 7. - -### 2. Refactorizar Track delete ← **nuevo** -- Crear `MarkTrackAsRemovingActivity` (execute-activity): carga Track, setea estado `Removing`, persiste. -- Crear `DeleteTrackFromDbActivity` (execute-activity): carga Track, llama `Remove()`, persiste. -- Actualizar `DeleteTrackRoutingSlipBuilder`: - - Carga el Track para calcular el plan (keys condicionales según `IsPicturesProcessed`, `IsAudioProcessed`). - - Encadena: `MarkTrackAsRemoving` → N × `RemoveFileFromBucket` → `DeleteTrackFromDb`. -- Eliminar `DeleteTrackActivity` monolítica (ya no se usa). -- Ajustar registration DI de MassTransit (quitar activity vieja, registrar las nuevas). - -### 3. Crear PlayList delete: evento + consumer + builder -- Añadir `DeletePlayListEvent(PlayListId, UserId)` en `Application/Events`. -- Crear `DeletePlayListConsumer` en Infrastructure: recibe el evento y ejecuta el builder. -- Crear `DeletePlayListRoutingSlipBuilder`: - - Carga la PlayList para calcular el plan (keys condicionales por presets, ruta de original). - - Encadena: `MarkPlayListAsRemoving` → N × `RemoveFileFromBucket` → `DeletePlayListFromDb`. - -### 4. Crear actividades de PlayList -- `MarkPlayListAsRemovingActivity` (execute-activity): carga PlayList, setea `Removing`, persiste. -- `DeletePlayListFromDbActivity` (execute-activity): carga PlayList, llama `Remove()`, persiste (cascade). - -### 5. Actualizar `RemoveFileFromBucketActivity` para NotFound -- Si el SDK lanza excepción por key inexistente → capturar, log `Information("Key {Key} not found, skipping")`, retornar `Completed()`. -- Si es otro error → log `Error` + throw (reintento normal). - -### 6. Cambiar `DeletePlayListCommandHandler` -- Reemplazar `PublishRemoveFileEventsAsync` por `eventBus.PublishAsync(new DeletePlayListEvent(...))`. -- No publicar más `RemoveFileEvent` para playlist delete. - -### 7. Ajustar DI / registration MassTransit -- Registrar: `DeletePlayListConsumer`, `MarkPlayListAsRemovingActivity`, `DeletePlayListFromDbActivity`. -- Registrar: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`. -- Desregistrar: `DeleteTrackActivity` (monolítica), y si procede `RemoveFileConsumer`. - -### 8. Eliminar código muerto -- `DeleteTrackActivity` monolítica. -- `RemoveFileEvent` y `RemoveFileConsumer` (si no tienen otros usos). -- Endpoints y registrations asociados. - -### 9. Build y verificación -- `dotnet build -c Release` sin warnings de referencias muertas. -- Smoke test manual: borrar un Track y una PlayList, verificar logs de cada activity en orden. \ No newline at end of file diff --git a/docs/presigned-upload-hardening.md b/docs/presigned-upload-hardening.md deleted file mode 100644 index f9159b8..0000000 --- a/docs/presigned-upload-hardening.md +++ /dev/null @@ -1,263 +0,0 @@ -# Hardening: abuso de pre-signed uploads (Opción B) - -## Contexto -Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando `pictureName/audioName`. - -Esto mejora latencia y descarga el host/servicio, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”). - -## Qué pasa hoy si el usuario spamea -- Se generan muchas URLs pre-firmadas (costo bajo pero no cero). -- El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB. -- El impacto principal es: - - coste/uso de S3 (storage + requests) - - posible saturación del componente de generación de URLs - -## Objetivo -Reducir abuso y coste, manteniendo: -- UX simple -- backend sin streams -- workflows asíncronos + MassTransit Outbox - -## Qué significa “sólido” aquí -Como el upload es directo a S3 (o compatible), el backend no puede garantizar “single-use” del upload en el lado del storage mientras una URL sea válida. Por tanto, el objetivo realista es: - -- Asegurar que **cualquier abuso solo pueda impactar un prefijo temporal** barato de limpiar. -- Poner límites duros (cuotas) en DB para que el coste tenga un techo. -- No aceptar referencias a objetos no válidos (verificación antes de consumir). -- Evitar pérdida de datos en reemplazos (no borrar la versión previa hasta éxito). - -## Principio de configuración (no hardcode) -Todas las rutas/prefijos/TTLs/límites deben ser **configurables**. - -Regla: -- En `Application`/`Infrastructure` no debe haber strings hardcodeadas de keys S3 (p.ej. `temp/`, `uploads/`, `PlayLists/`, `Tracks/`). -- Las rutas deben centralizarse en opciones/config (p.ej. `StorageRoutes`, `PlayListRoutes`, `TrackRoutes`, o similar), de forma que cambiar la convención de keys no requiera tocar lógica. - -Los ejemplos de este documento son **convenciones sugeridas** y deben leerse como plantillas parametrizadas. - -## Invariantes de seguridad (reglas que no se rompen) -- El cliente solo puede escribir en rutas temporales bajo `{TempRootPrefix}/...` (o el prefijo temporal equivalente). La ruta final **nunca** recibe credenciales pre-firmadas. -- Cada intent emite un `Key` **nuevo** (GUID). No hay keys “estables” para reusar. -- Una entidad solo apunta a un `pictureName/audioName` si su intent asociado se valida y se consume. -- Reemplazos: el borrado de la versión anterior ocurre **al final** del workflow, tras actualizar DB. - -## Prioridad (coste alto) -Si el coste es el principal riesgo, el orden recomendado es: -1) **Cuotas duras en DB** (intents activos + bytes activos) + job de expiración. -2) Prefijo temporal + cleanup por prefijo (lifecycle si existe, y job como fallback). -3) Validación de objeto (HEAD) antes de consumir. -4) (Opcional) Migrar a pre-signed POST si el backend de storage lo soporta bien. - -## Decisiones validadas (no tocaría) -- El orden de prioridad (cuotas → prefijo temporal → HEAD → POST) es el más sensato cuando el **coste** es el riesgo principal. -- La advertencia sobre S3-compatible / SeaweedFS es importante: el diseño debe ser seguro sin depender de features avanzadas. -- El principio de no hardcodear rutas y centralizar prefijos/convenciones en config es coherente con el estilo del repo. -- La semántica de errores debe permanecer separada del mapeo a códigos/respuestas (responsabilidad del host/presentación). -- En reemplazos, no borrar la versión anterior hasta éxito del workflow es clave para resiliencia con Courier. - -## Mitigaciones recomendadas (pueden coexistir) - -### 1) “Upload intents” en DB (control y conciliación) -Persistir una intención de upload cuando se genera un pre-signed URL. - -> Importante: **no usar el nombre original del fichero** como `pictureName/audioName`. La key debe usar un **identificador generado** (p.ej. GUID) para evitar colisiones, inputs “raros” y facilitar cleanup. - -Sugerencia práctica: -- `objectName = {guid}.{ext}` (o directamente `{guid}` si la extensión no aporta valor) -- Guardar `originalFileName` solo como metadata opcional (si se necesita para UI), pero **nunca** como key. - -Modelo sugerido (mínimo): -- `UploadIntent` - - `Id` - - `UserId` - - `Bucket` - - `Key` - - `ContentType` - - `ExpectedSizeBytes` (opcional; si el caller conoce el tamaño) - - `ExpiresAt` - - `Status`: `Issued | Consumed | Expired` - - `CreatedAt` - -Opcional (útil para auditoría/UX): -- `OriginalFileName` -- `Purpose` (`TrackPicture | TrackAudio | PlayListPicture`) - -Reglas: -- Al pedir URL (Request*Upload*): - - insertar intent `Issued` - - imponer límites por usuario (ver “Cuotas”) - - si supera el límite: fallar con un error semántico (p.ej. `UploadIntentLimitExceeded`) -- Al crear/actualizar entidad (Create*/Update* V2): - - validar que el `pictureName/audioName` recibido corresponde a un intent `Issued` del mismo `UserId` y no expirado - - validar que el objeto existe en S3 (HEAD) y que tamaño / `Content-Type` cumplen la policy del backend - - marcar intent como `Consumed` - -#### Cuotas (anti-cost) 🔴 Alta -Además del conteo de intents, imponer **cuotas por bytes** para que el coste tenga techo. - -Recomendación mínima: -- `MaxActiveUploadIntentsPerUser` (número) -- `MaxActiveUploadBytesPerUser` (bytes) — suma de objetos temporales “en vuelo” por usuario - -Cómo aproximarlo sin streams: -- Guardar `ExpectedSizeBytes` en el intent (si está disponible). -- Si no se dispone del tamaño, usar un valor conservador por `Purpose` (p.ej. imagen <= X, audio <= Y) y validarlo en HEAD antes de consumir. - -> Nota: si el caller puede mentir sobre tamaño, no confíes ciegamente en `ExpectedSizeBytes`; úsalo como guardrail y valida al consumir. - -#### Semántica de errores (intent inválido) 🟡 Media -Definir errores semánticos y dejar el mapeo al “caller” (host/presentación): -- `UploadIntentLimitExceeded` -- `UploadIntentNotFoundOrNotAccessible` (si se quiere no filtrar existencia) -- `UploadIntentAlreadyConsumed` -- `UploadIntentExpired` -- `UploadObjectNotFound` -- `UploadObjectPolicyViolation` (tamaño, `Content-Type`, extensión, etc.) - -#### Nota sobre race conditions en el límite 🟡 Media -El check “`count(Issued) < Max`” es vulnerable a concurrencia (dos requests paralelas pueden pasar el check y crear 2 intents extra). - -Mitigaciones (de menor a mayor robustez): -- Ejecutar **check + insert** en la **misma transacción** con aislamiento fuerte (p.ej. `SERIALIZABLE`). -- Usar un mecanismo de lock por usuario (p.ej. advisory lock en PostgreSQL) durante la emisión. -- Modelar el límite con una tabla/contador por usuario y aplicar **update condicional** (estilo “compare-and-swap”). - -#### Job de expiración de intents 🔴 Alta -Aunque `ExpiresAt` esté en DB, hace falta un mecanismo que marque y limpie intents caducados: -- Un job periódico (worker/cron) que: - - marque intents `Issued` con `ExpiresAt < now` como `Expired` - - opcionalmente encole el borrado del objeto S3 (si existe) - -Si se aplica lifecycle en S3 (ver sección 2), el job puede ser “solo DB” (marcar `Expired`) y dejar el borrado real a S3. - -Adicionalmente, este job es un buen sitio para: -- borrar intents `Expired` antiguos (retención de auditoría configurable) -- emitir métricas/contadores (intents expirados por usuario) - -Ventajas: -- Previene spam con reglas duras y auditables. -- Permite cleanup de huérfanos con certeza. - -Coste: -- entidad + migración + lógica de validación. - -### 2) Limitar el scope del pre-signed URL (reducción de daño) -Aplicar restricciones conservadoras: -- Reducir TTL: - - `ExpiresInSeconds` más bajo (p.ej. 60–120s en vez de 600s) -- Validar/limitar: - - `ContentType` permitido - - extensiones permitidas (`FileType`) -- Usar keys temporales por usuario: - - Opción recomendada (facilita lifecycle por prefijo): - - `{TempRootPrefix}/{userId}/playlists/{guid}.{ext}` - - `{TempRootPrefix}/{userId}/tracks/{guid}.{ext}` - - Alternativa (si se quiere mantener todo bajo `uploads/{userId}`): - - `{UploadsRootPrefix}/{userId}/PlayLists/TempUploads/{guid}.{ext}` - - `{UploadsRootPrefix}/{userId}/Tracks/TempUploads/{guid}.{ext}` - -Luego, en el workflow/worker: -- mover/copiar desde la ruta temporal (p.ej. `temp/...` o `.../TempUploads/...`) a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`) -- borrar el objeto temporal - -Ventajas: -- Cleanup masivo sencillo (por prefijo `{TempRootPrefix}/`) -- Reduce el impacto de huérfanos. - -#### S3 Lifecycle Rule en `TempUploads` 🔴 Alta -Configurar una **Lifecycle Rule** en el bucket para auto-expirar objetos bajo el prefijo de temporales, por ejemplo: -- Prefijo: `{TempRootPrefix}/` -- Expiración: 1–7 días (según tolerancia a reintentos) - -Nota: las reglas de lifecycle por **prefijo** no soportan comodines en medio (p.ej. `uploads/*/...`). -Si se usa la alternativa `uploads/{userId}/.../TempUploads/...`, para filtrar correctamente se recomienda: -- cambiar la convención de key para que el prefijo sea común (p.ej. `{TempRootPrefix}/...`), o -- usar **tags** en los objetos (p.ej. `temporary=true`) y filtrar la lifecycle rule por tag (si se puede firmar el tagging en el upload). - -Esto evita que el coste de huérfanos crezca indefinidamente incluso si el job de cleanup falla. - -#### Cleanup backend (fallback) 🔴 Alta -No dependas solo de lifecycle del storage (en S3 compatibles puede variar). Añadir un job de cleanup que: -- liste por prefijo temporal `{TempRootPrefix}/` -- borre objetos con edad > `TempUploadsRetentionDays` - -Este job debe ser: -- incremental (paginación) -- tolerante a errores (reintentos) -- con límites de throughput (para no auto-generar costes por borrar en masa) - -#### Restricción de `Content-Type` en S3 🟡 Media -Si el objetivo es enforcement “duro” a nivel de S3 (no solo validación posterior), **pre-signed POST** suele ser más robusto que PUT. - -**pre-signed PUT** (lo actual en este repo): -- Permite fijar `Content-Type` si se pre-firma incluyendo ese header. -- Es más limitado para imponer condiciones ricas (tamaño, tags, etc.). -- Recomendación: combinar con validación posterior (HEAD) antes de consumir el intent. - -**pre-signed POST** (recomendado si buscas más control): -- Permite policy conditions como: - - `content-length-range` (tamaño máximo) - - `eq`/`starts-with` para `Content-Type` - - key/prefijo (el cliente no elige keys fuera del scope) - - headers requeridos (p.ej. SSE) - - opcional: tagging (p.ej. `temporary=true`) para lifecycle por tag - -Trade-off: el cliente sube con `multipart/form-data` (POST), pero el backend sigue sin manejar streams. - -#### Nota S3-compatible / SeaweedFS 🟡 Media -No todos los backends S3 compatibles implementan al 100%: -- policy conditions de POST -- tagging en upload -- respuestas/errores idénticos a AWS - -Recomendación: -- Diseñar el sistema para ser seguro **sin depender** de condiciones avanzadas (keys únicas + prefijo temporal + cuotas + verificación HEAD). -- Tratar POST/condiciones como una mejora opcional, tras validar compatibilidad del backend. - -#### Reemplazos: no borrar originales antiguos hasta éxito ✅ -Al actualizar una imagen/audio existente: -- Subir el nuevo binario a la ruta temporal (p.ej. `{TempRootPrefix}/.../{newGuid}`). -- Procesar (resize/transcode) y subir a rutas finales con nombres nuevos. -- Actualizar la entidad en DB para apuntar a los nuevos `*PictureName/*AudioFolderName`. -- **Solo al final** borrar los objetos antiguos (original + sizes / audio procesado), cuando el workflow haya completado. - -Esto evita perder la versión anterior si el worker falla a mitad (punto crítico para resiliencia). - -## Validación del objeto antes de consumir (HEAD) 🔴 Alta -Antes de consumir un intent y actualizar DB: -- `HEAD Bucket/Key` -- validar `Content-Type` esperado (según `Purpose`) -- validar tamaño máximo (`MaxUploadBytes` o por propósito) -- (opcional) validar extensión inferida de `Key` si se usa - -Si falla la validación: -- no consumir el intent -- marcarlo como violación o dejarlo expirar -- el cleanup se encarga del objeto - -## Orden de implementación sugerido -1) Upload intents en DB (límite de activos + validación al crear/actualizar) -2) Reducir TTL y mover a rutas temporales + S3 lifecycle + cleanup -3) Si se requiere enforcement fuerte: migrar a pre-signed POST + policy conditions - -## Parámetros recomendados (config) -- `UploadUrlExpiresInSeconds` (default: 120) -- `MaxActiveUploadIntentsPerUser` (default: 5) -- `MaxActiveUploadBytesPerUser` (default sugerido: depende de coste; p.ej. 200MB) -- `AllowedPictureExtensions` / `AllowedAudioExtensions` -- `AllowedContentTypes` (lista blanca) -- `TempUploadsRetentionDays` (default: 3) -- `PresignedUploadMethod` (default sugerido: `POST` si se quiere enforcement fuerte; `PUT` si se prioriza simplicidad) -- `MaxUploadBytes` (límite máximo de tamaño por upload) - -Jobs (config): -- `UploadIntentsExpirationJobIntervalSeconds` -- `TempUploadsCleanupJobIntervalSeconds` -- `ExpiredIntentsRetentionDays` - -Rutas/prefijos (ejemplos): -- `TempRootPrefix` (default sugerido: `temp`) -- `UploadsRootPrefix` (default sugerido: `uploads`) - -## Notas -- Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos. diff --git a/docs/tech-stack.md b/docs/tech-stack.md deleted file mode 100644 index 9243f62..0000000 --- a/docs/tech-stack.md +++ /dev/null @@ -1,186 +0,0 @@ -# Tech stack y arquitectura (MusifyBackend) - -Este documento describe **qué tecnologías usa este repo** y **cómo encajan** (capas, workers, mensajería, storage, observabilidad). Está pensado para alguien que llega de cero o para una IA que necesita contexto. - -## Foto rápida -- Runtime: **.NET 10** (C#) -- Estilo: **Clean Architecture** (capas `Domain` / `Application` / `Infrastructure`) -- Casos de uso: **CQRS** con **DispatchR** (similar a MediatR) -- Persistencia: **Entity Framework Core** + **PostgreSQL** (provider `Npgsql`) -- Workers / mensajería: **MassTransit** + **RabbitMQ** -- Orquestación de workflows: **MassTransit Courier / RoutingSlip** (Activities) -- Storage de objetos: **SeaweedFS** expuesto como **S3-compatible** (vía AWS SDK `IAmazonS3`) -- Observabilidad: **OpenTelemetry** (traces + metrics, export OTLP) -- Procesado multimedia: - - Imágenes: **SixLabors.ImageSharp** (resize, WebP) - - Audio: **ffmpeg** (transcode a DASH) - -## Estructura del repo (capas) - -### `Domain/` (núcleo de negocio) -- Entidades puras (sin dependencias de infraestructura). -- Ejemplos: `Track`, `PlayList`, `Upload`, etc. -- Estados de negocio relevantes: - - `ProcessingStatus` (procesado en curso/finalizado, etc.). - - `LifeCycleStatus` (p.ej. `Active`, `Removing`) para deletes asíncronos. - -### `Application/` (casos de uso + contratos) -- Contiene comandos/queries/handlers por feature. -- CQRS: - - `Commands/` y `Queries/` por feature (`PlayLists`, `Tracks`, `Users`, ...). - - `Handlers/` ejecutan la lógica de aplicación (coordinar DB + eventos), pero **no implementan** detalles de infraestructura. -- Contratos con infraestructura: `Application/Abstractions/Infrastructure/*` - - `IDatabase` (abstracto de EF DbContext) - - `IEventBus` (publicación de eventos) - - `IStorageService`, `IPictureService`, `IAudioTranscoderService`, etc. -- Eventos de integración: `Application/Events/*` - - Se publican desde `Application` y se consumen en `Infrastructure`. - -### `Infrastructure/` (implementaciones + integraciones) -- Implementa las abstracciones de `Application`. -- Contiene: - - EF Core DbContext y migraciones - - MassTransit (consumers, activities, routing slips, outbox) - - Integración S3 (SeaweedFS) mediante AWS SDK - - Observabilidad OpenTelemetry - - Servicios de procesado (ImageSharp/ffmpeg) - -## CQRS con DispatchR -- Los “use cases” se modelan como: - - **Commands**: mutaciones (crear/actualizar/borrar). - - **Queries**: lecturas. -- Los handlers viven en `Application/*/Handlers/` y dependen de: - - `IDatabase` para persistencia (sin acoplarse al DbContext concreto). - - `IEventBus` para emitir eventos cuando hay trabajo asíncrono. - -## Persistencia (EF Core + PostgreSQL) -- DbContext: `Infrastructure/Persistence/Database.cs` - - Usa `UseNpgsql(connectionString)`. - - Incluye tablas de MassTransit Outbox/Inbox (ver sección de mensajería). -- DI: `Infrastructure/Persistence/DatabaseDependencyInjection.cs` -- Config: `Infrastructure/Configuration/DatabaseConfiguration.cs` (sección `Database`) - - `ConnectionString` - -## Mensajería y workers (MassTransit + RabbitMQ) - -### Qué se usa -- Broker: RabbitMQ (`MassTransit.RabbitMQ`). -- MassTransit está configurado en dos modos: - 1) **Cliente** (publica eventos): `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` → `AddMassTransitClient()` - 2) **Workers** (consumen y ejecutan activities): `AddMassTransitConsumers()` - -### Outbox / Inbox (robustez) -- Se usa **EntityFramework Outbox** (`AddEntityFrameworkOutbox()`) con: - - `UsePostgres()` - - `UseBusOutbox()` - - `QueryDelay = 1s` - - `DuplicateDetectionWindow = 30m` -- En el DbContext se registran entidades de MassTransit: - - `AddInboxStateEntity()` - - `AddOutboxMessageEntity()` - - `AddOutboxStateEntity()` - -Idea clave: -- La outbox asegura que, tras confirmar la transacción de DB, los mensajes/eventos pendientes se publican de forma fiable. - -### Courier / RoutingSlip (workflows asíncronos) -Para operaciones multi-paso (p.ej. imagen/audio y deletes), se usa **MassTransit Courier**: -- Un “RoutingSlip” es una lista de pasos. -- Cada paso es una **Activity** (con args + log opcional). -- Existen dos tipos que aparecen en este repo: - - `ExecuteActivity` (endpoint `_execute`), típicamente acciones atómicas. - - `Activity` (con compensación/log), típico para pasos de workflow. - -Dónde mirar: -- Builders: `Infrastructure/MassTransit/RoutingSlip/Builders/*` -- Activities: `Infrastructure/MassTransit/Activities/*` -- Consumers (punto de entrada del workflow): `Infrastructure/MassTransit/Consumers/*` -- Registro de endpoints: `Infrastructure/MassTransit/DependencyInjection/*` - -Ejemplos de workflows presentes: -- Procesado de imagen (resize + upload + update DB) -- Procesado de audio (transcode DASH + upload + update DB) -- Deletes asíncronos (marcar “removing”, borrar objetos, borrar filas) - -### Config de MassTransit -- `Infrastructure/Configuration/MassTransitConfiguration.cs` (sección `MassTransit`) - - `Address` - - `Username` - - `Password` - -## Storage de objetos (SeaweedFS como S3-compatible) - -### Qué se usa -- SDK: `Amazon.S3` (`IAmazonS3`). -- Implementación: `Infrastructure/Services/StorageService.cs` (usa `IAmazonS3`). -- DI: `Infrastructure/Services/ServicesDependencyInjection.cs` → `AddStorageService()` - -### Config -- `Infrastructure/Configuration/InfrastructureStorageConfiguration.cs` (sección `InfrastructureStorage`) - - `Address` (ServiceURL) - - `AccessKey` / `SecretAccessKey` - - `ForcePathStyle` (útil en S3-compatibles) - - `UseHttp` - -Notas prácticas (S3-compatible): -- Algunas features avanzadas de AWS S3 pueden no comportarse igual (depende de SeaweedFS). Diseñar workflows para que sean correctos con el subconjunto básico: PUT/GET/HEAD/LIST/DELETE y paths bien definidos. - -## Procesado multimedia - -### Imágenes -- Librería: `SixLabors.ImageSharp` -- Servicio: `Infrastructure/Services/PictureService.cs` -- Contrato: `Application/Abstractions/Infrastructure/IPictureService.cs` -- Patrón: workers descargan/transforman/suben; la app coordina por eventos. - -### Audio -- Herramienta: `ffmpeg` (binario externo) -- Config: `Infrastructure/Configuration/AudioTranscoderConfiguration.cs` (sección `AudioTranscoder`) - - Timeout, codec/bitrate/channels/sample-rate, segmentación DASH, etc. -- Servicio: `Infrastructure/Services/AudioTranscoderService.cs` -- Contrato: `Application/Abstractions/Infrastructure/IAudioTranscoderService.cs` - -### Directorio temporal (workers) -- Config: `Infrastructure/Configuration/WorkerConfiguration.cs` (sección `Workers`) - - `Routes.TemporaryFilesDirectory` - -## Observabilidad (OpenTelemetry) -- DI: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs` -- Config: `Infrastructure/Configuration/OpenTelemetryConfiguration.cs` (sección `OpenTelemetry`) - - `OtlpEndpoint` - -Qué instrumenta: -- Tracing: - - `MassTransit` - - ASP.NET Core - - HttpClient - - EF Core - - Exporter OTLP -- Métricas: - - `MassTransit` - - ASP.NET Core - - HttpClient - - Runtime + Process - - Exporter OTLP - -## Identidad (Keycloak) -- El repo asume integración con **Keycloak** (ver `AGENTS.md` y nombres como `GetUserByKeycloakQuery`). -- La capa `Application` trata el “Keycloak Id” como identificador de usuario en algunos casos de uso. -- La integración concreta (tokens, validación, etc.) puede vivir fuera de estas 3 capas o en otro proyecto/host. - -## Glosario rápido -- **Command / Query**: operación de escritura / lectura. -- **Handler**: clase que ejecuta el caso de uso (CQRS). -- **EventBus**: abstracción para publicar eventos (MassTransit por debajo). -- **Consumer**: “listener” de mensajes en una cola. -- **RoutingSlip**: workflow compuesto por Activities. -- **Activity**: paso del workflow (puede tener log/compensación). -- **Outbox**: patrón para publicar eventos de forma fiable tras commit de DB. - -## “Dónde empiezo a leer” (ruta recomendada) -1) Arquitectura general: `AGENTS.md` -2) Casos de uso: `Application/*/Commands`, `Application/*/Queries` y sus `Handlers/` -3) Persistencia: `Infrastructure/Persistence/Database.cs` + migraciones -4) Mensajería/workflows: `Infrastructure/MassTransit/*` -5) Integraciones: `Infrastructure/Services/*` (S3, imagen, audio) -6) Observabilidad: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs` From b87b4a7dd515a25d1fee8c4bc8a3c2882ccf214a Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 30 May 2026 03:29:21 +0200 Subject: [PATCH 156/172] Proxy --- .../ConfigurationDependencyInjection.cs | 12 +++++ .../StreamGatewayConfiguration.cs | 16 ++++++ Application/Contracts/IStreamTicketService.cs | 9 ++++ .../Handlers/GetTrackStreamQueryHandler.cs | 54 +++++++++++++++++++ .../Tracks/Queries/GetTrackStreamQuery.cs | 9 ++++ .../Tracks/Responses/TrackStreamResponse.cs | 7 +++ .../StreamTicketConfiguration.cs | 21 ++++++++ Infrastructure/Infrastructure.csproj | 1 + .../Services/ServicesDependencyInjection.cs | 14 +++++ .../Services/StreamTicketService.cs | 52 ++++++++++++++++++ 10 files changed, 195 insertions(+) create mode 100644 Application/Configuration/StreamGatewayConfiguration.cs create mode 100644 Application/Contracts/IStreamTicketService.cs create mode 100644 Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs create mode 100644 Application/Tracks/Queries/GetTrackStreamQuery.cs create mode 100644 Application/Tracks/Responses/TrackStreamResponse.cs create mode 100644 Infrastructure/Configuration/StreamTicketConfiguration.cs create mode 100644 Infrastructure/Services/StreamTicketService.cs diff --git a/Application/Configuration/ConfigurationDependencyInjection.cs b/Application/Configuration/ConfigurationDependencyInjection.cs index c7e2504..a4687af 100644 --- a/Application/Configuration/ConfigurationDependencyInjection.cs +++ b/Application/Configuration/ConfigurationDependencyInjection.cs @@ -41,5 +41,17 @@ namespace Musify.Application.Configuration return serviceDescriptors; } + + public static IServiceCollection AddStreamGatewayConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(StreamGatewayConfiguration.SectionName)) + .ValidateDataAnnotations(); + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + return serviceDescriptors; + } } } \ No newline at end of file diff --git a/Application/Configuration/StreamGatewayConfiguration.cs b/Application/Configuration/StreamGatewayConfiguration.cs new file mode 100644 index 0000000..e19cad6 --- /dev/null +++ b/Application/Configuration/StreamGatewayConfiguration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Application.Configuration +{ + public class StreamGatewayConfiguration + { + public const string SectionName = "StreamGateway"; + + [Url] + [Required] + public required string PublicBaseUrl { get; set; } + + [Required] + public string ManifestFileName { get; set; } = "manifest.mpd"; + } +} diff --git a/Application/Contracts/IStreamTicketService.cs b/Application/Contracts/IStreamTicketService.cs new file mode 100644 index 0000000..21b6f50 --- /dev/null +++ b/Application/Contracts/IStreamTicketService.cs @@ -0,0 +1,9 @@ +namespace Musify.Application.Contracts +{ + public interface IStreamTicketService + { + StreamTicket IssueTicket(Guid userId, string keyPrefix); + } + + public record StreamTicket(string Token, int ExpiresInSeconds); +} diff --git a/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs b/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs new file mode 100644 index 0000000..b9f4eff --- /dev/null +++ b/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs @@ -0,0 +1,54 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts; +using Musify.Application.Tracks.Queries; +using Musify.Application.Tracks.Responses; +using Musify.Domain.ValueObjects; + +namespace Musify.Application.Tracks.Handlers +{ + public class GetTrackStreamQueryHandler( + IDatabase database, + IStreamTicketService ticketService, + TrackConfiguration trackConfiguration, + StreamGatewayConfiguration streamGatewayConfiguration, + ILogger logger) + : IQueryHandler> + { + public async ValueTask> Handle(GetTrackStreamQuery request, CancellationToken cancellationToken) + { + var track = await database.Tracks.AsNoTracking() + .Where(t => t.Id == request.TrackId) + .Select(t => new { t.Id, t.AudioTranscodeProcessingStatus, t.AudioFolderName }) + .SingleOrDefaultAsync(cancellationToken); + + if (track is null) + return Result.NotFound(); + + if (string.IsNullOrWhiteSpace(track.AudioFolderName) + || track.AudioTranscodeProcessingStatus != ProcessingStatus.Completed) + { + logger.LogInformation("Stream requested for track {TrackId} but audio is not ready", request.TrackId); + return Result.Conflict("Track audio is not available for streaming yet."); + } + + var folderPath = trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName); + var keyPrefix = $"{folderPath}/"; + + var ticket = ticketService.IssueTicket(request.UserId, keyPrefix); + + var manifestUrl = string.Join('/', + streamGatewayConfiguration.PublicBaseUrl.TrimEnd('/'), + "media", + folderPath, + streamGatewayConfiguration.ManifestFileName); + + logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId); + + return Result.Success(new TrackStreamResponse(manifestUrl, ticket.Token, ticket.ExpiresInSeconds)); + } + } +} diff --git a/Application/Tracks/Queries/GetTrackStreamQuery.cs b/Application/Tracks/Queries/GetTrackStreamQuery.cs new file mode 100644 index 0000000..66733ad --- /dev/null +++ b/Application/Tracks/Queries/GetTrackStreamQuery.cs @@ -0,0 +1,9 @@ +using Ardalis.Result; +using Mediator; +using Musify.Application.Tracks.Responses; + +namespace Musify.Application.Tracks.Queries +{ + public record GetTrackStreamQuery(Guid TrackId, Guid UserId) + : IQuery>; +} diff --git a/Application/Tracks/Responses/TrackStreamResponse.cs b/Application/Tracks/Responses/TrackStreamResponse.cs new file mode 100644 index 0000000..c925a8b --- /dev/null +++ b/Application/Tracks/Responses/TrackStreamResponse.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Tracks.Responses +{ + public record TrackStreamResponse( + string ManifestUrl, + string Ticket, + int ExpiresInSeconds); +} diff --git a/Infrastructure/Configuration/StreamTicketConfiguration.cs b/Infrastructure/Configuration/StreamTicketConfiguration.cs new file mode 100644 index 0000000..0773c90 --- /dev/null +++ b/Infrastructure/Configuration/StreamTicketConfiguration.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class StreamTicketConfiguration + { + public const string SectionName = "StreamTicket"; + + [Required] + public required string PrivateKeyPath { get; set; } + + [Required] + public string Audience { get; set; } = "media-gateway"; + + [Required] + public string Issuer { get; set; } = "musify-webapi"; + + [Range(60, 86400)] + public int TicketTtlSeconds { get; set; } = 3600; + } +} diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 379581c..b64cac2 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -26,6 +26,7 @@ + diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 36d9b5c..3597e68 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -71,6 +71,20 @@ namespace Musify.Infrastructure.Services return serviceDescriptors; } + public static IServiceCollection AddStreamTicketService(this IServiceCollection serviceDescriptors, IConfiguration configuration) + { + serviceDescriptors + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetRequiredSection(StreamTicketConfiguration.SectionName)) + .ValidateDataAnnotations(); + + serviceDescriptors.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().Value); + + serviceDescriptors.AddSingleton(); + return serviceDescriptors; + } + public static IServiceCollection AddAudioTranscoder(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors diff --git a/Infrastructure/Services/StreamTicketService.cs b/Infrastructure/Services/StreamTicketService.cs new file mode 100644 index 0000000..2938ed4 --- /dev/null +++ b/Infrastructure/Services/StreamTicketService.cs @@ -0,0 +1,52 @@ +using System.Security.Cryptography; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Musify.Application.Contracts; +using Musify.Infrastructure.Configuration; + +namespace Musify.Infrastructure.Services +{ + public sealed class StreamTicketService : IStreamTicketService, IDisposable + { + private readonly StreamTicketConfiguration configuration; + private readonly RSA rsa; + private readonly SigningCredentials signingCredentials; + private readonly JsonWebTokenHandler tokenHandler = new(); + + public StreamTicketService(StreamTicketConfiguration configuration) + { + this.configuration = configuration; + + rsa = RSA.Create(); + rsa.ImportFromPem(File.ReadAllText(configuration.PrivateKeyPath)); + + var key = new RsaSecurityKey(rsa); + signingCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256); + } + + public StreamTicket IssueTicket(Guid userId, string keyPrefix) + { + var now = DateTimeOffset.UtcNow; + + var descriptor = new SecurityTokenDescriptor + { + Issuer = configuration.Issuer, + Audience = configuration.Audience, + IssuedAt = now.UtcDateTime, + NotBefore = now.UtcDateTime, + Expires = now.AddSeconds(configuration.TicketTtlSeconds).UtcDateTime, + Claims = new Dictionary + { + ["sub"] = userId.ToString(), + ["prefix"] = keyPrefix, + }, + SigningCredentials = signingCredentials, + }; + + var token = tokenHandler.CreateToken(descriptor); + return new StreamTicket(token, configuration.TicketTtlSeconds); + } + + public void Dispose() => rsa.Dispose(); + } +} From 584a8833c24dcc9815babe27c0e0ecb4a2e7378b Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 31 May 2026 18:25:39 +0200 Subject: [PATCH 157/172] Use long --- .../Configuration/PlayListConfiguration.cs | 4 +- .../Configuration/TrackConfiguration.cs | 8 +- Application/Contracts/IStreamTicketService.cs | 2 +- Application/Events/DeletePlayListEvent.cs | 2 +- Application/Events/DeleteTrackEvent.cs | 2 +- .../Commands/CreatePlayListCommand.cs | 2 +- .../Commands/DeletePlayListCommand.cs | 2 +- .../RequestPlayListPictureUploadCommand.cs | 2 +- .../Commands/UpdatePlayListCommand.cs | 2 +- .../Queries/GetPlayListsByUserIdQuery.cs | 2 +- Application/Services/UploadIntentValidator.cs | 4 +- .../Tracks/Commands/CreateTrackCommand.cs | 2 +- .../Tracks/Commands/DeleteTrackCommand.cs | 2 +- .../Commands/RequestTrackUploadUrlsCommand.cs | 2 +- .../Tracks/Queries/GetTrackStreamQuery.cs | 2 +- .../Tracks/Queries/GetTracksByUserIdQuery.cs | 2 +- .../Users/Commands/CreateUserCommand.cs | 2 +- Application/Users/Queries/GetUserByIdQuery.cs | 2 +- .../Responses/UserApplicationResponse.cs | 2 +- Domain/Entities/PlayList.cs | 2 +- Domain/Entities/UploadIntent.cs | 2 +- Domain/Entities/User.cs | 3 +- Domain/Entities/UserHasTrack.cs | 2 +- .../DeletePlayListRoutingSlipBuilder.cs | 2 +- .../Builders/DeleteTrackRoutingSlipBuilder.cs | 2 +- .../20260513230601_FirstMigration.Designer.cs | 332 ----------- ...514095113_AddMassTransitOutbox.Designer.cs | 512 ----------------- .../20260514095113_AddMassTransitOutbox.cs | 142 ----- ...60514161518_AddLifeCycleStatus.Designer.cs | 518 ------------------ .../20260514161518_AddLifeCycleStatus.cs | 40 -- .../20260514172047_AddUploadIntentTable.cs | 54 -- ... 20260530020027_InitialCreate.Designer.cs} | 23 +- ...ion.cs => 20260530020027_InitialCreate.cs} | 168 +++++- .../Migrations/DatabaseModelSnapshot.cs | 19 +- .../Services/StreamTicketService.cs | 2 +- 35 files changed, 216 insertions(+), 1655 deletions(-) delete mode 100644 Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs delete mode 100644 Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs delete mode 100644 Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs delete mode 100644 Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs delete mode 100644 Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs delete mode 100644 Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs rename Infrastructure/Persistence/Migrations/{20260514172047_AddUploadIntentTable.Designer.cs => 20260530020027_InitialCreate.Designer.cs} (97%) rename Infrastructure/Persistence/Migrations/{20260513230601_FirstMigration.cs => 20260530020027_InitialCreate.cs} (50%) diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 9b03e7c..8290d26 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -61,7 +61,7 @@ namespace Musify.Application.Configuration public string BuildOriginalPicturePath(string pictureName) => CombineKey(ParentFolders, OriginalPicturesFolder, pictureName); - public string BuildOriginalPicturePath(Guid userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName); + public string BuildOriginalPicturePath(long userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName); public string BuildSmallPicturePath(string pictureName) => CombineKey(ParentFolders, SmallPicturesFolder, pictureName); @@ -70,7 +70,7 @@ namespace Musify.Application.Configuration public string BuildLargePicturePath(string pictureName) => CombineKey(ParentFolders, LargePicturesFolder, pictureName); // Temp path used in Phase 2 for pre-signed upload keys (moved to final on consumption) - public string BuildTempPicturePath(string tempRootPrefix, Guid userId, string objectName) => + public string BuildTempPicturePath(string tempRootPrefix, long userId, string objectName) => CombineKey(tempRootPrefix, userId.ToString(), ParentFolders, objectName); static string CombineKey(params string[] segments) diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index 10e2eec..4740944 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -71,7 +71,7 @@ namespace Musify.Application.Configuration public string BuildOriginalPicturePath(string pictureName) => CombineKey(OriginalPicturesPath, pictureName); - public string BuildOriginalPicturePath(Guid userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), OriginalPicturesPath, pictureName); + public string BuildOriginalPicturePath(long userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), OriginalPicturesPath, pictureName); public string BuildSmallPicturePath(string pictureName) => CombineKey(SmallPicturesPath, pictureName); @@ -81,15 +81,15 @@ namespace Musify.Application.Configuration public string BuildOriginalAudioPath(string audioName) => CombineKey(OriginalAudiosPath, audioName); - public string BuildOriginalAudioPath(Guid userId, string audioName) => CombineKey(UploadsFolder, userId.ToString(), OriginalAudiosPath, audioName); + public string BuildOriginalAudioPath(long userId, string audioName) => CombineKey(UploadsFolder, userId.ToString(), OriginalAudiosPath, audioName); public string BuildProcessedAudioPath(string audioName) => CombineKey(ProcessedAudiosPath, audioName); // Temp paths used in Phase 2 for pre-signed upload keys (moved to final on consumption) - public string BuildTempPicturePath(string tempRootPrefix, Guid userId, string objectName) => + public string BuildTempPicturePath(string tempRootPrefix, long userId, string objectName) => CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); - public string BuildTempAudioPath(string tempRootPrefix, Guid userId, string objectName) => + public string BuildTempAudioPath(string tempRootPrefix, long userId, string objectName) => CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); static string CombineKey(params string[] segments) diff --git a/Application/Contracts/IStreamTicketService.cs b/Application/Contracts/IStreamTicketService.cs index 21b6f50..f72282b 100644 --- a/Application/Contracts/IStreamTicketService.cs +++ b/Application/Contracts/IStreamTicketService.cs @@ -2,7 +2,7 @@ namespace Musify.Application.Contracts { public interface IStreamTicketService { - StreamTicket IssueTicket(Guid userId, string keyPrefix); + StreamTicket IssueTicket(long userId, string keyPrefix); } public record StreamTicket(string Token, int ExpiresInSeconds); diff --git a/Application/Events/DeletePlayListEvent.cs b/Application/Events/DeletePlayListEvent.cs index 0c068df..06a7a2c 100644 --- a/Application/Events/DeletePlayListEvent.cs +++ b/Application/Events/DeletePlayListEvent.cs @@ -2,5 +2,5 @@ namespace Musify.Application.Events { public record DeletePlayListEvent( Guid PlayListId, - Guid UserId); + long UserId); } diff --git a/Application/Events/DeleteTrackEvent.cs b/Application/Events/DeleteTrackEvent.cs index 2f85a6a..8fb6923 100644 --- a/Application/Events/DeleteTrackEvent.cs +++ b/Application/Events/DeleteTrackEvent.cs @@ -2,5 +2,5 @@ namespace Musify.Application.Events { public record DeleteTrackEvent( Guid TrackId, - Guid UserId); + long UserId); } diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index fa7af4e..95b3788 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -5,7 +5,7 @@ using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Commands { public record CreatePlayListCommand( - Guid UserId, + long UserId, string Name, string Description, Guid? PictureIntentId) diff --git a/Application/PlayLists/Commands/DeletePlayListCommand.cs b/Application/PlayLists/Commands/DeletePlayListCommand.cs index c695479..c1860c5 100644 --- a/Application/PlayLists/Commands/DeletePlayListCommand.cs +++ b/Application/PlayLists/Commands/DeletePlayListCommand.cs @@ -3,6 +3,6 @@ using Mediator; namespace Musify.Application.PlayLists.Commands { - public record DeletePlayListCommand(Guid UserId, Guid PlayListId) + public record DeletePlayListCommand(long UserId, Guid PlayListId) : ICommand; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs b/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs index fc14a80..7f8a827 100644 --- a/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs +++ b/Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs @@ -5,7 +5,7 @@ using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Commands { public record RequestPlayListPictureUploadCommand( - Guid UserId, + long UserId, string FileType, string ContentType, long? ExpectedSizeBytes = null) diff --git a/Application/PlayLists/Commands/UpdatePlayListCommand.cs b/Application/PlayLists/Commands/UpdatePlayListCommand.cs index f6795c2..a2bfe18 100644 --- a/Application/PlayLists/Commands/UpdatePlayListCommand.cs +++ b/Application/PlayLists/Commands/UpdatePlayListCommand.cs @@ -5,7 +5,7 @@ using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Commands { public record UpdatePlayListCommand( - Guid UserId, + long UserId, Guid PlayListId, string? NewName, string? NewDescription, diff --git a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index c2e9da5..a4a11f5 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -6,7 +6,7 @@ using Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Queries { public record GetPlayListsByUserIdQuery( - Guid UserId, + long UserId, string? Name = null, int PageNumber = 1, int PageSize = 10) diff --git a/Application/Services/UploadIntentValidator.cs b/Application/Services/UploadIntentValidator.cs index 380fbb7..c3efaa5 100644 --- a/Application/Services/UploadIntentValidator.cs +++ b/Application/Services/UploadIntentValidator.cs @@ -13,7 +13,7 @@ public sealed class UploadIntentValidator( { public async Task CheckQuotaAsync( UploadIntentConfiguration config, - Guid userId, + long userId, long requiredBytes, int requiredIntentCount, CancellationToken cancellationToken) @@ -36,7 +36,7 @@ public sealed class UploadIntentValidator( public async Task> ValidateAndLoadAsync( UploadIntentConfiguration config, Guid intentId, - Guid userId, + long userId, CancellationToken cancellationToken) { var intent = await database.UploadIntents diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index bda3529..b5fd8a3 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -5,7 +5,7 @@ using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Commands { public record CreateTrackCommand( - Guid UserId, + long UserId, string Title, Guid PictureIntentId, Guid AudioIntentId) diff --git a/Application/Tracks/Commands/DeleteTrackCommand.cs b/Application/Tracks/Commands/DeleteTrackCommand.cs index 6d7615f..619869e 100644 --- a/Application/Tracks/Commands/DeleteTrackCommand.cs +++ b/Application/Tracks/Commands/DeleteTrackCommand.cs @@ -4,7 +4,7 @@ using Mediator; namespace Musify.Application.Tracks.Commands { public record DeleteTrackCommand( - Guid UserId, + long UserId, Guid TrackId) : ICommand; } \ No newline at end of file diff --git a/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs b/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs index 46b7151..ea82fbb 100644 --- a/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs +++ b/Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs @@ -5,7 +5,7 @@ using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Commands { public record RequestTrackUploadUrlsCommand( - Guid UserId, + long UserId, string PictureFileType, string PictureContentType, string AudioFileType, diff --git a/Application/Tracks/Queries/GetTrackStreamQuery.cs b/Application/Tracks/Queries/GetTrackStreamQuery.cs index 66733ad..03286ae 100644 --- a/Application/Tracks/Queries/GetTrackStreamQuery.cs +++ b/Application/Tracks/Queries/GetTrackStreamQuery.cs @@ -4,6 +4,6 @@ using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries { - public record GetTrackStreamQuery(Guid TrackId, Guid UserId) + public record GetTrackStreamQuery(Guid TrackId, long UserId) : IQuery>; } diff --git a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs index 00a00f6..6e389dd 100644 --- a/Application/Tracks/Queries/GetTracksByUserIdQuery.cs +++ b/Application/Tracks/Queries/GetTracksByUserIdQuery.cs @@ -6,7 +6,7 @@ using Musify.Application.Tracks.Responses; namespace Musify.Application.Tracks.Queries { public record GetTracksByUserIdQuery( - Guid UserId, + long UserId, string? Name = null, int PageNumber = 1, int PageSize = 10) diff --git a/Application/Users/Commands/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs index fef2150..6415e31 100644 --- a/Application/Users/Commands/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -5,7 +5,7 @@ using Musify.Domain.Entities; namespace Musify.Application.Users.Commands { - public record CreateUserCommand(Guid Id, + public record CreateUserCommand(long Id, string Name, string? FirstName, string? SecondName) diff --git a/Application/Users/Queries/GetUserByIdQuery.cs b/Application/Users/Queries/GetUserByIdQuery.cs index d23ead1..f17c5e6 100644 --- a/Application/Users/Queries/GetUserByIdQuery.cs +++ b/Application/Users/Queries/GetUserByIdQuery.cs @@ -4,6 +4,6 @@ using Musify.Application.Users.Responses; namespace Musify.Application.Users.Queries { - public record GetUserByIdQuery(Guid Id) + public record GetUserByIdQuery(long Id) : IQuery >; } \ No newline at end of file diff --git a/Application/Users/Responses/UserApplicationResponse.cs b/Application/Users/Responses/UserApplicationResponse.cs index 2c98ec2..e42616c 100644 --- a/Application/Users/Responses/UserApplicationResponse.cs +++ b/Application/Users/Responses/UserApplicationResponse.cs @@ -3,7 +3,7 @@ namespace Musify.Application.Users.Responses { public record UserApplicationResponse( - Guid Id, + long Id, string Name, string? FirstName, string? SecondName, diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 6e6c7a8..8789adb 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -13,7 +13,7 @@ namespace Musify.Domain.Entities public Guid Id { get; set; } = Guid.NewGuid(); [Required] - public required Guid UserId { get; set; } + public required long UserId { get; set; } [Required] [MaxLength(50)] diff --git a/Domain/Entities/UploadIntent.cs b/Domain/Entities/UploadIntent.cs index 11a44f3..d4f772b 100644 --- a/Domain/Entities/UploadIntent.cs +++ b/Domain/Entities/UploadIntent.cs @@ -12,7 +12,7 @@ namespace Musify.Domain.Entities public Guid Id { get; set; } = Guid.NewGuid(); [Required] - public required Guid UserId { get; set; } + public required long UserId { get; set; } [Required, MaxLength(128)] public required string Bucket { get; set; } diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index c0be246..2d2a0b0 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -7,7 +7,8 @@ namespace Musify.Domain.Entities public class User { [Key] - public required Guid Id { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public required long Id { get; set; } [Required] [MaxLength(48)] diff --git a/Domain/Entities/UserHasTrack.cs b/Domain/Entities/UserHasTrack.cs index fbf0d07..f2e72b3 100644 --- a/Domain/Entities/UserHasTrack.cs +++ b/Domain/Entities/UserHasTrack.cs @@ -10,7 +10,7 @@ namespace Musify.Domain.Entities public Guid Id { get; set; } = Guid.NewGuid(); [Required] - public required Guid UserId { get; set; } + public required long UserId { get; set; } [Required] public required Guid TrackId { get; set; } diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs index efe4837..4390061 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs @@ -17,7 +17,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { public async Task BuildAsync( Guid playListId, - Guid userId, + long userId, Guid? correlationId, CancellationToken cancellationToken) { diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index f6ed175..2c6a0f2 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -17,7 +17,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { public async Task BuildAsync( Guid trackId, - Guid userId, + long userId, Guid? correlationId, CancellationToken cancellationToken) { diff --git a/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs b/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs deleted file mode 100644 index 30ca380..0000000 --- a/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.Designer.cs +++ /dev/null @@ -1,332 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Musify.Infrastructure.Persistence; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(Database))] - [Migration("20260513230601_FirstMigration")] - partial class FirstMigration - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LargePictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MediumPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SmallPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("PlayLists"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PlayListId") - .HasColumnType("uuid"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("PlayListId"); - - b.HasIndex("TrackId"); - - b.ToTable("PlayListHasTrack"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudioFolderName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AudioTranscodeProcessingStatus") - .HasColumnType("integer"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LargePictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LastRetryAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediumPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OriginalAudioName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("PicturesProcessingStatus") - .HasColumnType("integer"); - - b.Property("RetryCount") - .HasColumnType("integer"); - - b.Property("SmallPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Upload", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Bucket") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("State") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("Upload"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FirstName") - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("SecondName") - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.HasIndex("UserId"); - - b.ToTable("UserHasTrack"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("PlayLists") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") - .WithMany("PlayListTracks") - .HasForeignKey("PlayListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("PlayListTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("PlayList"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("UserTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("UserTracks") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Track"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Navigation("PlayListTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Navigation("PlayListTracks"); - - b.Navigation("UserTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Navigation("PlayLists"); - - b.Navigation("UserTracks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs b/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs deleted file mode 100644 index ce2d9a3..0000000 --- a/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.Designer.cs +++ /dev/null @@ -1,512 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Musify.Infrastructure.Persistence; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(Database))] - [Migration("20260514095113_AddMassTransitOutbox")] - partial class AddMassTransitOutbox - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Consumed") - .HasColumnType("timestamp with time zone"); - - b.Property("ConsumerId") - .HasColumnType("uuid"); - - b.Property("Delivered") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone"); - - b.Property("LastSequenceNumber") - .HasColumnType("bigint"); - - b.Property("LockId") - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReceiveCount") - .HasColumnType("integer"); - - b.Property("Received") - .HasColumnType("timestamp with time zone"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("Delivered"); - - b.ToTable("InboxState"); - }); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => - { - b.Property("SequenceNumber") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); - - b.Property("Body") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContentType") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ConversationId") - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("DestinationAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EnqueueTime") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone"); - - b.Property("FaultAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Headers") - .HasColumnType("text"); - - b.Property("InboxConsumerId") - .HasColumnType("uuid"); - - b.Property("InboxMessageId") - .HasColumnType("uuid"); - - b.Property("InitiatorId") - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("MessageType") - .IsRequired() - .HasColumnType("text"); - - b.Property("OutboxId") - .HasColumnType("uuid"); - - b.Property("Properties") - .HasColumnType("text"); - - b.Property("RequestId") - .HasColumnType("uuid"); - - b.Property("ResponseAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("SentTime") - .HasColumnType("timestamp with time zone"); - - b.Property("SourceAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("SequenceNumber"); - - b.HasIndex("EnqueueTime"); - - b.HasIndex("ExpirationTime"); - - b.HasIndex("OutboxId", "SequenceNumber") - .IsUnique(); - - b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") - .IsUnique(); - - b.ToTable("OutboxMessage"); - }); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => - { - b.Property("OutboxId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Created") - .HasColumnType("timestamp with time zone"); - - b.Property("Delivered") - .HasColumnType("timestamp with time zone"); - - b.Property("LastSequenceNumber") - .HasColumnType("bigint"); - - b.Property("LockId") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.HasKey("OutboxId"); - - b.HasIndex("Created"); - - b.ToTable("OutboxState"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LargePictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("MediumPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SmallPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("PlayLists"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PlayListId") - .HasColumnType("uuid"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("PlayListId"); - - b.HasIndex("TrackId"); - - b.ToTable("PlayListHasTrack"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudioFolderName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AudioTranscodeProcessingStatus") - .HasColumnType("integer"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LargePictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LastRetryAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MediumPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OriginalAudioName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("PicturesProcessingStatus") - .HasColumnType("integer"); - - b.Property("RetryCount") - .HasColumnType("integer"); - - b.Property("SmallPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Upload", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Bucket") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("State") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("Upload"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FirstName") - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("SecondName") - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.HasIndex("UserId"); - - b.ToTable("UserHasTrack"); - }); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => - { - b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) - .WithMany() - .HasForeignKey("OutboxId"); - - b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) - .WithMany() - .HasForeignKey("InboxMessageId", "InboxConsumerId") - .HasPrincipalKey("MessageId", "ConsumerId"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("PlayLists") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") - .WithMany("PlayListTracks") - .HasForeignKey("PlayListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("PlayListTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("PlayList"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("UserTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("UserTracks") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Track"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Navigation("PlayListTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Navigation("PlayListTracks"); - - b.Navigation("UserTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Navigation("PlayLists"); - - b.Navigation("UserTracks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs b/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs deleted file mode 100644 index c4074bd..0000000 --- a/Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddMassTransitOutbox : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "InboxState", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - MessageId = table.Column(type: "uuid", nullable: false), - ConsumerId = table.Column(type: "uuid", nullable: false), - LockId = table.Column(type: "uuid", nullable: false), - RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), - Received = table.Column(type: "timestamp with time zone", nullable: false), - ReceiveCount = table.Column(type: "integer", nullable: false), - ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true), - Consumed = table.Column(type: "timestamp with time zone", nullable: true), - Delivered = table.Column(type: "timestamp with time zone", nullable: true), - LastSequenceNumber = table.Column(type: "bigint", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_InboxState", x => x.Id); - table.UniqueConstraint("AK_InboxState_MessageId_ConsumerId", x => new { x.MessageId, x.ConsumerId }); - }); - - migrationBuilder.CreateTable( - name: "OutboxState", - columns: table => new - { - OutboxId = table.Column(type: "uuid", nullable: false), - LockId = table.Column(type: "uuid", nullable: false), - RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), - Created = table.Column(type: "timestamp with time zone", nullable: false), - Delivered = table.Column(type: "timestamp with time zone", nullable: true), - LastSequenceNumber = table.Column(type: "bigint", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_OutboxState", x => x.OutboxId); - }); - - migrationBuilder.CreateTable( - name: "OutboxMessage", - columns: table => new - { - SequenceNumber = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - EnqueueTime = table.Column(type: "timestamp with time zone", nullable: true), - SentTime = table.Column(type: "timestamp with time zone", nullable: false), - Headers = table.Column(type: "text", nullable: true), - Properties = table.Column(type: "text", nullable: true), - InboxMessageId = table.Column(type: "uuid", nullable: true), - InboxConsumerId = table.Column(type: "uuid", nullable: true), - OutboxId = table.Column(type: "uuid", nullable: true), - MessageId = table.Column(type: "uuid", nullable: false), - ContentType = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - MessageType = table.Column(type: "text", nullable: false), - Body = table.Column(type: "text", nullable: false), - ConversationId = table.Column(type: "uuid", nullable: true), - CorrelationId = table.Column(type: "uuid", nullable: true), - InitiatorId = table.Column(type: "uuid", nullable: true), - RequestId = table.Column(type: "uuid", nullable: true), - SourceAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - DestinationAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - ResponseAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - FaultAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_OutboxMessage", x => x.SequenceNumber); - table.ForeignKey( - name: "FK_OutboxMessage_InboxState_InboxMessageId_InboxConsumerId", - columns: x => new { x.InboxMessageId, x.InboxConsumerId }, - principalTable: "InboxState", - principalColumns: new[] { "MessageId", "ConsumerId" }); - table.ForeignKey( - name: "FK_OutboxMessage_OutboxState_OutboxId", - column: x => x.OutboxId, - principalTable: "OutboxState", - principalColumn: "OutboxId"); - }); - - migrationBuilder.CreateIndex( - name: "IX_InboxState_Delivered", - table: "InboxState", - column: "Delivered"); - - migrationBuilder.CreateIndex( - name: "IX_OutboxMessage_EnqueueTime", - table: "OutboxMessage", - column: "EnqueueTime"); - - migrationBuilder.CreateIndex( - name: "IX_OutboxMessage_ExpirationTime", - table: "OutboxMessage", - column: "ExpirationTime"); - - migrationBuilder.CreateIndex( - name: "IX_OutboxMessage_InboxMessageId_InboxConsumerId_SequenceNumber", - table: "OutboxMessage", - columns: new[] { "InboxMessageId", "InboxConsumerId", "SequenceNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_OutboxMessage_OutboxId_SequenceNumber", - table: "OutboxMessage", - columns: new[] { "OutboxId", "SequenceNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_OutboxState_Created", - table: "OutboxState", - column: "Created"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "OutboxMessage"); - - migrationBuilder.DropTable( - name: "InboxState"); - - migrationBuilder.DropTable( - name: "OutboxState"); - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs b/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs deleted file mode 100644 index 98c3d87..0000000 --- a/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.Designer.cs +++ /dev/null @@ -1,518 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Musify.Infrastructure.Persistence; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(Database))] - [Migration("20260514161518_AddLifeCycleStatus")] - partial class AddLifeCycleStatus - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Consumed") - .HasColumnType("timestamp with time zone"); - - b.Property("ConsumerId") - .HasColumnType("uuid"); - - b.Property("Delivered") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone"); - - b.Property("LastSequenceNumber") - .HasColumnType("bigint"); - - b.Property("LockId") - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReceiveCount") - .HasColumnType("integer"); - - b.Property("Received") - .HasColumnType("timestamp with time zone"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("Delivered"); - - b.ToTable("InboxState"); - }); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => - { - b.Property("SequenceNumber") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); - - b.Property("Body") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContentType") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ConversationId") - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("DestinationAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EnqueueTime") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone"); - - b.Property("FaultAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Headers") - .HasColumnType("text"); - - b.Property("InboxConsumerId") - .HasColumnType("uuid"); - - b.Property("InboxMessageId") - .HasColumnType("uuid"); - - b.Property("InitiatorId") - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("MessageType") - .IsRequired() - .HasColumnType("text"); - - b.Property("OutboxId") - .HasColumnType("uuid"); - - b.Property("Properties") - .HasColumnType("text"); - - b.Property("RequestId") - .HasColumnType("uuid"); - - b.Property("ResponseAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("SentTime") - .HasColumnType("timestamp with time zone"); - - b.Property("SourceAddress") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("SequenceNumber"); - - b.HasIndex("EnqueueTime"); - - b.HasIndex("ExpirationTime"); - - b.HasIndex("OutboxId", "SequenceNumber") - .IsUnique(); - - b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") - .IsUnique(); - - b.ToTable("OutboxMessage"); - }); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => - { - b.Property("OutboxId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Created") - .HasColumnType("timestamp with time zone"); - - b.Property("Delivered") - .HasColumnType("timestamp with time zone"); - - b.Property("LastSequenceNumber") - .HasColumnType("bigint"); - - b.Property("LockId") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea"); - - b.HasKey("OutboxId"); - - b.HasIndex("Created"); - - b.ToTable("OutboxState"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LargePictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LifeCycleStatus") - .HasColumnType("integer"); - - b.Property("MediumPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SmallPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("PlayLists"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PlayListId") - .HasColumnType("uuid"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("PlayListId"); - - b.HasIndex("TrackId"); - - b.ToTable("PlayListHasTrack"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudioFolderName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AudioTranscodeProcessingStatus") - .HasColumnType("integer"); - - b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LargePictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LastRetryAt") - .HasColumnType("timestamp with time zone"); - - b.Property("LifeCycleStatus") - .HasColumnType("integer"); - - b.Property("MediumPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NormalizedTitle") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OriginalAudioName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("PicturesProcessingStatus") - .HasColumnType("integer"); - - b.Property("RetryCount") - .HasColumnType("integer"); - - b.Property("SmallPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("UpdatedDate") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Upload", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Bucket") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedTime") - .HasColumnType("timestamp with time zone"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("State") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("Upload"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FirstName") - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("NormalizedName") - .IsRequired() - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("SecondName") - .HasMaxLength(48) - .HasColumnType("character varying(48)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("TrackId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.HasIndex("UserId"); - - b.ToTable("UserHasTrack"); - }); - - modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => - { - b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) - .WithMany() - .HasForeignKey("OutboxId"); - - b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) - .WithMany() - .HasForeignKey("InboxMessageId", "InboxConsumerId") - .HasPrincipalKey("MessageId", "ConsumerId"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("PlayLists") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") - .WithMany("PlayListTracks") - .HasForeignKey("PlayListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("PlayListTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("PlayList"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => - { - b.HasOne("Musify.Domain.Entities.Track", "Track") - .WithMany("UserTracks") - .HasForeignKey("TrackId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Musify.Domain.Entities.User", "User") - .WithMany("UserTracks") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Track"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => - { - b.Navigation("PlayListTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.Track", b => - { - b.Navigation("PlayListTracks"); - - b.Navigation("UserTracks"); - }); - - modelBuilder.Entity("Musify.Domain.Entities.User", b => - { - b.Navigation("PlayLists"); - - b.Navigation("UserTracks"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs b/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs deleted file mode 100644 index 68a1b0c..0000000 --- a/Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddLifeCycleStatus : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LifeCycleStatus", - table: "Tracks", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "LifeCycleStatus", - table: "PlayLists", - type: "integer", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LifeCycleStatus", - table: "Tracks"); - - migrationBuilder.DropColumn( - name: "LifeCycleStatus", - table: "PlayLists"); - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs b/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs deleted file mode 100644 index 6d39875..0000000 --- a/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Musify.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddUploadIntentTable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "UploadIntents", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Bucket = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - Key = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - ObjectName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - ContentType = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - ExpectedSizeBytes = table.Column(type: "bigint", nullable: true), - Purpose = table.Column(type: "integer", nullable: false), - Status = table.Column(type: "integer", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UploadIntents", x => x.Id); - table.ForeignKey( - name: "FK_UploadIntents_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_UploadIntents_UserId", - table: "UploadIntents", - column: "UserId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "UploadIntents"); - } - } -} diff --git a/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs similarity index 97% rename from Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs rename to Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs index a27e8d3..1b193c6 100644 --- a/Infrastructure/Persistence/Migrations/20260514172047_AddUploadIntentTable.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs @@ -12,15 +12,15 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Musify.Infrastructure.Persistence.Migrations { [DbContext(typeof(Database))] - [Migration("20260514172047_AddUploadIntentTable")] - partial class AddUploadIntentTable + [Migration("20260530020027_InitialCreate")] + partial class InitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -243,8 +243,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("UpdatedDate") .HasColumnType("timestamp with time zone"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); @@ -418,8 +418,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("Status") .HasColumnType("integer"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); @@ -430,9 +430,8 @@ namespace Musify.Infrastructure.Persistence.Migrations modelBuilder.Entity("Musify.Domain.Entities.User", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + b.Property("Id") + .HasColumnType("bigint"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -472,8 +471,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("TrackId") .HasColumnType("uuid"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); diff --git a/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.cs b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs similarity index 50% rename from Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.cs rename to Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs index 17e8882..ffa9382 100644 --- a/Infrastructure/Persistence/Migrations/20260513230601_FirstMigration.cs +++ b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs @@ -1,16 +1,56 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Musify.Infrastructure.Persistence.Migrations { /// - public partial class FirstMigration : Migration + public partial class InitialCreate : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.CreateTable( + name: "InboxState", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MessageId = table.Column(type: "uuid", nullable: false), + ConsumerId = table.Column(type: "uuid", nullable: false), + LockId = table.Column(type: "uuid", nullable: false), + RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), + Received = table.Column(type: "timestamp with time zone", nullable: false), + ReceiveCount = table.Column(type: "integer", nullable: false), + ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true), + Consumed = table.Column(type: "timestamp with time zone", nullable: true), + Delivered = table.Column(type: "timestamp with time zone", nullable: true), + LastSequenceNumber = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InboxState", x => x.Id); + table.UniqueConstraint("AK_InboxState_MessageId_ConsumerId", x => new { x.MessageId, x.ConsumerId }); + }); + + migrationBuilder.CreateTable( + name: "OutboxState", + columns: table => new + { + OutboxId = table.Column(type: "uuid", nullable: false), + LockId = table.Column(type: "uuid", nullable: false), + RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), + Created = table.Column(type: "timestamp with time zone", nullable: false), + Delivered = table.Column(type: "timestamp with time zone", nullable: true), + LastSequenceNumber = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxState", x => x.OutboxId); + }); + migrationBuilder.CreateTable( name: "Tracks", columns: table => new @@ -26,6 +66,7 @@ namespace Musify.Infrastructure.Persistence.Migrations AudioFolderName = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), PicturesProcessingStatus = table.Column(type: "integer", nullable: false), AudioTranscodeProcessingStatus = table.Column(type: "integer", nullable: false), + LifeCycleStatus = table.Column(type: "integer", nullable: false), RetryCount = table.Column(type: "integer", nullable: false), LastRetryAt = table.Column(type: "timestamp with time zone", nullable: true), CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), @@ -56,7 +97,7 @@ namespace Musify.Infrastructure.Persistence.Migrations name: "User", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), + Id = table.Column(type: "bigint", nullable: false), Name = table.Column(type: "character varying(48)", maxLength: 48, nullable: false), NormalizedName = table.Column(type: "character varying(48)", maxLength: 48, nullable: false), FirstName = table.Column(type: "character varying(48)", maxLength: 48, nullable: true), @@ -69,12 +110,54 @@ namespace Musify.Infrastructure.Persistence.Migrations table.PrimaryKey("PK_User", x => x.Id); }); + migrationBuilder.CreateTable( + name: "OutboxMessage", + columns: table => new + { + SequenceNumber = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EnqueueTime = table.Column(type: "timestamp with time zone", nullable: true), + SentTime = table.Column(type: "timestamp with time zone", nullable: false), + Headers = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + InboxMessageId = table.Column(type: "uuid", nullable: true), + InboxConsumerId = table.Column(type: "uuid", nullable: true), + OutboxId = table.Column(type: "uuid", nullable: true), + MessageId = table.Column(type: "uuid", nullable: false), + ContentType = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + MessageType = table.Column(type: "text", nullable: false), + Body = table.Column(type: "text", nullable: false), + ConversationId = table.Column(type: "uuid", nullable: true), + CorrelationId = table.Column(type: "uuid", nullable: true), + InitiatorId = table.Column(type: "uuid", nullable: true), + RequestId = table.Column(type: "uuid", nullable: true), + SourceAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + DestinationAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ResponseAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + FaultAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessage", x => x.SequenceNumber); + table.ForeignKey( + name: "FK_OutboxMessage_InboxState_InboxMessageId_InboxConsumerId", + columns: x => new { x.InboxMessageId, x.InboxConsumerId }, + principalTable: "InboxState", + principalColumns: new[] { "MessageId", "ConsumerId" }); + table.ForeignKey( + name: "FK_OutboxMessage_OutboxState_OutboxId", + column: x => x.OutboxId, + principalTable: "OutboxState", + principalColumn: "OutboxId"); + }); + migrationBuilder.CreateTable( name: "PlayLists", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), NormalizedName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), Description = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), @@ -82,6 +165,7 @@ namespace Musify.Infrastructure.Persistence.Migrations SmallPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), MediumPictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), LargePictureName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + LifeCycleStatus = table.Column(type: "integer", nullable: false), CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -96,12 +180,39 @@ namespace Musify.Infrastructure.Persistence.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "UploadIntents", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + Bucket = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Key = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + ObjectName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ContentType = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ExpectedSizeBytes = table.Column(type: "bigint", nullable: true), + Purpose = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UploadIntents", x => x.Id); + table.ForeignKey( + name: "FK_UploadIntents_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "UserHasTrack", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), TrackId = table.Column(type: "uuid", nullable: false) }, constraints: table => @@ -148,6 +259,38 @@ namespace Musify.Infrastructure.Persistence.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateIndex( + name: "IX_InboxState_Delivered", + table: "InboxState", + column: "Delivered"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_EnqueueTime", + table: "OutboxMessage", + column: "EnqueueTime"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_ExpirationTime", + table: "OutboxMessage", + column: "ExpirationTime"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_InboxMessageId_InboxConsumerId_SequenceNumber", + table: "OutboxMessage", + columns: new[] { "InboxMessageId", "InboxConsumerId", "SequenceNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_OutboxId_SequenceNumber", + table: "OutboxMessage", + columns: new[] { "OutboxId", "SequenceNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OutboxState_Created", + table: "OutboxState", + column: "Created"); + migrationBuilder.CreateIndex( name: "IX_PlayListHasTrack_PlayListId", table: "PlayListHasTrack", @@ -163,6 +306,11 @@ namespace Musify.Infrastructure.Persistence.Migrations table: "PlayLists", column: "UserId"); + migrationBuilder.CreateIndex( + name: "IX_UploadIntents_UserId", + table: "UploadIntents", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_UserHasTrack_TrackId", table: "UserHasTrack", @@ -177,15 +325,27 @@ namespace Musify.Infrastructure.Persistence.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "OutboxMessage"); + migrationBuilder.DropTable( name: "PlayListHasTrack"); migrationBuilder.DropTable( name: "Upload"); + migrationBuilder.DropTable( + name: "UploadIntents"); + migrationBuilder.DropTable( name: "UserHasTrack"); + migrationBuilder.DropTable( + name: "InboxState"); + + migrationBuilder.DropTable( + name: "OutboxState"); + migrationBuilder.DropTable( name: "PlayLists"); diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 292cfc5..3293df5 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Musify.Infrastructure.Persistence.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -240,8 +240,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("UpdatedDate") .HasColumnType("timestamp with time zone"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); @@ -415,8 +415,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("Status") .HasColumnType("integer"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); @@ -427,9 +427,8 @@ namespace Musify.Infrastructure.Persistence.Migrations modelBuilder.Entity("Musify.Domain.Entities.User", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + b.Property("Id") + .HasColumnType("bigint"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -469,8 +468,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("TrackId") .HasColumnType("uuid"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); diff --git a/Infrastructure/Services/StreamTicketService.cs b/Infrastructure/Services/StreamTicketService.cs index 2938ed4..10f26d4 100644 --- a/Infrastructure/Services/StreamTicketService.cs +++ b/Infrastructure/Services/StreamTicketService.cs @@ -24,7 +24,7 @@ namespace Musify.Infrastructure.Services signingCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256); } - public StreamTicket IssueTicket(Guid userId, string keyPrefix) + public StreamTicket IssueTicket(long userId, string keyPrefix) { var now = DateTimeOffset.UtcNow; From ba9b744e5d0401fc7bfcbe7929a9ba62988bc7fe Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 31 May 2026 19:25:15 +0200 Subject: [PATCH 158/172] Fix audio DASH workflow: dispose source stream before delete and store S3 folder key as AudioFolderName --- .../Activities/Audio/TranscodeDashAudioActivity.cs | 12 +++++++----- .../Activities/Audio/UpdateTrackAudioActivity.cs | 13 ++----------- .../Arguments/UpdateTrackAudioArguments.cs | 2 +- .../Builders/AudioWorkflowRoutingSlipBuilder.cs | 2 +- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs index 3e8f6a8..5305cb0 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs @@ -38,11 +38,13 @@ namespace Musify.Infrastructure.MassTransit.Activities try { - await using var fileStream = File.OpenRead(sourceFilePath); - await audioTranscoder.TranscodeToDashAsync( - fileStream, - workingDirectory, - executeContext.CancellationToken); + await using (var fileStream = File.OpenRead(sourceFilePath)) + { + await audioTranscoder.TranscodeToDashAsync( + fileStream, + workingDirectory, + executeContext.CancellationToken); + } File.Delete(sourceFilePath); return executeContext.Completed(); diff --git a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 82d4d0b..befa597 100644 --- a/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -31,17 +31,8 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio throw; } - string? audioFolderKey; - try - { - audioFolderKey = executeContext.GetVariable(executeContext.Arguments.AudioFolderKeyVariable); - logger.LogDebug("Resolved audio folder key: {AudioFolderKey}", audioFolderKey); - } - catch (Exception exception) - { - logger.LogWarning(exception, "Audio folder key variable could not be resolved for track {TrackId}", track.Id); - return executeContext.Completed(); - } + var audioFolderKey = executeContext.Arguments.AudioFolderKey; + logger.LogDebug("Resolved audio folder key: {AudioFolderKey}", audioFolderKey); if (string.IsNullOrWhiteSpace(audioFolderKey)) { diff --git a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs index aabb5da..10edb2b 100644 --- a/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/UpdateTrackAudioArguments.cs @@ -2,5 +2,5 @@ { public record UpdateTrackAudioArguments( Guid TrackId, - string AudioFolderKeyVariable); + string AudioFolderKey); } \ No newline at end of file diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index d186353..6f2b0b9 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -58,7 +58,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders EndpointHelper.BuildExecuteActivityUri(UpdateTrackAudioActivity.ExecuteEndpointName), new UpdateTrackAudioArguments( message.TrackId, - RoutingSlipVariableNames.Audio.TranscodedDirectory)); + message.DestinationFolderKey)); return routingSlipBuilder; } From 539899741d7b9225eaa39a6add56b7cf3bbbd330 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 31 May 2026 19:28:08 +0200 Subject: [PATCH 159/172] Remove unused usings --- Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs | 1 - Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs | 1 - Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs | 1 - Application/Tracks/Handlers/DeleteTrackCommandHandler.cs | 1 - .../Activities/PlayLists/MarkPlayListAsRemovingActivity.cs | 1 - .../Activities/Tracks/MarkTrackAsRemovingActivity.cs | 1 - .../Activities/UploadIntents/ConsumeUploadIntentsActivity.cs | 1 - .../PublishPlayListPictureProcessingEventArguments.cs | 1 - .../Arguments/PublishTrackProcessingEventsArguments.cs | 1 - .../Persistence/Migrations/20260530020027_InitialCreate.cs | 3 +-- 10 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 38f43c4..9405a36 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -10,7 +10,6 @@ using Musify.Application.PlayLists.Responses; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index a27da58..459066f 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts; using Musify.Application.Events; using Musify.Application.PlayLists.Commands; -using Musify.Domain.Entities; using Musify.Domain.ValueObjects; namespace Musify.Application.PlayLists.Handlers diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index c099d5e..e8b9317 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -10,7 +10,6 @@ using Musify.Application.PlayLists.Responses; using Musify.Application.Extensions; using Musify.Application.Services; using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; using Musify.Application.Contracts; namespace Musify.Application.PlayLists.Handlers diff --git a/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs b/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs index eb1ff77..a59e79b 100644 --- a/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs +++ b/Application/Tracks/Handlers/DeleteTrackCommandHandler.cs @@ -3,7 +3,6 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Tracks.Commands; -using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Application.Events; using Musify.Application.Contracts; diff --git a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs index b68f23a..e9a8c36 100644 --- a/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs +++ b/Infrastructure/MassTransit/Activities/PlayLists/MarkPlayListAsRemovingActivity.cs @@ -2,7 +2,6 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Infrastructure.MassTransit.Arguments; -using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Application.Contracts; diff --git a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs index 2256f42..f54b42e 100644 --- a/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs +++ b/Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsRemovingActivity.cs @@ -2,7 +2,6 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Infrastructure.MassTransit.Arguments; -using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Application.Contracts; diff --git a/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs b/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs index 2f33f2b..1582d32 100644 --- a/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs +++ b/Infrastructure/MassTransit/Activities/UploadIntents/ConsumeUploadIntentsActivity.cs @@ -2,7 +2,6 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts; -using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; diff --git a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs index ce28509..5721ea8 100644 --- a/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs +++ b/Infrastructure/MassTransit/Arguments/PublishPlayListPictureProcessingEventArguments.cs @@ -1,5 +1,4 @@ using Musify.Application.Shared; -using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Arguments { diff --git a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs index 1d10795..a13a5e0 100644 --- a/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs +++ b/Infrastructure/MassTransit/Arguments/PublishTrackProcessingEventsArguments.cs @@ -1,5 +1,4 @@ using Musify.Application.Shared; -using Musify.Application.Events; namespace Musify.Infrastructure.MassTransit.Arguments { diff --git a/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs index ffa9382..839e7d6 100644 --- a/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs +++ b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable From 842b1ffd0829f333150e6a0e300a698827778e72 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 00:24:41 +0200 Subject: [PATCH 160/172] Cleanup --- .../Configuration/PlayListConfiguration.cs | 3 +-- .../Configuration/TrackConfiguration.cs | 3 +-- .../UploadIntentConfiguration.cs | 20 +++++-------------- ...uestPlayListPictureUploadCommandHandler.cs | 2 +- .../RequestTrackUploadUrlsCommandHandler.cs | 4 ++-- Domain/Entities/UploadIntent.cs | 2 -- .../Jobs/TemporalUploadsCleanupJob.cs | 10 +++++----- .../Jobs/UploadIntentExpirationJob.cs | 2 -- .../20260530020027_InitialCreate.Designer.cs | 5 ++--- .../20260530020027_InitialCreate.cs | 11 ++++------ .../Migrations/DatabaseModelSnapshot.cs | 2 +- .../Services/ServicesDependencyInjection.cs | 2 -- 12 files changed, 22 insertions(+), 44 deletions(-) diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index 8290d26..c2fc6cc 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Musify.Application.Configuration { @@ -69,7 +69,6 @@ namespace Musify.Application.Configuration public string BuildLargePicturePath(string pictureName) => CombineKey(ParentFolders, LargePicturesFolder, pictureName); - // Temp path used in Phase 2 for pre-signed upload keys (moved to final on consumption) public string BuildTempPicturePath(string tempRootPrefix, long userId, string objectName) => CombineKey(tempRootPrefix, userId.ToString(), ParentFolders, objectName); diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index 4740944..ec3189f 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Musify.Application.Configuration { @@ -85,7 +85,6 @@ namespace Musify.Application.Configuration public string BuildProcessedAudioPath(string audioName) => CombineKey(ProcessedAudiosPath, audioName); - // Temp paths used in Phase 2 for pre-signed upload keys (moved to final on consumption) public string BuildTempPicturePath(string tempRootPrefix, long userId, string objectName) => CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); diff --git a/Application/Configuration/UploadIntentConfiguration.cs b/Application/Configuration/UploadIntentConfiguration.cs index d25741f..b18d32d 100644 --- a/Application/Configuration/UploadIntentConfiguration.cs +++ b/Application/Configuration/UploadIntentConfiguration.cs @@ -6,47 +6,37 @@ namespace Musify.Application.Configuration { public const string SectionName = "UploadIntent"; - // Pre-signed URL TTL in seconds [Range(10, 3600)] public int UploadUrlExpiresInSeconds { get; set; } = 120; - // Max concurrent Issued intents per user [Range(1, 100)] public int MaxActiveUploadIntentsPerUser { get; set; } = 5; - // Max total bytes across all Issued intents for a user (default 200MB) [Range(1, long.MaxValue)] public long MaxActiveUploadBytesPerUser { get; set; } = 200L * 1024 * 1024; - // Max size validated via HEAD before consuming an intent (default 100MB) [Range(1, long.MaxValue)] public long MaxUploadBytes { get; set; } = 100L * 1024 * 1024; - // Conservative fallback size used when ExpectedSizeBytes is not provided [Range(1, long.MaxValue)] - public long DefaultExpectedPictureSizeBytes { get; set; } = 10L * 1024 * 1024; // 10MB + public long DefaultExpectedPictureSizeBytes { get; set; } = 10L * 1024 * 1024; [Range(1, long.MaxValue)] - public long DefaultExpectedAudioSizeBytes { get; set; } = 100L * 1024 * 1024; // 100MB + public long DefaultExpectedAudioSizeBytes { get; set; } = 100L * 1024 * 1024; - // How often the expiration job runs [Range(30, 86400)] public int ExpirationJobIntervalSeconds { get; set; } = 300; - // How many days to retain Expired intents before deleting the DB row [Range(0, 365)] public int ExpiredIntentsRetentionDays { get; set; } = 7; - // Root S3 prefix for temporary uploads (Phase 2) [Required, MinLength(1)] - public string TempRootPrefix { get; set; } = "temp"; + public string TemporalRootPrefix { get; set; } = "temporal"; - // How many days to retain orphaned temp objects in S3 before the cleanup job deletes them [Range(1, 30)] - public int TempUploadsRetentionDays { get; set; } = 3; + public int TemporalUploadsRetentionDays { get; set; } = 3; - // How often the temp-upload cleanup job runs [Range(60, 86400)] - public int TempCleanupJobIntervalSeconds { get; set; } = 3600; + public int TemporalCleanUpJobIntervalSeconds { get; set; } = 3600; } } diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index 4c0d25d..ff0ef0d 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -39,7 +39,7 @@ namespace Musify.Application.PlayLists.Handlers var objectName = $"{Guid.NewGuid() }.{request.FileType.TrimStart('.').ToLowerInvariant() }"; var tempKey = playListConfiguration.Routes.BuildTempPicturePath( - uploadIntentConfiguration.TempRootPrefix, request.UserId, objectName); + uploadIntentConfiguration.TemporalRootPrefix, request.UserId, objectName); try { diff --git a/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs index f20c9b3..c651282 100644 --- a/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs +++ b/Application/Tracks/Handlers/RequestTrackUploadUrlsCommandHandler.cs @@ -42,9 +42,9 @@ namespace Musify.Application.Tracks.Handlers var audioObjectName = $"{Guid.NewGuid()}.{request.AudioFileType.TrimStart('.').ToLowerInvariant()}"; var tempPictureKey = trackConfiguration.Routes.BuildTempPicturePath( - uploadIntentConfiguration.TempRootPrefix, request.UserId, pictureObjectName); + uploadIntentConfiguration.TemporalRootPrefix, request.UserId, pictureObjectName); var tempAudioKey = trackConfiguration.Routes.BuildTempAudioPath( - uploadIntentConfiguration.TempRootPrefix, request.UserId, audioObjectName); + uploadIntentConfiguration.TemporalRootPrefix, request.UserId, audioObjectName); try { diff --git a/Domain/Entities/UploadIntent.cs b/Domain/Entities/UploadIntent.cs index d4f772b..4f1d15e 100644 --- a/Domain/Entities/UploadIntent.cs +++ b/Domain/Entities/UploadIntent.cs @@ -17,11 +17,9 @@ namespace Musify.Domain.Entities [Required, MaxLength(128)] public required string Bucket { get; set; } - // Full S3 key (including prefix and object name) [Required, MaxLength(512)] public required string Key { get; set; } - // Just the filename portion stored in Track/PlayList entities (e.g. "{guid}.webp") [Required, MaxLength(128)] public required string ObjectName { get; set; } diff --git a/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs b/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs index d11924a..ac20eb7 100644 --- a/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs +++ b/Infrastructure/Jobs/TemporalUploadsCleanupJob.cs @@ -16,8 +16,8 @@ namespace Musify.Infrastructure.Jobs protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("TemporalUploadsCleanUpJob started (interval: {Interval}s, prefix: {Prefix})", - uploadIntentConfiguration.TempCleanupJobIntervalSeconds, - uploadIntentConfiguration.TempRootPrefix); + uploadIntentConfiguration.TemporalCleanUpJobIntervalSeconds, + uploadIntentConfiguration.TemporalRootPrefix); while (!stoppingToken.IsCancellationRequested) { @@ -30,7 +30,7 @@ namespace Musify.Infrastructure.Jobs logger.LogError(exception, "Error in TemporalUploadsCleanUpJob"); } - await Task.Delay(TimeSpan.FromSeconds(uploadIntentConfiguration.TempCleanupJobIntervalSeconds), stoppingToken); + await Task.Delay(TimeSpan.FromSeconds(uploadIntentConfiguration.TemporalCleanUpJobIntervalSeconds), stoppingToken); } } @@ -39,13 +39,13 @@ namespace Musify.Infrastructure.Jobs await using var scope = scopeFactory.CreateAsyncScope(); var storageService = scope.ServiceProvider.GetRequiredService(); - var cutOff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TempUploadsRetentionDays); + var cutOff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TemporalUploadsRetentionDays); var deleted = 0; var errors = 0; await foreach (var (key, lastModified) in storageService.ListObjectsAsync( storageConfiguration.Bucket, - uploadIntentConfiguration.TempRootPrefix, + uploadIntentConfiguration.TemporalRootPrefix, cancellationToken)) { if (lastModified >= cutOff) diff --git a/Infrastructure/Jobs/UploadIntentExpirationJob.cs b/Infrastructure/Jobs/UploadIntentExpirationJob.cs index 2f21219..6e0cc78 100644 --- a/Infrastructure/Jobs/UploadIntentExpirationJob.cs +++ b/Infrastructure/Jobs/UploadIntentExpirationJob.cs @@ -40,7 +40,6 @@ namespace Musify.Infrastructure.Jobs var now = DateTime.UtcNow; - // Mark expired intents var expiredIntents = await database.UploadIntents .Where(i => i.Status == UploadIntentStatus.Issued && i.ExpiresAt < now) .ToListAsync(cancellationToken); @@ -54,7 +53,6 @@ namespace Musify.Infrastructure.Jobs logger.LogInformation("Marked {Count} upload intents as Expired", expiredIntents.Count); } - // Delete old Expired intents past retention window if (uploadIntentConfiguration.ExpiredIntentsRetentionDays >= 0) { var retentionCutoff = now.AddDays(-uploadIntentConfiguration.ExpiredIntentsRetentionDays); diff --git a/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs index 1b193c6..f8d67e5 100644 --- a/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.Designer.cs @@ -1,4 +1,4 @@ -// + using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,8 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations [Migration("20260530020027_InitialCreate")] partial class InitialCreate { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) + protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder diff --git a/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs index 839e7d6..eb0d10c 100644 --- a/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs +++ b/Infrastructure/Persistence/Migrations/20260530020027_InitialCreate.cs @@ -1,15 +1,13 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Musify.Infrastructure.Persistence.Migrations { - /// - public partial class InitialCreate : Migration + public partial class InitialCreate : Migration { - /// - protected override void Up(MigrationBuilder migrationBuilder) + protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "InboxState", @@ -321,8 +319,7 @@ namespace Musify.Infrastructure.Persistence.Migrations column: "UserId"); } - /// - protected override void Down(MigrationBuilder migrationBuilder) + protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "OutboxMessage"); diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 3293df5..09f5aa4 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -1,4 +1,4 @@ -// + using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 3597e68..45df273 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -44,7 +44,6 @@ namespace Musify.Infrastructure.Services return serviceDescriptors; } - // Called by both the web API host and the worker public static IServiceCollection AddUploadIntentConfiguration(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors @@ -60,7 +59,6 @@ namespace Musify.Infrastructure.Services return serviceDescriptors; } - // Called only by the worker host (registers background jobs) public static IServiceCollection AddUploadIntentJobs(this IServiceCollection serviceDescriptors, IConfiguration configuration) { serviceDescriptors.AddUploadIntentConfiguration(configuration); From 7c44e6aa7359ab4002b7e747cce6183a665bdd76 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 01:20:17 +0200 Subject: [PATCH 161/172] Add and remove tracks from playlists (commands + handlers with ownership checks) --- .../Commands/AddTrackToPlayListCommand.cs | 8 +++ .../RemoveTrackFromPlayListCommand.cs | 8 +++ .../AddTrackToPlayListCommandHandler.cs | 70 +++++++++++++++++++ .../RemoveTrackFromPlayListCommandHandler.cs | 47 +++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 Application/PlayLists/Commands/AddTrackToPlayListCommand.cs create mode 100644 Application/PlayLists/Commands/RemoveTrackFromPlayListCommand.cs create mode 100644 Application/PlayLists/Handlers/AddTrackToPlayListCommandHandler.cs create mode 100644 Application/PlayLists/Handlers/RemoveTrackFromPlayListCommandHandler.cs diff --git a/Application/PlayLists/Commands/AddTrackToPlayListCommand.cs b/Application/PlayLists/Commands/AddTrackToPlayListCommand.cs new file mode 100644 index 0000000..3d5e96b --- /dev/null +++ b/Application/PlayLists/Commands/AddTrackToPlayListCommand.cs @@ -0,0 +1,8 @@ +using Ardalis.Result; +using Mediator; + +namespace Musify.Application.PlayLists.Commands +{ + public record AddTrackToPlayListCommand(long UserId, Guid PlayListId, Guid TrackId) + : ICommand; +} diff --git a/Application/PlayLists/Commands/RemoveTrackFromPlayListCommand.cs b/Application/PlayLists/Commands/RemoveTrackFromPlayListCommand.cs new file mode 100644 index 0000000..991267d --- /dev/null +++ b/Application/PlayLists/Commands/RemoveTrackFromPlayListCommand.cs @@ -0,0 +1,8 @@ +using Ardalis.Result; +using Mediator; + +namespace Musify.Application.PlayLists.Commands +{ + public record RemoveTrackFromPlayListCommand(long UserId, Guid PlayListId, Guid TrackId) + : ICommand; +} diff --git a/Application/PlayLists/Handlers/AddTrackToPlayListCommandHandler.cs b/Application/PlayLists/Handlers/AddTrackToPlayListCommandHandler.cs new file mode 100644 index 0000000..c078f9f --- /dev/null +++ b/Application/PlayLists/Handlers/AddTrackToPlayListCommandHandler.cs @@ -0,0 +1,70 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Application.PlayLists.Commands; +using Musify.Domain.Entities; + +namespace Musify.Application.PlayLists.Handlers +{ + public class AddTrackToPlayListCommandHandler( + IDatabase database, + ILogger logger) + : ICommandHandler + { + public async ValueTask Handle(AddTrackToPlayListCommand request, CancellationToken cancellationToken) + { + var playList = await database.PlayLists + .SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); + if (playList is null) + { + logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); + return Result.NotFound(); + } + + if (playList.UserId != request.UserId) + { + logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId); + return Result.Unauthorized(); + } + + var trackExists = await database.Tracks + .AsNoTracking() + .AnyAsync(t => t.Id == request.TrackId, cancellationToken); + if (!trackExists) + { + logger.LogInformation("Track {TrackId} not found", request.TrackId); + return Result.NotFound($"Track {request.TrackId} not found"); + } + + var alreadyAdded = await database.PlayListHasTracks + .AsNoTracking() + .AnyAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken); + if (alreadyAdded) + { + logger.LogInformation("Track {TrackId} already in playlist {PlayListId}", request.TrackId, request.PlayListId); + return Result.Conflict("Track is already in the playlist."); + } + + var nextPosition = await database.PlayListHasTracks + .Where(plt => plt.PlayListId == request.PlayListId) + .Select(plt => (int?)plt.Position) + .MaxAsync(cancellationToken) + 1 ?? 0; + + await database.PlayListHasTracks.AddAsync(new PlayListHasTrack + { + PlayListId = request.PlayListId, + TrackId = request.TrackId, + Position = nextPosition + }, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("Added track {TrackId} to playlist {PlayListId} at position {Position}", + request.TrackId, request.PlayListId, nextPosition); + + return Result.Success(); + } + } +} diff --git a/Application/PlayLists/Handlers/RemoveTrackFromPlayListCommandHandler.cs b/Application/PlayLists/Handlers/RemoveTrackFromPlayListCommandHandler.cs new file mode 100644 index 0000000..609ede9 --- /dev/null +++ b/Application/PlayLists/Handlers/RemoveTrackFromPlayListCommandHandler.cs @@ -0,0 +1,47 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Application.PlayLists.Commands; + +namespace Musify.Application.PlayLists.Handlers +{ + public class RemoveTrackFromPlayListCommandHandler( + IDatabase database, + ILogger logger) + : ICommandHandler + { + public async ValueTask Handle(RemoveTrackFromPlayListCommand request, CancellationToken cancellationToken) + { + var playList = await database.PlayLists + .SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); + if (playList is null) + { + logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); + return Result.NotFound(); + } + + if (playList.UserId != request.UserId) + { + logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId); + return Result.Unauthorized(); + } + + var link = await database.PlayListHasTracks + .SingleOrDefaultAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken); + if (link is null) + { + logger.LogInformation("Track {TrackId} not in playlist {PlayListId}", request.TrackId, request.PlayListId); + return Result.NotFound("Track is not in the playlist."); + } + + database.PlayListHasTracks.Remove(link); + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("Removed track {TrackId} from playlist {PlayListId}", request.TrackId, request.PlayListId); + + return Result.Success(); + } + } +} From 11af429ecdcee2c47fe4b503b375f23fb825c8a6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 01:47:47 +0200 Subject: [PATCH 162/172] Add user profile picture URL synced from the identity provider on login --- Application/Users/Commands/SyncUserCommand.cs | 13 + .../Users/Handlers/SyncUserCommandHandler.cs | 74 +++ .../Responses/UserApplicationResponse.cs | 2 + Domain/Entities/User.cs | 3 + ...34306_AddUserProfilePictureUrl.Designer.cs | 583 ++++++++++++++++++ ...20260531234306_AddUserProfilePictureUrl.cs | 29 + .../Migrations/DatabaseModelSnapshot.cs | 6 +- 7 files changed, 709 insertions(+), 1 deletion(-) create mode 100644 Application/Users/Commands/SyncUserCommand.cs create mode 100644 Application/Users/Handlers/SyncUserCommandHandler.cs create mode 100644 Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.cs diff --git a/Application/Users/Commands/SyncUserCommand.cs b/Application/Users/Commands/SyncUserCommand.cs new file mode 100644 index 0000000..5419614 --- /dev/null +++ b/Application/Users/Commands/SyncUserCommand.cs @@ -0,0 +1,13 @@ +using Ardalis.Result; +using Mediator; + +namespace Musify.Application.Users.Commands +{ + public record SyncUserCommand( + long Id, + string Name, + string? FirstName, + string? SecondName, + string? ProfilePictureUrl) + : ICommand; +} diff --git a/Application/Users/Handlers/SyncUserCommandHandler.cs b/Application/Users/Handlers/SyncUserCommandHandler.cs new file mode 100644 index 0000000..cbeeee5 --- /dev/null +++ b/Application/Users/Handlers/SyncUserCommandHandler.cs @@ -0,0 +1,74 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Application.Users.Commands; +using Musify.Domain.Entities; + +namespace Musify.Application.Users.Handlers +{ + public class SyncUserCommandHandler(IDatabase database, ILogger logger) + : ICommandHandler + { + public async ValueTask Handle(SyncUserCommand request, CancellationToken cancellationToken) + { + var user = await database.Users + .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); + + if (user is null) + { + await database.Users.AddAsync(new User + { + Id = request.Id, + Name = request.Name, + NormalizedName = request.Name.ToUpperInvariant(), + FirstName = request.FirstName, + SecondName = request.SecondName, + ProfilePictureUrl = request.ProfilePictureUrl + }, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Provisioned user {UserId}", request.Id); + + return Result.Success(); + } + + var hasChanges = false; + + if (user.Name != request.Name) + { + user.Name = request.Name; + user.NormalizedName = request.Name.ToUpperInvariant(); + hasChanges = true; + } + + if (user.FirstName != request.FirstName) + { + user.FirstName = request.FirstName; + hasChanges = true; + } + + if (user.SecondName != request.SecondName) + { + user.SecondName = request.SecondName; + hasChanges = true; + } + + if (user.ProfilePictureUrl != request.ProfilePictureUrl) + { + user.ProfilePictureUrl = request.ProfilePictureUrl; + hasChanges = true; + } + + if (hasChanges) + { + user.UpdatedAt = DateTime.UtcNow; + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Synced profile for user {UserId}", request.Id); + } + + return Result.Success(); + } + } +} diff --git a/Application/Users/Responses/UserApplicationResponse.cs b/Application/Users/Responses/UserApplicationResponse.cs index e42616c..bde3649 100644 --- a/Application/Users/Responses/UserApplicationResponse.cs +++ b/Application/Users/Responses/UserApplicationResponse.cs @@ -7,6 +7,7 @@ namespace Musify.Application.Users.Responses string Name, string? FirstName, string? SecondName, + string? ProfilePictureUrl, DateTime CreatedAt, DateTime UpdatedAt) { @@ -17,6 +18,7 @@ namespace Musify.Application.Users.Responses user.Name, user.FirstName, user.SecondName, + user.ProfilePictureUrl, user.CreatedAt, user.UpdatedAt); } diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 2d2a0b0..48408ff 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -24,6 +24,9 @@ namespace Musify.Domain.Entities [MaxLength(48)] public string? SecondName { get; set; } + [MaxLength(2048)] + public string? ProfilePictureUrl { get; set; } + [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.Designer.cs b/Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.Designer.cs new file mode 100644 index 0000000..28bd9da --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.Designer.cs @@ -0,0 +1,583 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260531234306_AddUserProfilePictureUrl")] + partial class AddUserProfilePictureUrl + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.cs b/Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.cs new file mode 100644 index 0000000..5342474 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260531234306_AddUserProfilePictureUrl.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddUserProfilePictureUrl : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProfilePictureUrl", + table: "User", + type: "character varying(2048)", + maxLength: 2048, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ProfilePictureUrl", + table: "User"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 09f5aa4..2192161 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -1,4 +1,4 @@ - +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -447,6 +447,10 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(48) .HasColumnType("character varying(48)"); + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + b.Property("SecondName") .HasMaxLength(48) .HasColumnType("character varying(48)"); From 42122c65fcc9e6ec62a7a6a751a140d0dc745f79 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 16:45:37 +0200 Subject: [PATCH 163/172] Extract storage key building into a shared StorageKey helper --- .../Configuration/PlayListConfiguration.cs | 34 ++++++-------- .../Configuration/TrackConfiguration.cs | 46 ++++++++----------- Application/Shared/StorageKey.cs | 12 +++++ 3 files changed, 46 insertions(+), 46 deletions(-) create mode 100644 Application/Shared/StorageKey.cs diff --git a/Application/Configuration/PlayListConfiguration.cs b/Application/Configuration/PlayListConfiguration.cs index c2fc6cc..39e0d39 100644 --- a/Application/Configuration/PlayListConfiguration.cs +++ b/Application/Configuration/PlayListConfiguration.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Musify.Application.Shared; namespace Musify.Application.Configuration { @@ -45,39 +46,32 @@ namespace Musify.Application.Configuration [Required] public string PresetLargePicture { get; set; } = "PresetLargePicture.webp"; - public string SmallPicturesPath => CombineKey(ParentFolders, SmallPicturesFolder); + public string SmallPicturesPath => StorageKey.Combine(ParentFolders, SmallPicturesFolder); - public string MediumPicturesPath => CombineKey(ParentFolders, MediumPicturesFolder); + public string MediumPicturesPath => StorageKey.Combine(ParentFolders, MediumPicturesFolder); - public string LargePicturesPath => CombineKey(ParentFolders, LargePicturesFolder); + public string LargePicturesPath => StorageKey.Combine(ParentFolders, LargePicturesFolder); - public string PresetSmallPicturePath => CombineKey(ParentFolders, PresetSmallPicture); + public string PresetSmallPicturePath => StorageKey.Combine(ParentFolders, PresetSmallPicture); - public string PresetMediumPicturePath => CombineKey(ParentFolders, PresetMediumPicture); + public string PresetMediumPicturePath => StorageKey.Combine(ParentFolders, PresetMediumPicture); - public string PresetLargePicturePath => CombineKey(ParentFolders, PresetLargePicture); + public string PresetLargePicturePath => StorageKey.Combine(ParentFolders, PresetLargePicture); - public string OriginalPicturesPath => CombineKey(ParentFolders, OriginalPicturesFolder); + public string OriginalPicturesPath => StorageKey.Combine(ParentFolders, OriginalPicturesFolder); - public string BuildOriginalPicturePath(string pictureName) => CombineKey(ParentFolders, OriginalPicturesFolder, pictureName); + public string BuildOriginalPicturePath(string pictureName) => StorageKey.Combine(ParentFolders, OriginalPicturesFolder, pictureName); - public string BuildOriginalPicturePath(long userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName); + public string BuildOriginalPicturePath(long userId, string pictureName) => StorageKey.Combine(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName); - public string BuildSmallPicturePath(string pictureName) => CombineKey(ParentFolders, SmallPicturesFolder, pictureName); + public string BuildSmallPicturePath(string pictureName) => StorageKey.Combine(ParentFolders, SmallPicturesFolder, pictureName); - public string BuildMediumPicturePath(string pictureName) => CombineKey(ParentFolders, MediumPicturesFolder, pictureName); + public string BuildMediumPicturePath(string pictureName) => StorageKey.Combine(ParentFolders, MediumPicturesFolder, pictureName); - public string BuildLargePicturePath(string pictureName) => CombineKey(ParentFolders, LargePicturesFolder, pictureName); + public string BuildLargePicturePath(string pictureName) => StorageKey.Combine(ParentFolders, LargePicturesFolder, pictureName); public string BuildTempPicturePath(string tempRootPrefix, long userId, string objectName) => - CombineKey(tempRootPrefix, userId.ToString(), ParentFolders, objectName); - - static string CombineKey(params string[] segments) - { - return string.Join('/', segments - .Where(static s => !string.IsNullOrWhiteSpace(s)) - .Select(static s => s.Trim().Trim('/', '\\'))); - } + StorageKey.Combine(tempRootPrefix, userId.ToString(), ParentFolders, objectName); } public class PlayListPicturesSizes diff --git a/Application/Configuration/TrackConfiguration.cs b/Application/Configuration/TrackConfiguration.cs index ec3189f..1304e29 100644 --- a/Application/Configuration/TrackConfiguration.cs +++ b/Application/Configuration/TrackConfiguration.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Musify.Application.Shared; namespace Musify.Application.Configuration { @@ -51,52 +52,45 @@ namespace Musify.Application.Configuration [Required] public string ProcessedAudioFolder { get; set; } = "ProcessedAudios"; - public string SmallPicturesPath => CombineKey(ParentFolder, SmallPicturesFolder); + public string SmallPicturesPath => StorageKey.Combine(ParentFolder, SmallPicturesFolder); - public string MediumPicturesPath => CombineKey(ParentFolder, MediumPicturesFolder); + public string MediumPicturesPath => StorageKey.Combine(ParentFolder, MediumPicturesFolder); - public string LargePicturesPath => CombineKey(ParentFolder, LargePicturesFolder); + public string LargePicturesPath => StorageKey.Combine(ParentFolder, LargePicturesFolder); - public string PresetSmallPicturePath => CombineKey(ParentFolder, PresetSmallPicture); + public string PresetSmallPicturePath => StorageKey.Combine(ParentFolder, PresetSmallPicture); - public string PresetMediumPicturePath => CombineKey(ParentFolder, PresetMediumPicture); + public string PresetMediumPicturePath => StorageKey.Combine(ParentFolder, PresetMediumPicture); - public string PresetLargePicturePath => CombineKey(ParentFolder, PresetLargePicture); + public string PresetLargePicturePath => StorageKey.Combine(ParentFolder, PresetLargePicture); - public string OriginalPicturesPath => CombineKey(ParentFolder, OriginalPicturesFolder); + public string OriginalPicturesPath => StorageKey.Combine(ParentFolder, OriginalPicturesFolder); - public string OriginalAudiosPath => CombineKey(ParentFolder, OriginalAudiosFolder); + public string OriginalAudiosPath => StorageKey.Combine(ParentFolder, OriginalAudiosFolder); - public string ProcessedAudiosPath => CombineKey(ParentFolder, ProcessedAudioFolder); + public string ProcessedAudiosPath => StorageKey.Combine(ParentFolder, ProcessedAudioFolder); - public string BuildOriginalPicturePath(string pictureName) => CombineKey(OriginalPicturesPath, pictureName); + public string BuildOriginalPicturePath(string pictureName) => StorageKey.Combine(OriginalPicturesPath, pictureName); - public string BuildOriginalPicturePath(long userId, string pictureName) => CombineKey(UploadsFolder, userId.ToString(), OriginalPicturesPath, pictureName); + public string BuildOriginalPicturePath(long userId, string pictureName) => StorageKey.Combine(UploadsFolder, userId.ToString(), OriginalPicturesPath, pictureName); - public string BuildSmallPicturePath(string pictureName) => CombineKey(SmallPicturesPath, pictureName); + public string BuildSmallPicturePath(string pictureName) => StorageKey.Combine(SmallPicturesPath, pictureName); - public string BuildMediumPicturePath(string pictureName) => CombineKey(MediumPicturesPath, pictureName); + public string BuildMediumPicturePath(string pictureName) => StorageKey.Combine(MediumPicturesPath, pictureName); - public string BuildLargePicturePath(string pictureName) => CombineKey(LargePicturesPath, pictureName); + public string BuildLargePicturePath(string pictureName) => StorageKey.Combine(LargePicturesPath, pictureName); - public string BuildOriginalAudioPath(string audioName) => CombineKey(OriginalAudiosPath, audioName); + public string BuildOriginalAudioPath(string audioName) => StorageKey.Combine(OriginalAudiosPath, audioName); - public string BuildOriginalAudioPath(long userId, string audioName) => CombineKey(UploadsFolder, userId.ToString(), OriginalAudiosPath, audioName); + public string BuildOriginalAudioPath(long userId, string audioName) => StorageKey.Combine(UploadsFolder, userId.ToString(), OriginalAudiosPath, audioName); - public string BuildProcessedAudioPath(string audioName) => CombineKey(ProcessedAudiosPath, audioName); + public string BuildProcessedAudioPath(string audioName) => StorageKey.Combine(ProcessedAudiosPath, audioName); public string BuildTempPicturePath(string tempRootPrefix, long userId, string objectName) => - CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); + StorageKey.Combine(tempRootPrefix, userId.ToString(), ParentFolder, objectName); public string BuildTempAudioPath(string tempRootPrefix, long userId, string objectName) => - CombineKey(tempRootPrefix, userId.ToString(), ParentFolder, objectName); - - static string CombineKey(params string[] segments) - { - return string.Join('/', segments - .Where(static s => !string.IsNullOrWhiteSpace(s)) - .Select(static s => s.Trim().Trim('/', '\\'))); - } + StorageKey.Combine(tempRootPrefix, userId.ToString(), ParentFolder, objectName); } public class TrackPicturesSizes diff --git a/Application/Shared/StorageKey.cs b/Application/Shared/StorageKey.cs new file mode 100644 index 0000000..82ddca8 --- /dev/null +++ b/Application/Shared/StorageKey.cs @@ -0,0 +1,12 @@ +namespace Musify.Application.Shared +{ + public static class StorageKey + { + public static string Combine(params string[] segments) + { + return string.Join('/', segments + .Where(static s => !string.IsNullOrWhiteSpace(s)) + .Select(static s => s.Trim().Trim('/', '\\'))); + } + } +} From e1e1c161db71bdf95b83199df473f7b7b3980c03 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 18:54:25 +0200 Subject: [PATCH 164/172] Add paginated query for tracks of a playlist; make PlayListHasTrack.Track navigation non-nullable --- .../Handlers/GetPlayListTracksQueryHandler.cs | 41 +++++++++++++++++++ .../Queries/GetPlayListTracksQuery.cs | 13 ++++++ Domain/Entities/PlayListHasTrack.cs | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs create mode 100644 Application/PlayLists/Queries/GetPlayListTracksQuery.cs diff --git a/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs new file mode 100644 index 0000000..b0fd8cf --- /dev/null +++ b/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs @@ -0,0 +1,41 @@ +using Ardalis.Result; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts; +using Musify.Application.Pagination; +using Musify.Application.PlayLists.Queries; +using Musify.Application.Tracks.Responses; +using X.PagedList.EF; + +namespace Musify.Application.PlayLists.Handlers +{ + public class GetPlayListTracksQueryHandler(IDatabase database) + : IQueryHandler> > + { + public async ValueTask >> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken) + { + var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; + var pageSize = request.PageSize < 1 ? 10 : request.PageSize; + + var playListExists = await database.PlayLists + .AsNoTracking() + .AnyAsync(p => p.Id == request.PlayListId, cancellationToken); + if (!playListExists) + return Result>.NotFound(); + + var tracksQuery = database.PlayListHasTracks + .AsNoTracking() + .Include(plt => plt.Track) + .Where(plt => plt.PlayListId == request.PlayListId); + + var totalCount = await tracksQuery.CountAsync(cancellationToken); + + var pagedTracks = await tracksQuery + .OrderBy(plt => plt.Position) + .Select(plt => TrackApplicationResponse.FromEntity(plt.Track)) + .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + + return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); + } + } +} diff --git a/Application/PlayLists/Queries/GetPlayListTracksQuery.cs b/Application/PlayLists/Queries/GetPlayListTracksQuery.cs new file mode 100644 index 0000000..49759ef --- /dev/null +++ b/Application/PlayLists/Queries/GetPlayListTracksQuery.cs @@ -0,0 +1,13 @@ +using Ardalis.Result; +using Mediator; +using Musify.Application.Pagination; +using Musify.Application.Tracks.Responses; + +namespace Musify.Application.PlayLists.Queries +{ + public record GetPlayListTracksQuery( + Guid PlayListId, + int PageNumber = 1, + int PageSize = 10) + : IQuery>>; +} diff --git a/Domain/Entities/PlayListHasTrack.cs b/Domain/Entities/PlayListHasTrack.cs index 38d0c86..3de229f 100644 --- a/Domain/Entities/PlayListHasTrack.cs +++ b/Domain/Entities/PlayListHasTrack.cs @@ -25,6 +25,6 @@ namespace Musify.Domain.Entities public PlayList PlayList { get; set; } [ForeignKey(nameof(TrackId)) ] - public Track? Track { get; set; } + public Track Track { get; set; } } } \ No newline at end of file From 06c13cf52294ce77f829c16a07f5b9ed3af2ce4e Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 19:07:33 +0200 Subject: [PATCH 165/172] Link created track to its owner via UserHasTrack --- Application/Tracks/Handlers/CreateTrackCommandHandler.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Application/Tracks/Handlers/CreateTrackCommandHandler.cs b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs index 04f4569..fcf4754 100644 --- a/Application/Tracks/Handlers/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handlers/CreateTrackCommandHandler.cs @@ -69,6 +69,12 @@ namespace Musify.Application.Tracks.Handlers await database.Tracks.AddAsync(trackEntity, cancellationToken); + await database.UserHasTracks.AddAsync(new UserHasTrack + { + UserId = request.UserId, + TrackId = trackEntity.Id + }, cancellationToken); + try { await eventBus.PublishAsync( From 0e30167526608aa1bc403adc69b430f9bfd31272 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 20:41:34 +0200 Subject: [PATCH 166/172] Remove checks --- .../PlayLists/Handlers/GetPlayListTracksQueryHandler.cs | 5 +---- .../PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs | 5 +---- Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs | 5 +---- Application/Tracks/Handlers/GetTracksQueryHandler.cs | 5 +---- Application/Users/Handlers/GetUsersQueryHandler.cs | 5 +---- 5 files changed, 5 insertions(+), 20 deletions(-) diff --git a/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs index b0fd8cf..6f9a3eb 100644 --- a/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListTracksQueryHandler.cs @@ -14,9 +14,6 @@ namespace Musify.Application.PlayLists.Handlers { public async ValueTask >> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken) { - var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; - var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var playListExists = await database.PlayLists .AsNoTracking() .AnyAsync(p => p.Id == request.PlayListId, cancellationToken); @@ -33,7 +30,7 @@ namespace Musify.Application.PlayLists.Handlers var pagedTracks = await tracksQuery .OrderBy(plt => plt.Position) .Select(plt => TrackApplicationResponse.FromEntity(plt.Track)) - .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); } diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index 2a0506b..a5f9123 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -14,9 +14,6 @@ namespace Musify.Application.PlayLists.Handlers { public async ValueTask >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) { - var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; - var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var playListsQuery = database.PlayLists .AsNoTracking() .Where(p => p.UserId == request.UserId); @@ -32,7 +29,7 @@ namespace Musify.Application.PlayLists.Handlers var pagedPlayLists = await playListsQuery .OrderBy(p => p.CreatedDate) .Select(p => PlayListApplicationResponse.FromEntity(p)) - .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } diff --git a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs index c1a1f6d..a2bf593 100644 --- a/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsQueryHandler.cs @@ -14,16 +14,13 @@ namespace Musify.Application.PlayLists.Handlers { public async ValueTask >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken) { - var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; - var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var totalCount = await database.PlayLists.CountAsync(cancellationToken); var pagedPlayLists = await database.PlayLists .AsNoTracking() .OrderBy(p => p.Id) .Select(p => PlayListApplicationResponse.FromEntity(p)) - .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); return Result.Success(PaginatedResponse.FromPagedList(pagedPlayLists)); } diff --git a/Application/Tracks/Handlers/GetTracksQueryHandler.cs b/Application/Tracks/Handlers/GetTracksQueryHandler.cs index 694b328..b19ebb6 100644 --- a/Application/Tracks/Handlers/GetTracksQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTracksQueryHandler.cs @@ -14,9 +14,6 @@ namespace Musify.Application.Tracks.Handlers { public async ValueTask >> Handle(GetTracksQuery request, CancellationToken cancellationToken) { - var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; - var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var tracksQuery = database.UserHasTracks .AsNoTracking() .OrderBy(t => t.Id) @@ -24,7 +21,7 @@ namespace Musify.Application.Tracks.Handlers .Select(t => TrackApplicationResponse.FromEntity(t.Track)); var totalCount = await tracksQuery.CountAsync(cancellationToken); - var pagedTracks = await tracksQuery.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + var pagedTracks = await tracksQuery.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); return Result.Success(PaginatedResponse.FromPagedList(pagedTracks)); } diff --git a/Application/Users/Handlers/GetUsersQueryHandler.cs b/Application/Users/Handlers/GetUsersQueryHandler.cs index e6d9723..cca1b12 100644 --- a/Application/Users/Handlers/GetUsersQueryHandler.cs +++ b/Application/Users/Handlers/GetUsersQueryHandler.cs @@ -14,9 +14,6 @@ namespace Musify.Application.Users.Handlers { public async ValueTask >> Handle(GetUsersQuery request, CancellationToken cancellationToken) { - var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber; - var pageSize = request.PageSize < 1 ? 10 : request.PageSize; - var user = database.Users .AsNoTracking() .AsQueryable(); @@ -30,7 +27,7 @@ namespace Musify.Application.Users.Handlers var totalCount = await user.CountAsync(cancellationToken); var pagedUser = await user .Select(u => UserApplicationResponse.FromEntity(u)) - .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); + .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); return Result.Success(PaginatedResponse.FromPagedList(pagedUser)); } From e70ccb8e589e8953c3ac3cde1ddd249d92fcda16 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 22:04:52 +0200 Subject: [PATCH 167/172] Auto-set CreatedAt/UpdatedAt via EF interceptor; standardize timestamp names across entities --- .../GetPlayListsByUserIdQueryHandler.cs | 2 +- .../Responses/PlayListApplicationResponse.cs | 4 +- .../Handlers/GetTracksByUserIdQueryHandler.cs | 2 +- .../Responses/TrackApplicationResponse.cs | 8 +- .../Users/Handlers/SyncUserCommandHandler.cs | 1 - Domain/Abstractions/IAuditable.cs | 9 + Domain/Entities/PlayList.cs | 7 +- Domain/Entities/Track.cs | 7 +- Domain/Entities/User.cs | 3 +- .../Persistence/AuditableEntityInterceptor.cs | 43 ++ Infrastructure/Persistence/Database.cs | 1 + ...95757_RenameTimestampsAndAudit.Designer.cs | 583 ++++++++++++++++++ ...20260601195757_RenameTimestampsAndAudit.cs | 58 ++ .../Migrations/DatabaseModelSnapshot.cs | 8 +- 14 files changed, 716 insertions(+), 20 deletions(-) create mode 100644 Domain/Abstractions/IAuditable.cs create mode 100644 Infrastructure/Persistence/AuditableEntityInterceptor.cs create mode 100644 Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.Designer.cs create mode 100644 Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.cs diff --git a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs index a5f9123..d4b571c 100644 --- a/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs +++ b/Application/PlayLists/Handlers/GetPlayListsByUserIdQueryHandler.cs @@ -27,7 +27,7 @@ namespace Musify.Application.PlayLists.Handlers var totalCount = await playListsQuery.CountAsync(cancellationToken); var pagedPlayLists = await playListsQuery - .OrderBy(p => p.CreatedDate) + .OrderBy(p => p.CreatedAt) .Select(p => PlayListApplicationResponse.FromEntity(p)) .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); diff --git a/Application/PlayLists/Responses/PlayListApplicationResponse.cs b/Application/PlayLists/Responses/PlayListApplicationResponse.cs index 994996a..6a577b4 100644 --- a/Application/PlayLists/Responses/PlayListApplicationResponse.cs +++ b/Application/PlayLists/Responses/PlayListApplicationResponse.cs @@ -21,8 +21,8 @@ namespace Musify.Application.PlayLists.Responses playList.SmallPictureName, playList.MediumPictureName, playList.LargePictureName, - playList.CreatedDate, - playList.UpdatedDate); + playList.CreatedAt, + playList.UpdatedAt); } } } \ No newline at end of file diff --git a/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs b/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs index bf76206..5822428 100644 --- a/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTracksByUserIdQueryHandler.cs @@ -31,7 +31,7 @@ namespace Musify.Application.Tracks.Handlers var totalCount = await tracksQuery.CountAsync(cancellationToken); var pagedTracks = await tracksQuery - .OrderBy(t => t.Track.CreatedDate) + .OrderBy(t => t.Track.CreatedAt) .Select(t => TrackApplicationResponse.FromEntity(t.Track)) .ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken); diff --git a/Application/Tracks/Responses/TrackApplicationResponse.cs b/Application/Tracks/Responses/TrackApplicationResponse.cs index 33d8a83..83d3fa7 100644 --- a/Application/Tracks/Responses/TrackApplicationResponse.cs +++ b/Application/Tracks/Responses/TrackApplicationResponse.cs @@ -5,16 +5,16 @@ namespace Musify.Application.Tracks.Responses public record TrackApplicationResponse( Guid Id, string Title, - DateTime CreatedDate, - DateTime UpdatedDate) + DateTime CreatedAt, + DateTime UpdatedAt) { public static TrackApplicationResponse FromEntity(Track track) { return new TrackApplicationResponse( track.Id, track.Title, - track.CreatedDate, - track.UpdatedDate); + track.CreatedAt, + track.UpdatedAt); } } } \ No newline at end of file diff --git a/Application/Users/Handlers/SyncUserCommandHandler.cs b/Application/Users/Handlers/SyncUserCommandHandler.cs index cbeeee5..0686829 100644 --- a/Application/Users/Handlers/SyncUserCommandHandler.cs +++ b/Application/Users/Handlers/SyncUserCommandHandler.cs @@ -63,7 +63,6 @@ namespace Musify.Application.Users.Handlers if (hasChanges) { - user.UpdatedAt = DateTime.UtcNow; await database.SaveChangesAsync(cancellationToken); logger.LogInformation("Synced profile for user {UserId}", request.Id); } diff --git a/Domain/Abstractions/IAuditable.cs b/Domain/Abstractions/IAuditable.cs new file mode 100644 index 0000000..4815e2c --- /dev/null +++ b/Domain/Abstractions/IAuditable.cs @@ -0,0 +1,9 @@ +namespace Musify.Domain.Abstractions +{ + public interface IAuditable + { + DateTime CreatedAt { get; set; } + + DateTime UpdatedAt { get; set; } + } +} diff --git a/Domain/Entities/PlayList.cs b/Domain/Entities/PlayList.cs index 8789adb..cf3d509 100644 --- a/Domain/Entities/PlayList.cs +++ b/Domain/Entities/PlayList.cs @@ -1,13 +1,14 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Musify.Domain.Abstractions; using Musify.Domain.ValueObjects; namespace Musify.Domain.Entities { #pragma warning disable CS8618 [Table("PlayLists")] - public class PlayList + public class PlayList : IAuditable { [Key] public Guid Id { get; set; } = Guid.NewGuid(); @@ -47,10 +48,10 @@ namespace Musify.Domain.Entities public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; [Required] - public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [Required] - public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; [ForeignKey(nameof(UserId)) ] public User User { get; set; } diff --git a/Domain/Entities/Track.cs b/Domain/Entities/Track.cs index c569b5a..8299c3d 100644 --- a/Domain/Entities/Track.cs +++ b/Domain/Entities/Track.cs @@ -2,12 +2,13 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; +using Musify.Domain.Abstractions; using Musify.Domain.ValueObjects; namespace Musify.Domain.Entities { #pragma warning disable CS8618 - public class Track + public class Track : IAuditable { [Key] public Guid Id { get; set; } = Guid.NewGuid(); @@ -55,10 +56,10 @@ namespace Musify.Domain.Entities public DateTime? LastRetryAt { get; set; } [Required] - public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [Required] - public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public ICollection UserTracks { get; set; } = new List(); diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 48408ff..184dbc6 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -1,10 +1,11 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Musify.Domain.Abstractions; namespace Musify.Domain.Entities { [Table("User")] - public class User + public class User : IAuditable { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] diff --git a/Infrastructure/Persistence/AuditableEntityInterceptor.cs b/Infrastructure/Persistence/AuditableEntityInterceptor.cs new file mode 100644 index 0000000..40c7e69 --- /dev/null +++ b/Infrastructure/Persistence/AuditableEntityInterceptor.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Musify.Domain.Abstractions; + +namespace Musify.Infrastructure.Persistence +{ + public sealed class AuditableEntityInterceptor : SaveChangesInterceptor + { + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + ApplyTimestamps(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + ApplyTimestamps(eventData.Context); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private static void ApplyTimestamps(DbContext? context) + { + if (context is null) + return; + + var now = DateTime.UtcNow; + + foreach (var entry in context.ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedAt = now; + entry.Entity.UpdatedAt = now; + } + else if (entry.State == EntityState.Modified) + { + entry.Entity.UpdatedAt = now; + } + } + } + } +} diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 98f1ea1..3723077 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -14,6 +14,7 @@ namespace Musify.Infrastructure.Persistence protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(configuration.ConnectionString); + optionsBuilder.AddInterceptors(new AuditableEntityInterceptor()); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.Designer.cs b/Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.Designer.cs new file mode 100644 index 0000000..279f318 --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.Designer.cs @@ -0,0 +1,583 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260601195757_RenameTimestampsAndAudit")] + partial class RenameTimestampsAndAudit + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.cs b/Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.cs new file mode 100644 index 0000000..540250e --- /dev/null +++ b/Infrastructure/Persistence/Migrations/20260601195757_RenameTimestampsAndAudit.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class RenameTimestampsAndAudit : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "UpdatedDate", + table: "Tracks", + newName: "UpdatedAt"); + + migrationBuilder.RenameColumn( + name: "CreatedDate", + table: "Tracks", + newName: "CreatedAt"); + + migrationBuilder.RenameColumn( + name: "UpdatedDate", + table: "PlayLists", + newName: "UpdatedAt"); + + migrationBuilder.RenameColumn( + name: "CreatedDate", + table: "PlayLists", + newName: "CreatedAt"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "UpdatedAt", + table: "Tracks", + newName: "UpdatedDate"); + + migrationBuilder.RenameColumn( + name: "CreatedAt", + table: "Tracks", + newName: "CreatedDate"); + + migrationBuilder.RenameColumn( + name: "UpdatedAt", + table: "PlayLists", + newName: "UpdatedDate"); + + migrationBuilder.RenameColumn( + name: "CreatedAt", + table: "PlayLists", + newName: "CreatedDate"); + } + } +} diff --git a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 2192161..b6f5653 100644 --- a/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -196,7 +196,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedDate") + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); b.Property("Description") @@ -237,7 +237,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("UpdatedDate") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("UserId") @@ -290,7 +290,7 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("AudioTranscodeProcessingStatus") .HasColumnType("integer"); - b.Property("CreatedDate") + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); b.Property("LargePictureName") @@ -337,7 +337,7 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("UpdatedDate") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); From 6da1e8d5cd53cf9e9da8f41a71685b6ef985932d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 22:11:48 +0200 Subject: [PATCH 168/172] Simplify user sync: rely on EF change tracking instead of manual hasChanges flag --- .../Users/Handlers/SyncUserCommandHandler.cs | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/Application/Users/Handlers/SyncUserCommandHandler.cs b/Application/Users/Handlers/SyncUserCommandHandler.cs index 0686829..61aabb8 100644 --- a/Application/Users/Handlers/SyncUserCommandHandler.cs +++ b/Application/Users/Handlers/SyncUserCommandHandler.cs @@ -34,38 +34,15 @@ namespace Musify.Application.Users.Handlers return Result.Success(); } - var hasChanges = false; + user.Name = request.Name; + user.NormalizedName = request.Name.ToUpperInvariant(); + user.FirstName = request.FirstName; + user.SecondName = request.SecondName; + user.ProfilePictureUrl = request.ProfilePictureUrl; - if (user.Name != request.Name) - { - user.Name = request.Name; - user.NormalizedName = request.Name.ToUpperInvariant(); - hasChanges = true; - } - - if (user.FirstName != request.FirstName) - { - user.FirstName = request.FirstName; - hasChanges = true; - } - - if (user.SecondName != request.SecondName) - { - user.SecondName = request.SecondName; - hasChanges = true; - } - - if (user.ProfilePictureUrl != request.ProfilePictureUrl) - { - user.ProfilePictureUrl = request.ProfilePictureUrl; - hasChanges = true; - } - - if (hasChanges) - { - await database.SaveChangesAsync(cancellationToken); + var written = await database.SaveChangesAsync(cancellationToken); + if (written > 0) logger.LogInformation("Synced profile for user {UserId}", request.Id); - } return Result.Success(); } From be8fbbbcdc120d6b82c14ea3eac01079d03d521d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 1 Jun 2026 22:48:42 +0200 Subject: [PATCH 169/172] Fix bugs --- .../Activities/Pictures/UpdateTrackPictureActivity.cs | 5 +++-- Infrastructure/Services/StorageService.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 55e9150..7b16eaf 100644 --- a/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,6 +1,7 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts; +using Musify.Domain.ValueObjects; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; @@ -46,6 +47,7 @@ namespace Musify.Infrastructure.MassTransit.Activities track.SmallPictureName = Path.GetFileName(smallResizedVariable); track.MediumPictureName = Path.GetFileName(mediumResizedVariable); track.LargePictureName = Path.GetFileName(largeResizedVariable); + track.PicturesProcessingStatus = ProcessingStatus.Completed; database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); @@ -72,14 +74,13 @@ namespace Musify.Infrastructure.MassTransit.Activities cancellationToken: compensateContext.CancellationToken); if (track is null) - { return compensateContext.Compensated(); - } track.OriginalPictureName = compensateContext.Log.PreviousOriginalPictureKey; track.SmallPictureName = compensateContext.Log.PreviousSmallPictureKey; track.MediumPictureName = compensateContext.Log.PreviousMediumPictureKey; track.LargePictureName = compensateContext.Log.PreviousLargePictureKey; + track.PicturesProcessingStatus = ProcessingStatus.Failed; database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 76bbb5e..3a97ee5 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -241,8 +241,8 @@ namespace Musify.Infrastructure.Services do { listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken); - foreach (var obj in listResponse.S3Objects) - yield return new StorageObject(obj.Key, (obj.LastModified ?? DateTime.UtcNow).ToUniversalTime()); + foreach (var s3Object in listResponse.S3Objects ?? [] ) + yield return new StorageObject(s3Object.Key, (s3Object.LastModified ?? DateTime.UtcNow).ToUniversalTime()); listRequest.ContinuationToken = listResponse.NextContinuationToken; } @@ -263,7 +263,7 @@ namespace Musify.Infrastructure.Services { listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken); - if (listResponse.S3Objects.Count == 0) + if (listResponse.S3Objects is null or { Count: 0 }) break; var deleteRequest = new DeleteObjectsRequest From c1d57098b1185aad25870cbc53a473445669f950 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 2 Jun 2026 19:05:11 +0200 Subject: [PATCH 170/172] Migrate audio transcoding from DASH to a single AAC (.m4a) file --- .../StreamGatewayConfiguration.cs | 2 +- .../Contracts/IAudioTranscoderService.cs | 2 +- .../Handlers/GetTrackStreamQueryHandler.cs | 2 +- .../AudioTranscoderConfiguration.cs | 11 +------- ...oActivity.cs => TranscodeAudioActivity.cs} | 16 +++++------ ...rguments.cs => TranscodeAudioArguments.cs} | 6 ++-- ...TransitDependencyInjection.Registration.cs | 6 ++-- .../MassTransit/Logs/TranscodeAudioLog.cs | 4 +++ .../MassTransit/Logs/TranscodeDashAudioLog.cs | 4 --- .../AudioWorkflowRoutingSlipBuilder.cs | 4 +-- .../Services/AudioTranscoderService.cs | 28 ++++++++----------- 11 files changed, 35 insertions(+), 50 deletions(-) rename Infrastructure/MassTransit/Activities/Audio/{TranscodeDashAudioActivity.cs => TranscodeAudioActivity.cs} (89%) rename Infrastructure/MassTransit/Arguments/{TranscodeDashAudioArguments.cs => TranscodeAudioArguments.cs} (50%) create mode 100644 Infrastructure/MassTransit/Logs/TranscodeAudioLog.cs delete mode 100644 Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs diff --git a/Application/Configuration/StreamGatewayConfiguration.cs b/Application/Configuration/StreamGatewayConfiguration.cs index e19cad6..edba302 100644 --- a/Application/Configuration/StreamGatewayConfiguration.cs +++ b/Application/Configuration/StreamGatewayConfiguration.cs @@ -11,6 +11,6 @@ namespace Musify.Application.Configuration public required string PublicBaseUrl { get; set; } [Required] - public string ManifestFileName { get; set; } = "manifest.mpd"; + public string AudioFileName { get; set; } = "audio.m4a"; } } diff --git a/Application/Contracts/IAudioTranscoderService.cs b/Application/Contracts/IAudioTranscoderService.cs index d2c06dd..c4f0d02 100644 --- a/Application/Contracts/IAudioTranscoderService.cs +++ b/Application/Contracts/IAudioTranscoderService.cs @@ -2,7 +2,7 @@ namespace Musify.Application.Contracts { public interface IAudioTranscoderService { - Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); + Task TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); Task IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken); } diff --git a/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs b/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs index b9f4eff..b96925e 100644 --- a/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs +++ b/Application/Tracks/Handlers/GetTrackStreamQueryHandler.cs @@ -44,7 +44,7 @@ namespace Musify.Application.Tracks.Handlers streamGatewayConfiguration.PublicBaseUrl.TrimEnd('/'), "media", folderPath, - streamGatewayConfiguration.ManifestFileName); + streamGatewayConfiguration.AudioFileName); logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId); diff --git a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs index e4ef863..7bf532c 100644 --- a/Infrastructure/Configuration/AudioTranscoderConfiguration.cs +++ b/Infrastructure/Configuration/AudioTranscoderConfiguration.cs @@ -19,7 +19,7 @@ namespace Musify.Infrastructure.Configuration public string ExecutableName { get; set; } = "ffmpeg"; [Required] - public string ManifestFileName { get; set; } = "manifest.mpd"; + public string OutputFileName { get; set; } = "audio.m4a"; [Required] public string AudioCodec { get; set; } = "aac"; @@ -36,15 +36,6 @@ namespace Musify.Infrastructure.Configuration [Required] public string AudioProfile { get; set; } = "aac_low"; - [Range(1, 60)] - public int SegmentDurationSeconds { get; set; } = 4; - - [Required] - public string InitSegmentName { get; set; } = "init-stream$RepresentationID$.m4s"; - - [Required] - public string MediaSegmentName { get; set; } = "chunk-stream$RepresentationID$-$Number$.m4s"; - public string AdditionalArguments { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs b/Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs similarity index 89% rename from Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs rename to Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs index 5305cb0..36f3f68 100644 --- a/Infrastructure/MassTransit/Activities/Audio/TranscodeDashAudioActivity.cs +++ b/Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs @@ -9,15 +9,15 @@ using Musify.Infrastructure.MassTransit.Logs; namespace Musify.Infrastructure.MassTransit.Activities { - internal class TranscodeDashAudioActivity( + internal class TranscodeAudioActivity( IDatabase database, IAudioTranscoderService audioTranscoder, - ILogger logger) - : IActivity + ILogger logger) + : IActivity { - public const string ExecuteEndpointName = "transcode-dash-audio"; + public const string ExecuteEndpointName = "transcode-audio"; - public async Task Execute(ExecuteContext executeContext) + public async Task Execute(ExecuteContext executeContext) { var sourceFilePath = executeContext.GetVariable(executeContext.Arguments.SourceFilePathVariable); ArgumentNullException.ThrowIfNull(sourceFilePath, nameof(sourceFilePath)); @@ -40,7 +40,7 @@ namespace Musify.Infrastructure.MassTransit.Activities { await using (var fileStream = File.OpenRead(sourceFilePath)) { - await audioTranscoder.TranscodeToDashAsync( + await audioTranscoder.TranscodeToAudioFileAsync( fileStream, workingDirectory, executeContext.CancellationToken); @@ -68,7 +68,7 @@ namespace Musify.Infrastructure.MassTransit.Activities } } - public Task Compensate(CompensateContext compensateContext) + public Task Compensate(CompensateContext compensateContext) { try { @@ -86,4 +86,4 @@ namespace Musify.Infrastructure.MassTransit.Activities } } } -} \ No newline at end of file +} diff --git a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs b/Infrastructure/MassTransit/Arguments/TranscodeAudioArguments.cs similarity index 50% rename from Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs rename to Infrastructure/MassTransit/Arguments/TranscodeAudioArguments.cs index 08f0cef..1533480 100644 --- a/Infrastructure/MassTransit/Arguments/TranscodeDashAudioArguments.cs +++ b/Infrastructure/MassTransit/Arguments/TranscodeAudioArguments.cs @@ -1,7 +1,7 @@ -namespace Musify.Infrastructure.MassTransit.Arguments +namespace Musify.Infrastructure.MassTransit.Arguments { - public record TranscodeDashAudioArguments( + public record TranscodeAudioArguments( Guid TrackId, string SourceFilePathVariable, string WorkingDirectoryVariable); -} \ No newline at end of file +} diff --git a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index c700164..457f956 100644 --- a/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -52,7 +52,7 @@ namespace Musify.Infrastructure.MassTransit options.AddActivity(); options.AddActivity(); options.AddActivity(); - options.AddActivity(); + options.AddActivity(); options.AddActivity(); options.AddActivity(); options.AddActivity(); @@ -178,10 +178,10 @@ namespace Musify.Infrastructure.MassTransit busRegistrationContext, DownloadFileFromBucketActivity.ExecuteEndpointName); - ConfigureActivityEndpoint( + ConfigureActivityEndpoint( busFactoryConfigurator, busRegistrationContext, - TranscodeDashAudioActivity.ExecuteEndpointName); + TranscodeAudioActivity.ExecuteEndpointName); ConfigureActivityEndpoint( busFactoryConfigurator, diff --git a/Infrastructure/MassTransit/Logs/TranscodeAudioLog.cs b/Infrastructure/MassTransit/Logs/TranscodeAudioLog.cs new file mode 100644 index 0000000..c85933a --- /dev/null +++ b/Infrastructure/MassTransit/Logs/TranscodeAudioLog.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.MassTransit.Logs +{ + public record TranscodeAudioLog(string WorkingDirectory); +} diff --git a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs b/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs deleted file mode 100644 index 05c6a9f..0000000 --- a/Infrastructure/MassTransit/Logs/TranscodeDashAudioLog.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Musify.Infrastructure.MassTransit.Logs -{ - public record TranscodeDashAudioLog(string WorkingDirectory); -} \ No newline at end of file diff --git a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index 6f2b0b9..cbf30fe 100644 --- a/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -39,8 +39,8 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders routingSlipBuilder.AddActivity( ActivityNames.TranscodeAudio, - EndpointHelper.BuildExecuteActivityUri(TranscodeDashAudioActivity.ExecuteEndpointName), - new TranscodeDashAudioArguments( + EndpointHelper.BuildExecuteActivityUri(TranscodeAudioActivity.ExecuteEndpointName), + new TranscodeAudioArguments( message.TrackId, RoutingSlipVariableNames.Audio.SourceFilePath, RoutingSlipVariableNames.Workflow.TemporalDirectory)); diff --git a/Infrastructure/Services/AudioTranscoderService.cs b/Infrastructure/Services/AudioTranscoderService.cs index 805f4fc..c3edd07 100644 --- a/Infrastructure/Services/AudioTranscoderService.cs +++ b/Infrastructure/Services/AudioTranscoderService.cs @@ -11,29 +11,29 @@ namespace Musify.Infrastructure.Services { record FfmpegExecutionResult(int ExitCode, string StandardOutput, string StandardError); - public async Task TranscodeToDashAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken) + public async Task TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken) { try { Directory.CreateDirectory(destinationPath); - logger.LogInformation("Starting audio transcoding to DASH for {DestinationPath}", destinationPath); + logger.LogInformation("Starting audio transcoding for {DestinationPath}", destinationPath); var executionResult = await ExecuteFfmpegWithInputAsync( - BuildDashArguments(destinationPath, audioTranscoderConfiguration.Ffmpeg), + BuildAudioFileArguments(destinationPath, audioTranscoderConfiguration.Ffmpeg), destinationPath, audioStream, - "TranscodeToDash", + "TranscodeToAudioFile", cancellationToken); if (executionResult.ExitCode != 0) { - logger.LogError("Audio transcoding to DASH failed with exit code {ExitCode}. Output: {StandardOutput}, Error: {StandardError}", + logger.LogError("Audio transcoding failed with exit code {ExitCode}. Output: {StandardOutput}, Error: {StandardError}", executionResult.ExitCode, executionResult.StandardOutput, executionResult.StandardError); } else { - logger.LogInformation("Audio transcoding to DASH completed for {DestinationPath}", destinationPath); + logger.LogInformation("Audio transcoding completed for {DestinationPath}", destinationPath); } return executionResult.ExitCode; @@ -45,7 +45,7 @@ namespace Musify.Infrastructure.Services } catch (Exception exception) { - logger.LogError(exception, "Failed to transcode audio to DASH for {DestinationPath}", destinationPath); + logger.LogError(exception, "Failed to transcode audio for {DestinationPath}", destinationPath); throw; } } @@ -188,9 +188,9 @@ namespace Musify.Infrastructure.Services } } - static string BuildDashArguments(string outputDirectory, FfmpegConfiguration ffmpegConfiguration) + static string BuildAudioFileArguments(string outputDirectory, FfmpegConfiguration ffmpegConfiguration) { - var manifestPath = Path.Combine(outputDirectory, ffmpegConfiguration.ManifestFileName); + var outputPath = Path.Combine(outputDirectory, ffmpegConfiguration.OutputFileName); var additionalArguments = string.IsNullOrWhiteSpace(ffmpegConfiguration.AdditionalArguments) ? string.Empty : $" {ffmpegConfiguration.AdditionalArguments.Trim()}"; @@ -204,15 +204,9 @@ namespace Musify.Infrastructure.Services $"-ac {ffmpegConfiguration.AudioChannels} " + $"-ar {ffmpegConfiguration.AudioSampleRate} " + $"-profile:a {ffmpegConfiguration.AudioProfile} " + - "-f dash " + - $"-seg_duration {ffmpegConfiguration.SegmentDurationSeconds} " + - "-streaming 1 " + - "-use_template 1 " + - "-use_timeline 0 " + - $"-init_seg_name \"{ffmpegConfiguration.InitSegmentName}\" " + - $"-media_seg_name \"{ffmpegConfiguration.MediaSegmentName}\"" + + "-movflags +faststart" + additionalArguments + - $" \"{manifestPath}\""; + $" \"{outputPath}\""; } static string BuildValidateAudioArguments(string filePath) From c89645564de6e71ce099f24aca9a05bd52f37528 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 2 Jun 2026 19:26:45 +0200 Subject: [PATCH 171/172] Set Content-Type on transferred objects so .m4a is served as audio/mp4 --- Infrastructure/Services/StorageService.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Infrastructure/Services/StorageService.cs b/Infrastructure/Services/StorageService.cs index 3a97ee5..b07c765 100644 --- a/Infrastructure/Services/StorageService.cs +++ b/Infrastructure/Services/StorageService.cs @@ -122,18 +122,27 @@ namespace Musify.Infrastructure.Services var key = string.Join('/', new[] { route, fileName } .Where(static s => !string.IsNullOrWhiteSpace(s)) .Select(static s => s.Trim().Trim('/', '\\'))); + var contentType = MimeUtility.GetMimeMapping(filePath); var upload = new Upload { Id = Guid.NewGuid(), Bucket = bucket, Key = key, - ContentType = MimeUtility.GetMimeMapping(filePath) + ContentType = contentType }; await database.Uploads.AddAsync(upload, cancellationToken); try { - await trasnsferUtility.UploadAsync(filePath, bucket, key, cancellationToken); + await trasnsferUtility.UploadAsync( + new TransferUtilityUploadRequest + { + FilePath = filePath, + BucketName = bucket, + Key = key, + ContentType = contentType + }, + cancellationToken); upload.State = UploadState.Successful; successCount++; logger.LogDebug("Transferred file to S3 {Bucket}/{Key}", bucket, key); From 43c5061fac6b73a40048d540df7d485ca63d1249 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Wed, 3 Jun 2026 00:32:37 +0200 Subject: [PATCH 172/172] Adjust --- Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs index a4a11f5..1c28036 100644 --- a/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs +++ b/Application/PlayLists/Queries/GetPlayListsByUserIdQuery.cs @@ -7,7 +7,7 @@ namespace Musify.Application.PlayLists.Queries { public record GetPlayListsByUserIdQuery( long UserId, - string? Name = null, + string? Name, int PageNumber = 1, int PageSize = 10) : IQuery> >;