From daec1a8d05cb816c47590f24f218fc64b1e884d8 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 17 Jul 2026 17:58:56 +0200 Subject: [PATCH] refactor(backend): group track pipeline state into owned types Track mixed catalog fields (Title, Artist credit, Duration) with picture-processing and audio-transcode pipeline state as ten flat columns plus two computed [NotMapped] flags, making the entity read like a media-pipeline worksheet. Introduce TrackPictures and TrackAudio as EF owned types mapped onto the same existing columns (explicit column names, no schema change - verified via `dotnet ef migrations has-pending-model-changes`). Track now reads as Title/Artist/Duration/Pictures/Audio instead of fourteen loose properties. --- .../PlayLists/AddYouTubeTrackToPlayList.cs | 14 +- .../Services/YouTubeTrackProvisioner.cs | 20 +-- .../Musify.Application/Tracks/CreateTrack.cs | 20 ++- .../Musify.Application/Tracks/DeleteTrack.cs | 4 +- .../Tracks/GetTrackCover.cs | 8 +- .../Tracks/GetTrackStream.cs | 8 +- .../Responses/TrackApplicationResponse.cs | 2 +- .../YouTube/ResolveYouTubeTrackStream.cs | 4 +- backend/Musify.Domain/Entities/Track.cs | 47 +------ .../Musify.Domain/ValueObjects/TrackAudio.cs | 23 ++++ .../ValueObjects/TrackPictures.cs | 23 ++++ .../Audio/DownloadYouTubeAudioActivity.cs | 2 +- .../GenerateAudioWorkflowPathsActivity.cs | 4 +- .../Audio/TranscodeAudioActivity.cs | 2 +- .../Audio/UpdateTrackAudioActivity.cs | 12 +- .../Pictures/UpdateTrackPictureActivity.cs | 28 ++-- .../Builders/DeleteTrackRoutingSlipBuilder.cs | 20 +-- .../Persistence/Database.cs | 23 ++++ .../Migrations/DatabaseModelSnapshot.cs | 130 ++++++++++++++++-- 19 files changed, 270 insertions(+), 124 deletions(-) create mode 100644 backend/Musify.Domain/ValueObjects/TrackAudio.cs create mode 100644 backend/Musify.Domain/ValueObjects/TrackPictures.cs diff --git a/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs b/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs index 75b1bd0..e022699 100644 --- a/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs +++ b/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs @@ -51,19 +51,19 @@ namespace Musify.Application.PlayLists var track = provisionResult.Value.Track; - var failed = track.AudioTranscodeProcessingStatus == ProcessingStatus.Failed; - var neverQueued = !track.DownloadRequested && !track.IsAudioProcessed; + var failed = track.Audio.TranscodeStatus == ProcessingStatus.Failed; + var neverQueued = !track.Audio.DownloadRequested && !track.Audio.IsProcessed; if (failed || neverQueued) { if (failed) { - track.AudioTranscodeProcessingStatus = ProcessingStatus.Pending; - track.PicturesProcessingStatus = ProcessingStatus.Pending; + track.Audio.TranscodeStatus = ProcessingStatus.Pending; + track.Pictures.ProcessingStatus = ProcessingStatus.Pending; track.LifeCycleStatus = LifeCycleStatus.Active; - track.RetryCount += 1; - track.LastRetryAt = DateTime.UtcNow; + track.Audio.RetryCount += 1; + track.Audio.LastRetryAt = DateTime.UtcNow; } - track.DownloadRequested = true; + track.Audio.DownloadRequested = true; var thumbnailUrl = await ResolveThumbnailUrlAsync(request.VideoId, provisionResult.Value.Song, cancellationToken); if (thumbnailUrl.IsError) diff --git a/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs b/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs index ecfd252..f2ef0d4 100644 --- a/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs +++ b/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs @@ -50,15 +50,19 @@ public sealed class YouTubeTrackProvisioner( NormalizedTitle = title.ToUpperInvariant(), Source = TrackSource.YouTube, ExternalId = videoId, - DownloadRequested = false, Duration = song.DurationSeconds, - OriginalPictureName = null, - OriginalAudioName = null, - SmallPictureName = trackConfiguration.Routes.PresetSmallPicture, - MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, - LargePictureName = trackConfiguration.Routes.PresetLargePicture, - PicturesProcessingStatus = ProcessingStatus.Pending, - AudioTranscodeProcessingStatus = ProcessingStatus.Pending + Pictures = new TrackPictures + { + SmallName = trackConfiguration.Routes.PresetSmallPicture, + MediumName = trackConfiguration.Routes.PresetMediumPicture, + LargeName = trackConfiguration.Routes.PresetLargePicture, + ProcessingStatus = ProcessingStatus.Pending + }, + Audio = new TrackAudio + { + DownloadRequested = false, + TranscodeStatus = ProcessingStatus.Pending + } }; await database.Tracks.AddAsync(track, cancellationToken); diff --git a/backend/Musify.Application/Tracks/CreateTrack.cs b/backend/Musify.Application/Tracks/CreateTrack.cs index e73c9da..3add270 100644 --- a/backend/Musify.Application/Tracks/CreateTrack.cs +++ b/backend/Musify.Application/Tracks/CreateTrack.cs @@ -66,13 +66,19 @@ namespace Musify.Application.Tracks Title = request.Title, NormalizedTitle = request.Title.ToUpperInvariant(), OwnerUserId = user.Id, - OriginalPictureName = pictureIntent.ObjectName, - OriginalAudioName = audioIntent.ObjectName, - SmallPictureName = trackConfiguration.Routes.PresetSmallPicture, - MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, - LargePictureName = trackConfiguration.Routes.PresetLargePicture, - PicturesProcessingStatus = ProcessingStatus.Pending, - AudioTranscodeProcessingStatus = ProcessingStatus.Pending + Pictures = new TrackPictures + { + OriginalName = pictureIntent.ObjectName, + SmallName = trackConfiguration.Routes.PresetSmallPicture, + MediumName = trackConfiguration.Routes.PresetMediumPicture, + LargeName = trackConfiguration.Routes.PresetLargePicture, + ProcessingStatus = ProcessingStatus.Pending + }, + Audio = new TrackAudio + { + OriginalName = audioIntent.ObjectName, + TranscodeStatus = ProcessingStatus.Pending + } }; await database.Tracks.AddAsync(trackEntity, cancellationToken); diff --git a/backend/Musify.Application/Tracks/DeleteTrack.cs b/backend/Musify.Application/Tracks/DeleteTrack.cs index ed24c8d..f5f823e 100644 --- a/backend/Musify.Application/Tracks/DeleteTrack.cs +++ b/backend/Musify.Application/Tracks/DeleteTrack.cs @@ -34,8 +34,8 @@ namespace Musify.Application.Tracks return Error.Unauthorized(); } - if (track.AudioTranscodeProcessingStatus == ProcessingStatus.Processing - || track.AudioTranscodeProcessingStatus == ProcessingStatus.Pending) + if (track.Audio.TranscodeStatus == ProcessingStatus.Processing + || track.Audio.TranscodeStatus == ProcessingStatus.Pending) { logger.LogWarning("Track {TrackId} is currently being processed and cannot be deleted", request.TrackId); return Error.Conflict(description: "Track is currently being processed and cannot be deleted"); diff --git a/backend/Musify.Application/Tracks/GetTrackCover.cs b/backend/Musify.Application/Tracks/GetTrackCover.cs index 903f8a6..917ff46 100644 --- a/backend/Musify.Application/Tracks/GetTrackCover.cs +++ b/backend/Musify.Application/Tracks/GetTrackCover.cs @@ -21,7 +21,7 @@ namespace Musify.Application.Tracks { var track = await database.Tracks.AsNoTracking() .Where(t => t.Id == request.TrackId) - .Select(t => new { t.SmallPictureName, t.MediumPictureName, t.LargePictureName }) + .Select(t => new { t.Pictures.SmallName, t.Pictures.MediumName, t.Pictures.LargeName }) .SingleOrDefaultAsync(cancellationToken); if (track == null) @@ -30,9 +30,9 @@ namespace Musify.Application.Tracks var routes = trackConfiguration.Routes; var (name, key) = request.Size.ToLowerInvariant() switch { - "small" => (track.SmallPictureName, BuildKey(routes.BuildSmallPicturePath, track.SmallPictureName)), - "large" => (track.LargePictureName, BuildKey(routes.BuildLargePicturePath, track.LargePictureName)), - _ => (track.MediumPictureName, BuildKey(routes.BuildMediumPicturePath, track.MediumPictureName)) + "small" => (track.SmallName, BuildKey(routes.BuildSmallPicturePath, track.SmallName)), + "large" => (track.LargeName, BuildKey(routes.BuildLargePicturePath, track.LargeName)), + _ => (track.MediumName, BuildKey(routes.BuildMediumPicturePath, track.MediumName)) }; if (string.IsNullOrEmpty(name) || key == null) diff --git a/backend/Musify.Application/Tracks/GetTrackStream.cs b/backend/Musify.Application/Tracks/GetTrackStream.cs index 1790a62..03593b4 100644 --- a/backend/Musify.Application/Tracks/GetTrackStream.cs +++ b/backend/Musify.Application/Tracks/GetTrackStream.cs @@ -22,20 +22,20 @@ namespace Musify.Application.Tracks { var track = await database.Tracks.AsNoTracking() .Where(t => t.Id == request.TrackId) - .Select(t => new { t.Id, t.AudioTranscodeProcessingStatus, t.AudioFolderName } ) + .Select(t => new { t.Id, t.Audio.TranscodeStatus, t.Audio.FolderName } ) .SingleOrDefaultAsync(cancellationToken); if (track == null) return Error.NotFound(); - if (string.IsNullOrWhiteSpace(track.AudioFolderName) - || track.AudioTranscodeProcessingStatus != ProcessingStatus.Completed) + if (string.IsNullOrWhiteSpace(track.FolderName) + || track.TranscodeStatus != ProcessingStatus.Completed) { logger.LogInformation("Stream requested for track {TrackId} but audio is not ready", request.TrackId); return Error.Conflict(description: "Track audio is not available for streaming yet."); } - var response = await streamIssuer.IssueAsync(track.Id, track.AudioFolderName, request.UserId, cancellationToken); + var response = await streamIssuer.IssueAsync(track.Id, track.FolderName, request.UserId, cancellationToken); logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId); diff --git a/backend/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs b/backend/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs index 2466cd6..76937a8 100644 --- a/backend/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs +++ b/backend/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs @@ -24,7 +24,7 @@ namespace Musify.Application.Tracks.Responses FormatArtist(track), track.Source, track.ExternalId, - track.AudioTranscodeProcessingStatus, + track.Audio.TranscodeStatus, track.Duration, listensCount, track.OwnerUserId, diff --git a/backend/Musify.Application/YouTube/ResolveYouTubeTrackStream.cs b/backend/Musify.Application/YouTube/ResolveYouTubeTrackStream.cs index f3ee205..f4d9555 100644 --- a/backend/Musify.Application/YouTube/ResolveYouTubeTrackStream.cs +++ b/backend/Musify.Application/YouTube/ResolveYouTubeTrackStream.cs @@ -24,9 +24,9 @@ namespace Musify.Application.YouTube { var track = await provisioner.FindAsync(request.VideoId, cancellationToken); - if (track is not null && track.IsAudioProcessed) + if (track is not null && track.Audio.IsProcessed) { - var serverStream = await streamIssuer.IssueAsync(track.Id, track.AudioFolderName, request.UserId, cancellationToken); + var serverStream = await streamIssuer.IssueAsync(track.Id, track.Audio.FolderName, request.UserId, cancellationToken); return new YouTubeStreamResponse( YouTubeStreamMode.Server, serverStream.ManifestUrl, diff --git a/backend/Musify.Domain/Entities/Track.cs b/backend/Musify.Domain/Entities/Track.cs index 183158a..4d001fe 100644 --- a/backend/Musify.Domain/Entities/Track.cs +++ b/backend/Musify.Domain/Entities/Track.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Diagnostics.CodeAnalysis; using Musify.Domain.Abstractions; using Musify.Domain.ValueObjects; @@ -31,47 +30,19 @@ namespace Musify.Domain.Entities [MaxLength(16)] public string? ExternalId { get; set; } - [Required] - public bool DownloadRequested { get; set; } = false; - - [MaxLength(64)] - public string? OriginalPictureName { get; set; } - - [MaxLength(64)] - public string? SmallPictureName { get; set; } - - [MaxLength(64)] - public string? MediumPictureName { get; set; } - - [MaxLength(64)] - public string? LargePictureName { get; set; } - - [MaxLength(64)] - public string? OriginalAudioName { get; set; } - - [MaxLength(64)] - public string? AudioFolderName { get; set; } - - [Required] - public ProcessingStatus PicturesProcessingStatus { get; set; } = ProcessingStatus.Pending; - - [Required] - public ProcessingStatus AudioTranscodeProcessingStatus { get; set; } = ProcessingStatus.Pending; - [Required] public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; - [Required] - public int RetryCount { get; set; } = 0; - - public DateTime? LastRetryAt { get; set; } - [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [Required] public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public TrackPictures Pictures { get; set; } = new(); + + public TrackAudio Audio { get; set; } = new(); + [ForeignKey(nameof(OwnerUserId))] public User? Owner { get; set; } @@ -82,15 +53,5 @@ namespace Musify.Domain.Entities public ICollection ListeningHistories { get; set; } = new List(); public ICollection TrackArtists { 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/backend/Musify.Domain/ValueObjects/TrackAudio.cs b/backend/Musify.Domain/ValueObjects/TrackAudio.cs new file mode 100644 index 0000000..41c1b05 --- /dev/null +++ b/backend/Musify.Domain/ValueObjects/TrackAudio.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Musify.Domain.ValueObjects +{ +#pragma warning disable CS8618 + public class TrackAudio + { + public string? OriginalName { get; set; } + + public string? FolderName { get; set; } + + 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/Musify.Domain/ValueObjects/TrackPictures.cs b/backend/Musify.Domain/ValueObjects/TrackPictures.cs new file mode 100644 index 0000000..143d9da --- /dev/null +++ b/backend/Musify.Domain/ValueObjects/TrackPictures.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Musify.Domain.ValueObjects +{ +#pragma warning disable CS8618 + public class TrackPictures + { + public string? OriginalName { get; set; } + + public string? SmallName { get; set; } + + public string? MediumName { get; set; } + + public string? LargeName { get; set; } + + public ProcessingStatus ProcessingStatus { get; set; } = ProcessingStatus.Pending; + + [MemberNotNullWhen(true, nameof(SmallName))] + [MemberNotNullWhen(true, nameof(MediumName))] + [MemberNotNullWhen(true, nameof(LargeName))] + public bool IsProcessed => ProcessingStatus == ProcessingStatus.Completed; + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/DownloadYouTubeAudioActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/DownloadYouTubeAudioActivity.cs index c2b5738..7337a2e 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/DownloadYouTubeAudioActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/DownloadYouTubeAudioActivity.cs @@ -62,7 +62,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio try { - track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + track.Audio.TranscodeStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(executeContext.CancellationToken); } catch (Exception dbException) diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs index 1db4ba5..85b5216 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/GenerateAudioWorkflowPathsActivity.cs @@ -32,7 +32,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio try { - track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing; + track.Audio.TranscodeStatus = ProcessingStatus.Processing; var folderName = Guid.NewGuid().ToString(); var sourceFileName = Path.GetFileName(executeContext.Arguments.SourceKey); var workingDirectory = Path.Combine(executeContext.Arguments.TemporaryRootDirectory, folderName); @@ -57,7 +57,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio try { - track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + track.Audio.TranscodeStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(executeContext.CancellationToken); } catch (Exception dbException) diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs index ef5a86b..c09875c 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/TranscodeAudioActivity.cs @@ -64,7 +64,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio try { - track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + track.Audio.TranscodeStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(executeContext.CancellationToken); } catch (Exception dbException) diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 4fffae6..854961a 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -52,12 +52,12 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio var log = new UpdateTrackAudioLog( track.Id, - track.AudioFolderName, + track.Audio.FolderName, track.Duration); - track.AudioFolderName = Path.GetFileName(audioFolderKey); + track.Audio.FolderName = Path.GetFileName(audioFolderKey); track.Duration = durationSeconds; - track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; + track.Audio.TranscodeStatus = ProcessingStatus.Completed; await database.SaveChangesAsync(executeContext.CancellationToken); logger.LogInformation("Updated track {TrackId} audio", executeContext.Arguments.TrackId); @@ -75,7 +75,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio try { - track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + track.Audio.TranscodeStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(executeContext.CancellationToken); } catch (Exception dbException) @@ -96,9 +96,9 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio if (track is null) throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found"); - track.AudioFolderName = compensateContext.Log.PreviousAudioFolderName; + track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName; track.Duration = compensateContext.Log.PreviousDuration; - track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed; + track.Audio.TranscodeStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(compensateContext.CancellationToken); logger.LogInformation("Compensated track {TrackId} audio", compensateContext.Log.TrackId); diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 068b44b..6de5103 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -40,16 +40,16 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures var log = new UpdateTrackPictureLog( track.Id, - track.OriginalPictureName, - track.SmallPictureName, - track.MediumPictureName, - track.LargePictureName); + track.Pictures.OriginalName, + track.Pictures.SmallName, + track.Pictures.MediumName, + track.Pictures.LargeName); - track.OriginalPictureName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); - track.SmallPictureName = Path.GetFileName(smallResizedVariable); - track.MediumPictureName = Path.GetFileName(mediumResizedVariable); - track.LargePictureName = Path.GetFileName(largeResizedVariable); - track.PicturesProcessingStatus = ProcessingStatus.Completed; + track.Pictures.OriginalName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); + track.Pictures.SmallName = Path.GetFileName(smallResizedVariable); + track.Pictures.MediumName = Path.GetFileName(mediumResizedVariable); + track.Pictures.LargeName = Path.GetFileName(largeResizedVariable); + track.Pictures.ProcessingStatus = ProcessingStatus.Completed; database.Tracks.Update(track); await database.SaveChangesAsync(executeContext.CancellationToken); @@ -82,11 +82,11 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures 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; + track.Pictures.OriginalName = compensateContext.Log.PreviousOriginalPictureKey; + track.Pictures.SmallName = compensateContext.Log.PreviousSmallPictureKey; + track.Pictures.MediumName = compensateContext.Log.PreviousMediumPictureKey; + track.Pictures.LargeName = compensateContext.Log.PreviousLargePictureKey; + track.Pictures.ProcessingStatus = ProcessingStatus.Failed; database.Tracks.Update(track); await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs index 3f5aaca..b5d9800 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs @@ -41,58 +41,58 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders { var bucket = storageConfiguration.Bucket; - if (!string.IsNullOrEmpty(track.OriginalPictureName)) + if (!string.IsNullOrEmpty(track.Pictures.OriginalName)) { routingSlipBuilder.AddActivity( ActivityNames.RemoveTrackOriginalPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName))); + trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName))); } - if (track.IsPicturesProcessed) + if (track.Pictures.IsProcessed) { routingSlipBuilder.AddActivity( ActivityNames.RemoveTrackSmallPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName))); + trackConfiguration.Routes.BuildSmallPicturePath(track.Pictures.SmallName))); routingSlipBuilder.AddActivity( ActivityNames.RemoveTrackMediumPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName))); + trackConfiguration.Routes.BuildMediumPicturePath(track.Pictures.MediumName))); routingSlipBuilder.AddActivity( ActivityNames.RemoveTrackLargePicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName))); + trackConfiguration.Routes.BuildLargePicturePath(track.Pictures.LargeName))); } - if (!string.IsNullOrEmpty(track.OriginalAudioName)) + if (!string.IsNullOrEmpty(track.Audio.OriginalName)) { routingSlipBuilder.AddActivity( ActivityNames.RemoveTrackOriginalAudio, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName))); + trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.Audio.OriginalName))); } - if (track.IsAudioProcessed) + if (track.Audio.IsProcessed) { routingSlipBuilder.AddActivity( ActivityNames.RemoveTrackProcessedAudio, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName))); + trackConfiguration.Routes.BuildProcessedAudioPath(track.Audio.FolderName))); } } diff --git a/backend/Musify.Infrastructure/Persistence/Database.cs b/backend/Musify.Infrastructure/Persistence/Database.cs index 3ab2357..3bd9ceb 100644 --- a/backend/Musify.Infrastructure/Persistence/Database.cs +++ b/backend/Musify.Infrastructure/Persistence/Database.cs @@ -45,6 +45,29 @@ namespace Musify.Infrastructure.Persistence "CK_Tracks_OwnerUserId_Source", "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)")); + modelBuilder.Entity() + .OwnsOne(track => track.Pictures, pictures => + { + pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64); + pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64); + pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64); + pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64); + pictures.Property(p => p.ProcessingStatus).HasColumnName("PicturesProcessingStatus"); + }); + modelBuilder.Entity().Navigation(track => track.Pictures).IsRequired(); + + modelBuilder.Entity() + .OwnsOne(track => track.Audio, audio => + { + 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(); + modelBuilder.Entity() .HasKey(trackArtist => new { trackArtist.TrackId, trackArtist.ArtistId }); diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index d0ca149..4a7e30a 100644 --- a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -190,6 +190,50 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("OutboxState"); }); + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExternalId") + .IsUnique(); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasFilter("\"ExternalId\" IS NULL AND \"UserId\" IS NULL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Artists"); + }); + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => { b.Property("Id") @@ -307,11 +351,6 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Artist") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - b.Property("AudioFolderName") .HasMaxLength(64) .HasColumnType("character varying(64)"); @@ -329,7 +368,6 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasColumnType("integer"); b.Property("ExternalId") - .IsRequired() .HasMaxLength(16) .HasColumnType("character varying(16)"); @@ -353,15 +391,16 @@ namespace Musify.Infrastructure.Persistence.Migrations .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("OwnerUserId") + .HasColumnType("bigint"); + b.Property("PicturesProcessingStatus") .HasColumnType("integer"); @@ -385,11 +424,33 @@ namespace Musify.Infrastructure.Persistence.Migrations b.HasKey("Id"); - b.HasIndex("Source", "ExternalId") - .IsUnique() - .HasFilter("\"ExternalId\" <> ''"); + b.HasIndex("OwnerUserId"); - b.ToTable("Tracks"); + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("Tracks", t => + { + t.HasCheckConstraint("CK_Tracks_OwnerUserId_Source", "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)"); + }); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.HasKey("TrackId", "ArtistId"); + + b.HasIndex("ArtistId"); + + b.ToTable("TrackArtist"); }); modelBuilder.Entity("Musify.Domain.Entities.Upload", b => @@ -606,6 +667,15 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasPrincipalKey("MessageId", "ConsumerId"); }); + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => { b.HasOne("Musify.Domain.Entities.Track", "Track") @@ -655,6 +725,35 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("Track"); }); + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.HasOne("Musify.Domain.Entities.Artist", "Artist") + .WithMany("TrackArtists") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("TrackArtists") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + + b.Navigation("Track"); + }); + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => { b.HasOne("Musify.Domain.Entities.User", "User") @@ -685,6 +784,11 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Navigation("TrackArtists"); + }); + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => { b.Navigation("PlayListTracks"); @@ -696,6 +800,8 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("PlayListTracks"); + b.Navigation("TrackArtists"); + b.Navigation("UserTracks"); });