From 5e819d76762428a5d3e1adb13bdc35f820ad65e6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 24 Sep 2026 05:19:12 +0200 Subject: [PATCH] Remove dead code: unused storage/transcoder members, Upload table, retry columns, redundant packages --- .../Musify.Application/Albums/UpdateAlbum.cs | 10 +- .../Contracts/IAudioTranscoderService.cs | 2 - .../Musify.Application/Contracts/IDatabase.cs | 2 - .../Contracts/IStorageService.cs | 4 - .../PlayLists/UpdatePlayList.cs | 10 +- .../Core/Musify.Application/Users/GetUsers.cs | 3 +- backend/Core/Musify.Domain/Entities/Upload.cs | 28 - .../Musify.Domain/ValueObjects/TrackAudio.cs | 6 - .../Musify.Domain/ValueObjects/UploadState.cs | 8 - .../Infrastructure.csproj | 4 +- .../Jobs/UploadIntentExpirationJob.cs | 23 +- .../Files/DownloadFileFromUrlActivity.cs | 68 - .../Pictures/UpdateAlbumPictureActivity.cs | 2 - .../Pictures/UpdatePlayListPictureActivity.cs | 2 - .../Pictures/UpdateTrackPictureActivity.cs | 2 - .../Arguments/DownloadFileFromUrlArguments.cs | 5 - ...TransitDependencyInjection.Registration.cs | 1 - .../Logs/DownloadFileFromUrlLog.cs | 5 - .../MassTransit/RoutingSlip/ActivityNames.cs | 1 - .../Persistence/Database.cs | 8 +- .../Persistence/DatabaseDesignTimeFactory.cs | 4 - ...TableAndTrackAudioRetryColumns.Designer.cs | 1133 +++++++++++++++++ ...ropUploadTableAndTrackAudioRetryColumns.cs | 70 + .../Migrations/DatabaseModelSnapshot.cs | 41 - .../Services/AudioTranscoderService.cs | 44 - .../Services/ServicesDependencyInjection.cs | 2 +- .../Services/SingleFlightCache.cs | 40 - .../Services/StorageService.cs | 88 +- backend/Hosts/Musify.Api/Api.csproj | 17 - .../Musify.Api/Authentication/CurrentUser.cs | 7 - backend/Hosts/Musify.Api/appsettings.json | 18 - backend/Hosts/Musify.Worker/Program.cs | 3 +- backend/Hosts/Musify.Worker/Worker.csproj | 9 - backend/Hosts/Musify.Worker/appsettings.json | 3 - .../Authentication/CurrentUserTests.cs | 18 - .../TestSupport/TestDatabase.cs | 2 - .../Services/SingleFlightCacheTests.cs | 80 -- .../Services/StorageServiceTests.cs | 22 +- deploy/compose.yml | 2 - docs/architecture.md | 1 - docs/development.md | 2 +- 41 files changed, 1230 insertions(+), 570 deletions(-) delete mode 100644 backend/Core/Musify.Domain/Entities/Upload.cs delete mode 100644 backend/Core/Musify.Domain/ValueObjects/UploadState.cs delete mode 100644 backend/Core/Musify.Infrastructure/MassTransit/Activities/Files/DownloadFileFromUrlActivity.cs delete mode 100644 backend/Core/Musify.Infrastructure/MassTransit/Arguments/DownloadFileFromUrlArguments.cs delete mode 100644 backend/Core/Musify.Infrastructure/MassTransit/Logs/DownloadFileFromUrlLog.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.Designer.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.cs delete mode 100644 backend/Core/Musify.Infrastructure/Services/SingleFlightCache.cs delete mode 100644 backend/Tests/Musify.Infrastructure.Tests/Services/SingleFlightCacheTests.cs diff --git a/backend/Core/Musify.Application/Albums/UpdateAlbum.cs b/backend/Core/Musify.Application/Albums/UpdateAlbum.cs index 3c23fea..f03bfcf 100644 --- a/backend/Core/Musify.Application/Albums/UpdateAlbum.cs +++ b/backend/Core/Musify.Application/Albums/UpdateAlbum.cs @@ -55,9 +55,6 @@ public class UpdateAlbumCommandHandler( album.Description = request.Description; album.ReleaseYear = request.ReleaseYear; - UploadIntent? pictureIntent = null; - string? finalPictureKey = null; - if (request.NewPictureIntentId.HasValue) { var validation = await uploadIntentValidator.ValidateAndLoadAsync( @@ -66,13 +63,10 @@ public class UpdateAlbumCommandHandler( if (validation.IsError) return validation.Errors; - pictureIntent = validation.Value; + var pictureIntent = validation.Value; album.Pictures = AlbumPictures.Pending(pictureIntent.ObjectName); - finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); - } + var finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); - if (pictureIntent != null && finalPictureKey != null) - { var publishResult = await PublishUpdateAlbumPictureSourceEventAsync( album.Id, pictureIntent, finalPictureKey, cancellationToken); if (publishResult.IsError) diff --git a/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs b/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs index 3f0558a..29f8b81 100644 --- a/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs +++ b/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs @@ -3,6 +3,4 @@ namespace Musify.Application.Contracts; public interface IAudioTranscoderService { public Task TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); - - public Task IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken); } diff --git a/backend/Core/Musify.Application/Contracts/IDatabase.cs b/backend/Core/Musify.Application/Contracts/IDatabase.cs index de3b8e2..9bed6b1 100644 --- a/backend/Core/Musify.Application/Contracts/IDatabase.cs +++ b/backend/Core/Musify.Application/Contracts/IDatabase.cs @@ -24,8 +24,6 @@ public interface IDatabase public DbSet MixItems { get; } - public DbSet Uploads { get; } - public DbSet UploadIntents { get; } public DbSet ListeningHistories { get; } diff --git a/backend/Core/Musify.Application/Contracts/IStorageService.cs b/backend/Core/Musify.Application/Contracts/IStorageService.cs index ea79bdb..c5c0a70 100644 --- a/backend/Core/Musify.Application/Contracts/IStorageService.cs +++ b/backend/Core/Musify.Application/Contracts/IStorageService.cs @@ -6,8 +6,6 @@ public interface IStorageService public Task HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken); - public Task GetUrlAsync(string bucket, string key, TimeSpan ExpirationTime, CancellationToken cancellationToken); - public Task GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, CancellationToken cancellationToken, bool preventOverwrite = true); public Task UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken); @@ -21,7 +19,5 @@ public interface IStorageService public Task CopyFileAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, CancellationToken cancellationToken); - public Task RemoveFolderAsync(string bucket, string folderKey, CancellationToken cancellationToken); - public IAsyncEnumerable ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken); } diff --git a/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs b/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs index d497e12..9516d44 100644 --- a/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs +++ b/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs @@ -63,9 +63,6 @@ public class UpdatePlayListCommandHandler( playListEntity.Visibility = request.NewVisibility.Value; } - UploadIntent? pictureIntent = null; - string? finalPictureKey = null; - if (request.NewPictureIntentId.HasValue) { var validation = await uploadIntentValidator.ValidateAndLoadAsync( @@ -74,13 +71,10 @@ public class UpdatePlayListCommandHandler( if (validation.IsError) return validation.Errors; - pictureIntent = validation.Value; + var pictureIntent = validation.Value; playListEntity.Pictures = PlayListPictures.Pending(pictureIntent.ObjectName); - finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); - } + var finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); - if (pictureIntent != null && finalPictureKey != null) - { var publishResult = await PublishUpdatePlayListPictureSourceEventAsync( playListEntity.Id, pictureIntent, finalPictureKey, cancellationToken); if (publishResult.IsError) diff --git a/backend/Core/Musify.Application/Users/GetUsers.cs b/backend/Core/Musify.Application/Users/GetUsers.cs index 2d17ebf..cbe48bb 100644 --- a/backend/Core/Musify.Application/Users/GetUsers.cs +++ b/backend/Core/Musify.Application/Users/GetUsers.cs @@ -17,8 +17,7 @@ public class GetUsersQueryHandler(IDatabase database) public async ValueTask>> Handle(GetUsersQuery request, CancellationToken cancellationToken) { var usersQuery = database.Users - .AsNoTracking() - .AsQueryable(); + .AsNoTracking(); if (!string.IsNullOrEmpty(request.UsernameSearch)) { diff --git a/backend/Core/Musify.Domain/Entities/Upload.cs b/backend/Core/Musify.Domain/Entities/Upload.cs deleted file mode 100644 index 5c8db80..0000000 --- a/backend/Core/Musify.Domain/Entities/Upload.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -using Musify.Domain.ValueObjects; - -namespace Musify.Domain.Entities; - -[Table("Upload")] -public class Upload -{ - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - [Required] - public required string Bucket { get; set; } - - [Required] - public required string Key { get; set; } - - [Required] - public required string ContentType { get; set; } - - [Required] - public DateTime CreatedTime { get; set; } = DateTime.UtcNow; - - [Required] - public UploadState State { get; set; } = UploadState.Pending; -} diff --git a/backend/Core/Musify.Domain/ValueObjects/TrackAudio.cs b/backend/Core/Musify.Domain/ValueObjects/TrackAudio.cs index c1c3148..a558d13 100644 --- a/backend/Core/Musify.Domain/ValueObjects/TrackAudio.cs +++ b/backend/Core/Musify.Domain/ValueObjects/TrackAudio.cs @@ -11,12 +11,6 @@ public class TrackAudio public ProcessingStatus TranscodeStatus { get; set; } = ProcessingStatus.Pending; - public bool DownloadRequested { get; set; } - - public int RetryCount { get; set; } - - public DateTime? LastRetryAt { get; set; } - [MemberNotNullWhen(true, nameof(FolderName))] public bool IsProcessed => TranscodeStatus == ProcessingStatus.Completed; diff --git a/backend/Core/Musify.Domain/ValueObjects/UploadState.cs b/backend/Core/Musify.Domain/ValueObjects/UploadState.cs deleted file mode 100644 index 729378b..0000000 --- a/backend/Core/Musify.Domain/ValueObjects/UploadState.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Musify.Domain.ValueObjects; - -public enum UploadState -{ - Pending, - Failed, - Successful, -} diff --git a/backend/Core/Musify.Infrastructure/Infrastructure.csproj b/backend/Core/Musify.Infrastructure/Infrastructure.csproj index 306716f..a24d3fc 100644 --- a/backend/Core/Musify.Infrastructure/Infrastructure.csproj +++ b/backend/Core/Musify.Infrastructure/Infrastructure.csproj @@ -14,16 +14,18 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + diff --git a/backend/Core/Musify.Infrastructure/Jobs/UploadIntentExpirationJob.cs b/backend/Core/Musify.Infrastructure/Jobs/UploadIntentExpirationJob.cs index c2bc8f0..40cf862 100644 --- a/backend/Core/Musify.Infrastructure/Jobs/UploadIntentExpirationJob.cs +++ b/backend/Core/Musify.Infrastructure/Jobs/UploadIntentExpirationJob.cs @@ -54,20 +54,17 @@ public class UploadIntentExpirationJob( logger.LogInformation("Marked {Count} upload intents as Expired", expiredIntents.Count); } - 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) { - 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); - } + database.UploadIntents.RemoveRange(oldExpiredIntents); + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Deleted {Count} old Expired upload intents past retention window", oldExpiredIntents.Count); } } } diff --git a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Files/DownloadFileFromUrlActivity.cs b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Files/DownloadFileFromUrlActivity.cs deleted file mode 100644 index 192e668..0000000 --- a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Files/DownloadFileFromUrlActivity.cs +++ /dev/null @@ -1,68 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Infrastructure.MassTransit.Arguments; -using Musify.Infrastructure.MassTransit.Logs; - -namespace Musify.Infrastructure.MassTransit.Activities.Files; - -internal class DownloadFileFromUrlActivity( - IHttpClientFactory httpClientFactory, - ILogger logger) - : IActivity -{ - public const string ExecuteEndpointName = "download-file-from-url"; - - public async Task Execute(ExecuteContext executeContext) - { - var destinationPath = executeContext.GetVariable(executeContext.Arguments.DestinationFilePathVariable); - ArgumentNullException.ThrowIfNull(destinationPath, nameof(destinationPath)); - - try - { - var httpClient = httpClientFactory.CreateClient(); - - using var response = await httpClient.GetAsync( - executeContext.Arguments.SourceUrl, - HttpCompletionOption.ResponseHeadersRead, - executeContext.CancellationToken); - response.EnsureSuccessStatusCode(); - - var destinationDirectory = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrWhiteSpace(destinationDirectory)) - Directory.CreateDirectory(destinationDirectory); - - await using (var destinationStream = File.Create(destinationPath)) - { - await response.Content.CopyToAsync(destinationStream, executeContext.CancellationToken); - } - - logger.LogInformation("Downloaded {SourceUrl} to {DestinationPath}", - executeContext.Arguments.SourceUrl, destinationPath); - - return executeContext.Completed(new DownloadFileFromUrlLog( - executeContext.Arguments.SourceUrl, - destinationPath)); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to download file from {SourceUrl}", executeContext.Arguments.SourceUrl); - 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, "Failed to compensate downloaded file {DestinationFilePath}", - compensateContext.Log.DestinationFilePath); - return Task.FromResult(compensateContext.Failed(exception)); - } - } -} diff --git a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateAlbumPictureActivity.cs b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateAlbumPictureActivity.cs index 8d4bf2e..6f9b0a2 100644 --- a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateAlbumPictureActivity.cs +++ b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateAlbumPictureActivity.cs @@ -57,7 +57,6 @@ internal class UpdateAlbumPictureActivity( album.Pictures.MediumName = Path.GetFileName(mediumResizedVariable); album.Pictures.LargeName = Path.GetFileName(largeResizedVariable); - database.Albums.Update(album); await database.SaveChangesAsync(executeContext.CancellationToken); logger.LogInformation("Updated album {AlbumId} pictures", @@ -98,7 +97,6 @@ internal class UpdateAlbumPictureActivity( LargeName = compensateContext.Log.PreviousLargePictureKey }; - database.Albums.Update(album); await database.SaveChangesAsync(compensateContext.CancellationToken); return compensateContext.Compensated(); diff --git a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 36233e6..f5ab673 100644 --- a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -57,7 +57,6 @@ internal class UpdatePlayListPictureActivity( playList.Pictures.MediumName = Path.GetFileName(mediumResizedVariable); playList.Pictures.LargeName = Path.GetFileName(largeResizedVariable); - database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); logger.LogInformation("Updated playlist {PlayListId} pictures", @@ -98,7 +97,6 @@ internal class UpdatePlayListPictureActivity( LargeName = compensateContext.Log.PreviousLargePictureKey }; - database.PlayLists.Update(playList); await database.SaveChangesAsync(compensateContext.CancellationToken); return compensateContext.Compensated(); diff --git a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index ab1999a..e209d28 100644 --- a/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/backend/Core/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -51,7 +51,6 @@ internal class UpdateTrackPictureActivity( track.Pictures.LargeName = Path.GetFileName(largeResizedVariable); track.Pictures.ProcessingStatus = ProcessingStatus.Completed; - database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); logger.LogInformation("Updated track {TrackId} pictures", @@ -88,7 +87,6 @@ internal class UpdateTrackPictureActivity( track.Pictures.LargeName = compensateContext.Log.PreviousLargePictureKey; track.Pictures.ProcessingStatus = ProcessingStatus.Failed; - database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); return compensateContext.Compensated(); diff --git a/backend/Core/Musify.Infrastructure/MassTransit/Arguments/DownloadFileFromUrlArguments.cs b/backend/Core/Musify.Infrastructure/MassTransit/Arguments/DownloadFileFromUrlArguments.cs deleted file mode 100644 index 4784e12..0000000 --- a/backend/Core/Musify.Infrastructure/MassTransit/Arguments/DownloadFileFromUrlArguments.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Musify.Infrastructure.MassTransit.Arguments; - -public record DownloadFileFromUrlArguments( - string SourceUrl, - string DestinationFilePathVariable); diff --git a/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index 678b679..f86a6cc 100644 --- a/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -75,6 +75,5 @@ public static partial class MassTransitDependencyInjection options.AddActivity(); options.AddActivity(); options.AddActivity(); - options.AddActivity(); } } diff --git a/backend/Core/Musify.Infrastructure/MassTransit/Logs/DownloadFileFromUrlLog.cs b/backend/Core/Musify.Infrastructure/MassTransit/Logs/DownloadFileFromUrlLog.cs deleted file mode 100644 index f063578..0000000 --- a/backend/Core/Musify.Infrastructure/MassTransit/Logs/DownloadFileFromUrlLog.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Musify.Infrastructure.MassTransit.Logs; - -public record DownloadFileFromUrlLog( - string SourceUrl, - string DestinationFilePath); diff --git a/backend/Core/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/backend/Core/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 76d0a02..c6ddefd 100644 --- a/backend/Core/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/backend/Core/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -16,7 +16,6 @@ internal static class ActivityNames internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths"; internal const string DownloadFile = "DownloadFile"; - internal const string DownloadThumbnail = "DownloadThumbnail"; internal const string TranscodeAudio = "TranscodeAudio"; diff --git a/backend/Core/Musify.Infrastructure/Persistence/Database.cs b/backend/Core/Musify.Infrastructure/Persistence/Database.cs index 23a7e94..e2b04b2 100644 --- a/backend/Core/Musify.Infrastructure/Persistence/Database.cs +++ b/backend/Core/Musify.Infrastructure/Persistence/Database.cs @@ -8,12 +8,11 @@ using Musify.Domain.ValueObjects; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Sagas; using Npgsql; -using AppIDatabase = Musify.Application.Contracts.IDatabase; namespace Musify.Infrastructure.Persistence; public class Database(DatabaseConfiguration configuration) - : DbContext, AppIDatabase + : DbContext, Musify.Application.Contracts.IDatabase { private static readonly AuditableEntityInterceptor AuditInterceptor = new(); @@ -54,9 +53,6 @@ public class Database(DatabaseConfiguration configuration) audio.Property(a => a.OriginalName).HasColumnName("OriginalAudioName").HasMaxLength(64); audio.Property(a => a.FolderName).HasColumnName("AudioFolderName").HasMaxLength(64); audio.Property(a => a.TranscodeStatus).HasColumnName("AudioTranscodeProcessingStatus"); - audio.Property(a => a.DownloadRequested).HasColumnName("DownloadRequested"); - audio.Property(a => a.RetryCount).HasColumnName("RetryCount"); - audio.Property(a => a.LastRetryAt).HasColumnName("LastRetryAt"); }); modelBuilder.Entity().Navigation(track => track.Audio).IsRequired(); @@ -193,8 +189,6 @@ public class Database(DatabaseConfiguration configuration) public DbSet MixItems => Set(); - public DbSet Uploads => Set(); - public DbSet UploadIntents => Set(); public DbSet ListeningHistories => Set(); diff --git a/backend/Core/Musify.Infrastructure/Persistence/DatabaseDesignTimeFactory.cs b/backend/Core/Musify.Infrastructure/Persistence/DatabaseDesignTimeFactory.cs index a71b19c..b4e68ee 100644 --- a/backend/Core/Musify.Infrastructure/Persistence/DatabaseDesignTimeFactory.cs +++ b/backend/Core/Musify.Infrastructure/Persistence/DatabaseDesignTimeFactory.cs @@ -1,4 +1,3 @@ -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; using Musify.Infrastructure.Configuration; @@ -21,9 +20,6 @@ public class DatabaseDesignTimeFactory : IDesignTimeDbContextFactory .Get() ?? throw new InvalidOperationException($"{DatabaseConfiguration.SectionName} configuration section not found."); - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(databaseConfiguration.ConnectionString); - return new Database(databaseConfiguration); } } diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.Designer.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.Designer.cs new file mode 100644 index 0000000..33f55c8 --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.Designer.cs @@ -0,0 +1,1133 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260924031745_DropUploadTableAndTrackAudioRetryColumns")] + partial class DropUploadTableAndTrackAudioRetryColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" }); + 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.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Albums"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AlbumId") + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("TrackNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("AlbumId", "TrackId") + .IsUnique(); + + b.ToTable("AlbumHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsCounted") + .HasColumnType("boolean"); + + b.Property("LastProgressAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ListenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayedSeconds") + .HasColumnType("double precision"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("ListeningHistory"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Position"); + + b.ToTable("Mixes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MixId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("MixId", "Position"); + + b.ToTable("MixItems"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + 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("TrackId"); + + b.HasIndex("PlayListId", "TrackId") + .IsUnique(); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("IsExplicit") + .HasColumnType("boolean"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId", "TrackId") + .IsUnique(); + + b.ToTable("TrackLikes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Tag") + .HasColumnType("genre"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId", "Tag") + .IsUnique(); + + b.ToTable("TrackTags"); + }); + + 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.UserFollow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FollowedId") + .HasColumnType("bigint"); + + b.Property("FollowerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FollowedId"); + + b.HasIndex("FollowerId", "FollowedId") + .IsUnique(); + + b.ToTable("UserFollows"); + }); + + 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("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("AlbumProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + 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.Album", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.AlbumPictures", "Pictures", b1 => + { + b1.Property("AlbumId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("AlbumId"); + + b1.ToTable("Albums"); + + b1.WithOwner() + .HasForeignKey("AlbumId"); + }); + + b.Navigation("Owner"); + + b.Navigation("Pictures"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Album", "Album") + .WithMany("AlbumTracks") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("AlbumTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("ListeningHistories") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("ListeningHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.HasOne("Musify.Domain.Entities.Mix", "Mix") + .WithMany("Items") + .HasForeignKey("MixId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Mix"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 => + { + b1.Property("PlayListId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures"); + + 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.Track", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("FolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("AudioFolderName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalAudioName"); + + b1.Property("TranscodeStatus") + .HasColumnType("integer") + .HasColumnName("AudioTranscodeProcessingStatus"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackPictures", "Pictures", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("ProcessingStatus") + .HasColumnType("integer") + .HasColumnName("PicturesProcessingStatus"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.Navigation("Audio") + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Pictures") + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("Tags") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.UserFollow", b => + { + b.HasOne("Musify.Domain.Entities.User", "Followed") + .WithMany() + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Follower") + .WithMany() + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + 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.Album", b => + { + b.Navigation("AlbumTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("AlbumTracks"); + + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("Tags"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.cs new file mode 100644 index 0000000..6f91664 --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260924031745_DropUploadTableAndTrackAudioRetryColumns.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class DropUploadTableAndTrackAudioRetryColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Upload"); + + migrationBuilder.DropColumn( + name: "DownloadRequested", + table: "Tracks"); + + migrationBuilder.DropColumn( + name: "LastRetryAt", + table: "Tracks"); + + migrationBuilder.DropColumn( + name: "RetryCount", + table: "Tracks"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DownloadRequested", + table: "Tracks", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LastRetryAt", + table: "Tracks", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryCount", + table: "Tracks", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "Upload", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Bucket = table.Column(type: "text", nullable: false), + ContentType = table.Column(type: "text", nullable: false), + CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), + Key = table.Column(type: "text", nullable: false), + State = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Upload", x => x.Id); + }); + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index c708d49..f543d34 100644 --- a/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -511,35 +511,6 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("TrackTags"); }); - 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") @@ -970,28 +941,16 @@ namespace Musify.Infrastructure.Persistence.Migrations b1.Property("TrackId") .HasColumnType("uuid"); - b1.Property("DownloadRequested") - .HasColumnType("boolean") - .HasColumnName("DownloadRequested"); - b1.Property("FolderName") .HasMaxLength(64) .HasColumnType("character varying(64)") .HasColumnName("AudioFolderName"); - b1.Property("LastRetryAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("LastRetryAt"); - b1.Property("OriginalName") .HasMaxLength(64) .HasColumnType("character varying(64)") .HasColumnName("OriginalAudioName"); - b1.Property("RetryCount") - .HasColumnType("integer") - .HasColumnName("RetryCount"); - b1.Property("TranscodeStatus") .HasColumnType("integer") .HasColumnName("AudioTranscodeProcessingStatus"); diff --git a/backend/Core/Musify.Infrastructure/Services/AudioTranscoderService.cs b/backend/Core/Musify.Infrastructure/Services/AudioTranscoderService.cs index b073cea..bc13de5 100644 --- a/backend/Core/Musify.Infrastructure/Services/AudioTranscoderService.cs +++ b/backend/Core/Musify.Infrastructure/Services/AudioTranscoderService.cs @@ -75,47 +75,6 @@ public partial class AudioService(ILogger logger, return new TimeSpan(hours, minutes, 0) + TimeSpan.FromSeconds(seconds); } - public async Task IsValidAudioFileAsync(string filePath, CancellationToken cancellationToken) - { - if (!File.Exists(filePath)) - { - logger.LogWarning("Audio validation requested for non-existing file {FilePath}", filePath); - return false; - } - - try - { - logger.LogDebug("Validating audio file {FilePath}", filePath); - - 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 {FilePath}: {StandardError}", filePath, executionResult.StandardError); - } - else - { - logger.LogDebug("Audio validation successful for {FilePath}", filePath); - } - - return isValid; - } - catch (TimeoutException timeoutException) - { - logger.LogError(timeoutException, "Audio validation timed out for {FilePath}", filePath); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to validate audio file {FilePath}", filePath); - } - return false; - } - private async Task ExecuteFfmpegWithInputAsync( string arguments, string workingDirectory, @@ -239,9 +198,6 @@ public partial class AudioService(ILogger logger, $" \"{outputPath}\""; } - private static string BuildValidateAudioArguments(string filePath) - => $"-v error -i \"{filePath}\" -map 0:a:0 -f null -"; - private static string BuildProbeDurationArguments(string filePath) => $"-hide_banner -i \"{filePath}\" -f null -"; } diff --git a/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs b/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs index 8149ec8..67e025d 100644 --- a/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs +++ b/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs @@ -39,7 +39,7 @@ public static class ServicesDependencyInjection return serviceDescriptors; } - public static IServiceCollection AddPictureService(this IServiceCollection serviceDescriptors, IConfiguration configuration) + public static IServiceCollection AddPictureService(this IServiceCollection serviceDescriptors) { serviceDescriptors.AddScoped(); return serviceDescriptors; diff --git a/backend/Core/Musify.Infrastructure/Services/SingleFlightCache.cs b/backend/Core/Musify.Infrastructure/Services/SingleFlightCache.cs deleted file mode 100644 index 681080c..0000000 --- a/backend/Core/Musify.Infrastructure/Services/SingleFlightCache.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Concurrent; -using ErrorOr; -using Microsoft.Extensions.Caching.Memory; - -namespace Musify.Infrastructure.Services; - -public sealed class SingleFlightCache(IMemoryCache cache) -{ - private readonly ConcurrentDictionary inFlight = new(); - - public async Task> GetOrCreateAsync( - string key, - TimeSpan duration, - Func>> factory) - { - if (duration <= TimeSpan.Zero) - return await factory(); - - if (cache.TryGetValue(key, out TValue? cached) && cached != null) - return cached; - - var pending = (Lazy>>)inFlight.GetOrAdd( - key, - _ => new Lazy>>(factory)); - - try - { - var result = await pending.Value; - - if (!result.IsError) - cache.Set(key, result.Value, duration); - - return result; - } - finally - { - inFlight.TryRemove(key, out _); - } - } -} diff --git a/backend/Core/Musify.Infrastructure/Services/StorageService.cs b/backend/Core/Musify.Infrastructure/Services/StorageService.cs index 69e9855..57fbecc 100644 --- a/backend/Core/Musify.Infrastructure/Services/StorageService.cs +++ b/backend/Core/Musify.Infrastructure/Services/StorageService.cs @@ -4,14 +4,12 @@ using Amazon.S3.Transfer; using Microsoft.Extensions.Logging; using MimeMapping; using Musify.Application.Contracts; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; using Musify.Infrastructure.Configuration; using ObjectMetaData = Musify.Application.Contracts.ObjectMetaData; namespace Musify.Infrastructure.Services; -public class StorageService(IDatabase database, IAmazonS3 amazonS3, ILogger logger, +public class StorageService(IAmazonS3 amazonS3, ILogger logger, InfrastructureStorageConfiguration storageClientConfiguration) : IStorageService { @@ -41,20 +39,6 @@ public class StorageService(IDatabase database, IAmazonS3 amazonS3, ILogger GetUrlAsync(string bucket, string key, TimeSpan expirationTime, CancellationToken cancellationToken) - { - var request = new GetPreSignedUrlRequest - { - BucketName = bucket, - Key = key, - Expires = DateTime.UtcNow + expirationTime, - Protocol = storageClientConfiguration.UseHttp ? Protocol.HTTP : Protocol.HTTPS - }; - - logger.LogDebug("Generating pre-signed URL for {Bucket}/{Key}", bucket, key); - return await amazonS3.GetPreSignedURLAsync(request); - } - public async Task GetUploadUrlAsync( string bucket, string key, @@ -129,15 +113,6 @@ public class StorageService(IDatabase database, IAmazonS3 amazonS3, ILogger !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 = contentType - }; - await database.Uploads.AddAsync(upload, cancellationToken); - try { await trasnsferUtility.UploadAsync( @@ -149,7 +124,6 @@ public class StorageService(IDatabase database, IAmazonS3 amazonS3, ILogger new KeyVersion { Key = o.Key }) - .ToList() - }; - - var deleteResponse = await amazonS3.DeleteObjectsAsync(deleteRequest, cancellationToken); - - var deleteErrors = deleteResponse.DeleteErrors ?? []; - if (deleteErrors.Count > 0) - { - var errors = string.Join(", ", 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); - } } diff --git a/backend/Hosts/Musify.Api/Api.csproj b/backend/Hosts/Musify.Api/Api.csproj index b693556..ed556be 100644 --- a/backend/Hosts/Musify.Api/Api.csproj +++ b/backend/Hosts/Musify.Api/Api.csproj @@ -8,29 +8,12 @@ - - - - - - - - - - - - - - - - - diff --git a/backend/Hosts/Musify.Api/Authentication/CurrentUser.cs b/backend/Hosts/Musify.Api/Authentication/CurrentUser.cs index 9e3c5bc..b6d15fb 100644 --- a/backend/Hosts/Musify.Api/Authentication/CurrentUser.cs +++ b/backend/Hosts/Musify.Api/Authentication/CurrentUser.cs @@ -7,7 +7,6 @@ public sealed class CurrentUser : IBindableFromHttpContext { private CurrentUser(ClaimsPrincipal principal) { - Principal = principal; IsAuthenticated = principal.Identity?.IsAuthenticated ?? false; var rawId = principal.FindFirstValue(ClaimTypes.NameIdentifier); @@ -19,8 +18,6 @@ public sealed class CurrentUser : IBindableFromHttpContext LastName = principal.FindFirstValue(ClaimTypes.Surname); } - public ClaimsPrincipal Principal { get; } - public bool IsAuthenticated { get; } public long? Id { get; } @@ -33,10 +30,6 @@ public sealed class CurrentUser : IBindableFromHttpContext public long RequiredId => Id ?? throw new InvalidOperationException("User Id claim is missing or invalid."); - public bool HasClaim(string type) => Principal.HasClaim(c => c.Type == type); - - public string? FindClaim(string type) => Principal.FindFirstValue(type); - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => ValueTask.FromResult(new CurrentUser(context.User)); } diff --git a/backend/Hosts/Musify.Api/appsettings.json b/backend/Hosts/Musify.Api/appsettings.json index 4c0c5aa..e771a5e 100644 --- a/backend/Hosts/Musify.Api/appsettings.json +++ b/backend/Hosts/Musify.Api/appsettings.json @@ -116,19 +116,6 @@ "LargePictureHeight": 512 } }, - "AudioTranscoder": { - "TranscodingTimeout": "00:05:00", - "Ffmpeg": { - "ExecutableName": "ffmpeg", - "OutputFileName": "audio.m4a", - "AudioCodec": "aac", - "AudioBitrate": "128k", - "AudioChannels": 2, - "AudioSampleRate": 48000, - "AudioProfile": "aac_low", - "AdditionalArguments": "" - } - }, "UploadIntent": { "UploadUrlExpiresInSeconds": 120, "MaxActiveUploadIntentsPerUser": 5, @@ -141,10 +128,5 @@ "TempRootPrefix": "temp", "TempUploadsRetentionDays": 3, "TempCleanupJobIntervalSeconds": 3600 - }, - "Workers": { - "Routes": { - "TemporaryFilesDirectory": "" - } } } diff --git a/backend/Hosts/Musify.Worker/Program.cs b/backend/Hosts/Musify.Worker/Program.cs index bda423d..92389ba 100644 --- a/backend/Hosts/Musify.Worker/Program.cs +++ b/backend/Hosts/Musify.Worker/Program.cs @@ -23,8 +23,7 @@ services.AddValidatedOptions(configuration, TrackConfigurati services.AddValidatedOptions(configuration, MixConfiguration.SectionName); services.AddStorageService(configuration); services.AddAudioTranscoder(configuration); -services.AddHttpClient(); -services.AddPictureService(configuration); +services.AddPictureService(); services.AddDatabase(configuration); services.AddMassTransitConsumers(configuration); services.AddUploadIntentJobs(configuration); diff --git a/backend/Hosts/Musify.Worker/Worker.csproj b/backend/Hosts/Musify.Worker/Worker.csproj index e264aa3..00dadc0 100644 --- a/backend/Hosts/Musify.Worker/Worker.csproj +++ b/backend/Hosts/Musify.Worker/Worker.csproj @@ -7,17 +7,8 @@ - - - - - - - - - diff --git a/backend/Hosts/Musify.Worker/appsettings.json b/backend/Hosts/Musify.Worker/appsettings.json index 5a93fc4..394b30e 100644 --- a/backend/Hosts/Musify.Worker/appsettings.json +++ b/backend/Hosts/Musify.Worker/appsettings.json @@ -70,9 +70,6 @@ } }, "AudioTranscoder": { - "Routes": { - "WorkingDirectory": "" - }, "TranscodingTimeout": "00:05:00" }, "Workers": { diff --git a/backend/Tests/Musify.Api.Tests/Authentication/CurrentUserTests.cs b/backend/Tests/Musify.Api.Tests/Authentication/CurrentUserTests.cs index 981dda9..0336538 100644 --- a/backend/Tests/Musify.Api.Tests/Authentication/CurrentUserTests.cs +++ b/backend/Tests/Musify.Api.Tests/Authentication/CurrentUserTests.cs @@ -63,22 +63,4 @@ public sealed class CurrentUserTests Assert.Throws(() => user.RequiredId); } - - [Fact] - public async Task HasClaim_ExistingClaimType_ReturnsTrue() - { - var user = await BindAsync(AuthenticatedPrincipal(new Claim("role", "admin"))); - - Assert.True(user.HasClaim("role")); - Assert.False(user.HasClaim("missing")); - } - - [Fact] - public async Task FindClaim_ExistingClaimType_ReturnsItsValue() - { - var user = await BindAsync(AuthenticatedPrincipal(new Claim("picture", "https://avatar.example/jane.png"))); - - Assert.Equal("https://avatar.example/jane.png", user.FindClaim("picture")); - Assert.Null(user.FindClaim("missing")); - } } diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs index c891090..37bab0e 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs @@ -151,8 +151,6 @@ public sealed class TestDatabase : DbContext, IDatabase public DbSet MixItems => Set(); - public DbSet Uploads => Set(); - public DbSet UploadIntents => Set(); public DbSet ListeningHistories => Set(); diff --git a/backend/Tests/Musify.Infrastructure.Tests/Services/SingleFlightCacheTests.cs b/backend/Tests/Musify.Infrastructure.Tests/Services/SingleFlightCacheTests.cs deleted file mode 100644 index da41272..0000000 --- a/backend/Tests/Musify.Infrastructure.Tests/Services/SingleFlightCacheTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using ErrorOr; -using Microsoft.Extensions.Caching.Memory; -using Musify.Infrastructure.Services; -using Xunit; - -namespace Musify.Infrastructure.Tests.Services; - -public sealed class SingleFlightCacheTests -{ - private static SingleFlightCache CreateCache() => new(new MemoryCache(new MemoryCacheOptions())); - - [Fact] - public async Task GetOrCreateAsync_SecondCallWithinDuration_ReturnsCachedValueWithoutCallingFactory() - { - var cache = CreateCache(); - var calls = 0; - Task> Factory() { calls++; return Task.FromResult>(calls); } - - var first = await cache.GetOrCreateAsync("key", TimeSpan.FromMinutes(1), Factory); - var second = await cache.GetOrCreateAsync("key", TimeSpan.FromMinutes(1), Factory); - - Assert.Equal(1, first.Value); - Assert.Equal(1, second.Value); - Assert.Equal(1, calls); - } - - [Fact] - public async Task GetOrCreateAsync_ConcurrentCallsForTheSameKey_CollapseIntoASingleFactoryCall() - { - var cache = CreateCache(); - var calls = 0; - var gate = new TaskCompletionSource(); - - async Task> Factory() - { - Interlocked.Increment(ref calls); - await gate.Task; - return 7; - } - - var first = cache.GetOrCreateAsync("concurrent-key", TimeSpan.FromMinutes(1), Factory); - var second = cache.GetOrCreateAsync("concurrent-key", TimeSpan.FromMinutes(1), Factory); - gate.SetResult(); - - var results = await Task.WhenAll(first, second); - - Assert.Equal(1, calls); - Assert.All(results, result => Assert.Equal(7, result.Value)); - } - - [Fact] - public async Task GetOrCreateAsync_FactoryReturnsError_DoesNotCacheIt() - { - var cache = CreateCache(); - var calls = 0; - Task> Factory() - { - calls++; - return Task.FromResult>(Error.Failure(description: "boom")); - } - - await cache.GetOrCreateAsync("error-key", TimeSpan.FromMinutes(1), Factory); - await cache.GetOrCreateAsync("error-key", TimeSpan.FromMinutes(1), Factory); - - Assert.Equal(2, calls); - } - - [Fact] - public async Task GetOrCreateAsync_NonPositiveDuration_AlwaysCallsTheFactory() - { - var cache = CreateCache(); - var calls = 0; - Task> Factory() { calls++; return Task.FromResult>(calls); } - - await cache.GetOrCreateAsync("no-cache-key", TimeSpan.Zero, Factory); - await cache.GetOrCreateAsync("no-cache-key", TimeSpan.Zero, Factory); - - Assert.Equal(2, calls); - } -} diff --git a/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs b/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs index 80bbcc1..7065485 100644 --- a/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs +++ b/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs @@ -3,7 +3,6 @@ using System.Net.Http.Headers; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.Persistence; using Musify.Infrastructure.Services; using Musify.Infrastructure.Tests.TestSupport; using Xunit; @@ -13,12 +12,10 @@ namespace Musify.Infrastructure.Tests.Services; [Collection(InfrastructureCollection.Name)] public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAsyncLifetime { - private Database database = null!; private StorageService service = null!; public ValueTask InitializeAsync() { - database = fixture.CreateDatabase(); var storageConfiguration = new InfrastructureStorageConfiguration { Address = fixture.S3ServiceUrl, @@ -27,11 +24,11 @@ public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAs ForcePathStyle = true, UseHttp = true, }; - service = new StorageService(database, fixture.CreateS3Client(), NullLogger.Instance, storageConfiguration); + service = new StorageService(fixture.CreateS3Client(), NullLogger.Instance, storageConfiguration); return ValueTask.CompletedTask; } - public ValueTask DisposeAsync() => database.DisposeAsync(); + public ValueTask DisposeAsync() => ValueTask.CompletedTask; [Fact] public async Task UploadThenGetFile_RoundTripsTheSameBytes() @@ -114,21 +111,6 @@ public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAs Assert.Equal([$"{prefix}/a.txt", $"{prefix}/b.txt"], keys.Order()); } - [Fact] - public async Task RemoveFolderAsync_DeletesEveryObjectUnderThePrefix() - { - var prefix = $"tests/{Guid.NewGuid():N}"; - await service.UploadFileAsync(new MemoryStream("a"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/a.txt", TestContext.Current.CancellationToken); - await service.UploadFileAsync(new MemoryStream("b"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/nested/b.txt", TestContext.Current.CancellationToken); - - await service.RemoveFolderAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken); - - var remaining = new List(); - await foreach (var item in service.ListObjectsAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken)) - remaining.Add(item.Key); - Assert.Empty(remaining); - } - [Fact] public async Task GetUploadUrlAsync_PresignedPut_CanActuallyUploadTheObject() { diff --git a/deploy/compose.yml b/deploy/compose.yml index c3b5318..9d7d976 100644 --- a/deploy/compose.yml +++ b/deploy/compose.yml @@ -315,7 +315,6 @@ services: ASPNETCORE_URLS: http://+:5111 StreamGateway__PublicBaseUrl: ${PUBLIC_STREAM_URL} StreamTicket__PrivateKeyPath: /keys/stream_private.pem - Workers__Routes__TemporaryFilesDirectory: /tmp/musify # AUTH_CLIENT_ID is written to the env file by the zitadel-init service. Authentication__MetadataAddress: "${PUBLIC_AUTH_URL}/.well-known/openid-configuration" Authentication__IssuerAddress: ${PUBLIC_AUTH_URL} @@ -344,7 +343,6 @@ services: environment: <<: *backend-env DOTNET_ENVIRONMENT: ${DOTNET_ENVIRONMENT} - AudioTranscoder__Routes__WorkingDirectory: /tmp/musify Workers__Routes__TemporaryFilesDirectory: /tmp/musify restart: unless-stopped diff --git a/docs/architecture.md b/docs/architecture.md index 3af19ac..167b6ed 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -52,4 +52,3 @@ See [authentication.md](authentication.md). - **PlayListHasTrack**: the tracks inside a playlist (with `Position`). - **UserHasTrack**: ownership, which user owns which track. - **UploadIntent**: an upload reservation (key, bucket, expected size, status, expiration) used for validation and quota. -- **Upload**: a record of objects uploaded or transferred to storage. diff --git a/docs/development.md b/docs/development.md index 9f9824b..a2d7a4f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -88,7 +88,7 @@ dotnet test backend/Musify.slnx |---|---|---| | `Musify.Domain.Tests` | Value object logic (`TrackAudio`/`TrackPictures`: `IsProcessed`, `IsFailed`, `IsInProgress`). Entities are mostly anemic, with no behavior of their own beyond that. | No | | `Musify.Application.Tests` | The ~45 handlers and services in `Musify.Application` (Albums, Tracks, PlayLists, Users, Mixes). | No | -| `Musify.Infrastructure.Tests` | `Database`/migrations, `StorageService`, `StreamTicketService`, `AuditableEntityInterceptor`, `PictureService`, `SingleFlightCache`, `PlayListPresetSeeder`. | **Yes** | +| `Musify.Infrastructure.Tests` | `Database`/migrations, `StorageService`, `StreamTicketService`, `AuditableEntityInterceptor`, `PictureService`, `PlayListPresetSeeder`. | **Yes** | | `Musify.Api.Tests` | Real HTTP endpoints (routing, auth, `ValidationFilter`, `ErrorOr`→HTTP mapping) via `WebApplicationFactory`, plus `ErrorOrHttpExtensions`/`CurrentUser`/FluentValidation validators in isolation. | **Yes** | | `Musify.StreamingGateway.Tests` | `TicketValidator` (RS256) and `TicketValidationMiddleware` (traversal, prefix limits, ticket extraction). | No |