From 4fed9bd0595a92da42a27da9ab032c1d72e378fa Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 17 Jul 2026 18:56:23 +0200 Subject: [PATCH] A lot of changes --- .../PlayLists/AddTrackToPlayList.cs | 11 +- .../PlayLists/AddYouTubeTrackToPlayList.cs | 13 +- .../PlayLists/CreatePlayList.cs | 12 +- .../PlayLists/GetPlayListCover.cs | 8 +- .../Responses/PlayListApplicationResponse.cs | 6 +- .../PlayLists/UpdatePlayList.cs | 2 +- .../Services/UploadIntentValidator.cs | 4 +- .../Services/YouTubeTrackProvisioner.cs | 2 +- .../Musify.Application/Tracks/DeleteTrack.cs | 3 +- .../Tracks/GetTrackStream.cs | 8 +- .../Responses/TrackApplicationResponse.cs | 2 +- backend/Musify.Domain/Entities/PlayList.cs | 18 +- backend/Musify.Domain/Entities/Track.cs | 7 +- .../Musify.Domain/Entities/UploadIntent.cs | 6 + .../ValueObjects/PlayListPictures.cs | 13 + .../Musify.Domain/ValueObjects/TrackAudio.cs | 4 + .../Audio/UpdateTrackAudioActivity.cs | 6 +- .../Pictures/UpdatePlayListPictureActivity.cs | 24 +- .../MassTransit/Logs/UpdateTrackAudioLog.cs | 2 +- .../DeletePlayListRoutingSlipBuilder.cs | 16 +- .../Persistence/Database.cs | 10 + ...ovePlayListPicturesToOwnedType.Designer.cs | 885 ++++++++++++++++++ ...7164632_MovePlayListPicturesToOwnedType.cs | 22 + ...TrackDurationToDurationSeconds.Designer.cs | 885 ++++++++++++++++++ ...12_RenameTrackDurationToDurationSeconds.cs | 28 + .../Migrations/DatabaseModelSnapshot.cs | 183 ++-- backend/Musify.Worker/Musify.Worker.csproj | 1 - frontend/src/lib/components/Navbar.svelte | 2 +- frontend/src/lib/components/Sidebar.svelte | 6 +- .../src/routes/(app)/explore/+page.svelte | 13 +- .../src/routes/(app)/library/+page.server.ts | 19 +- .../src/routes/(app)/library/+page.svelte | 79 +- .../src/routes/(app)/playlists/+page.svelte | 6 +- .../(app)/playlists/[id]/+page.server.ts | 2 +- frontend/src/routes/(app)/upload/+page.svelte | 4 +- 35 files changed, 2127 insertions(+), 185 deletions(-) create mode 100644 backend/Musify.Domain/ValueObjects/PlayListPictures.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.Designer.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.Designer.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.cs diff --git a/backend/Musify.Application/PlayLists/AddTrackToPlayList.cs b/backend/Musify.Application/PlayLists/AddTrackToPlayList.cs index 2e81be1..4aa584f 100644 --- a/backend/Musify.Application/PlayLists/AddTrackToPlayList.cs +++ b/backend/Musify.Application/PlayLists/AddTrackToPlayList.cs @@ -49,10 +49,13 @@ namespace Musify.Application.PlayLists return Error.Conflict(description: "Track is already in the playlist."); } - var nextPosition = await database.PlayListHasTracks - .Where(plt => plt.PlayListId == request.PlayListId) - .Select(plt => (int?)plt.Position) - .MaxAsync(cancellationToken) + 1 ?? 0; + var playListTracksQuery = database.PlayListHasTracks + .Where(plt => plt.PlayListId == request.PlayListId); + + var hasTracks = await playListTracksQuery.AnyAsync(cancellationToken); + var nextPosition = hasTracks + ? await playListTracksQuery.MaxAsync(plt => plt.Position, cancellationToken) + 1 + : 0; await database.PlayListHasTracks.AddAsync(new PlayListHasTrack { diff --git a/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs b/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs index e022699..fe4bb4f 100644 --- a/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs +++ b/backend/Musify.Application/PlayLists/AddYouTubeTrackToPlayList.cs @@ -51,7 +51,7 @@ namespace Musify.Application.PlayLists var track = provisionResult.Value.Track; - var failed = track.Audio.TranscodeStatus == ProcessingStatus.Failed; + var failed = track.Audio.IsFailed; var neverQueued = !track.Audio.DownloadRequested && !track.Audio.IsProcessed; if (failed || neverQueued) { @@ -82,10 +82,13 @@ namespace Musify.Application.PlayLists return Error.Conflict(description: "Track is already in the playlist."); } - var nextPosition = await database.PlayListHasTracks - .Where(plt => plt.PlayListId == request.PlayListId) - .Select(plt => (int?)plt.Position) - .MaxAsync(cancellationToken) + 1 ?? 0; + var playListTracksQuery = database.PlayListHasTracks + .Where(plt => plt.PlayListId == request.PlayListId); + + var hasTracks = await playListTracksQuery.AnyAsync(cancellationToken); + var nextPosition = hasTracks + ? await playListTracksQuery.MaxAsync(plt => plt.Position, cancellationToken) + 1 + : 0; await database.PlayListHasTracks.AddAsync(new PlayListHasTrack { diff --git a/backend/Musify.Application/PlayLists/CreatePlayList.cs b/backend/Musify.Application/PlayLists/CreatePlayList.cs index 8d3fe1c..a6dc445 100644 --- a/backend/Musify.Application/PlayLists/CreatePlayList.cs +++ b/backend/Musify.Application/PlayLists/CreatePlayList.cs @@ -8,6 +8,7 @@ using Musify.Application.Events; using Musify.Application.PlayLists.Responses; using Musify.Application.Services; using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; using Musify.Application.Contracts; namespace Musify.Application.PlayLists @@ -65,10 +66,13 @@ namespace Musify.Application.PlayLists Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = request.Description, - OriginalPictureName = originalPictureName, - SmallPictureName = playListConfiguration.Routes.PresetSmallPicture, - MediumPictureName = playListConfiguration.Routes.PresetMediumPicture, - LargePictureName = playListConfiguration.Routes.PresetLargePicture + Pictures = new PlayListPictures + { + OriginalName = originalPictureName, + SmallName = playListConfiguration.Routes.PresetSmallPicture, + MediumName = playListConfiguration.Routes.PresetMediumPicture, + LargeName = playListConfiguration.Routes.PresetLargePicture + } }; await database.PlayLists.AddAsync(playList, cancellationToken); diff --git a/backend/Musify.Application/PlayLists/GetPlayListCover.cs b/backend/Musify.Application/PlayLists/GetPlayListCover.cs index ea3d87b..0aab5ca 100644 --- a/backend/Musify.Application/PlayLists/GetPlayListCover.cs +++ b/backend/Musify.Application/PlayLists/GetPlayListCover.cs @@ -22,7 +22,7 @@ namespace Musify.Application.PlayLists { var playList = await database.PlayLists.AsNoTracking() .Where(p => p.Id == request.PlayListId) - .Select(p => new { p.SmallPictureName, p.MediumPictureName, p.LargePictureName }) + .Select(p => new { p.Pictures.SmallName, p.Pictures.MediumName, p.Pictures.LargeName }) .SingleOrDefaultAsync(cancellationToken); if (playList == null) @@ -31,9 +31,9 @@ namespace Musify.Application.PlayLists var routes = playListConfiguration.Routes; var (name, key, presetName) = request.Size.ToLowerInvariant() switch { - "small" => (playList.SmallPictureName, BuildKey(routes.BuildSmallPicturePath, playList.SmallPictureName), routes.PresetSmallPicture), - "large" => (playList.LargePictureName, BuildKey(routes.BuildLargePicturePath, playList.LargePictureName), routes.PresetLargePicture), - _ => (playList.MediumPictureName, BuildKey(routes.BuildMediumPicturePath, playList.MediumPictureName), routes.PresetMediumPicture) + "small" => (playList.SmallName, BuildKey(routes.BuildSmallPicturePath, playList.SmallName), routes.PresetSmallPicture), + "large" => (playList.LargeName, BuildKey(routes.BuildLargePicturePath, playList.LargeName), routes.PresetLargePicture), + _ => (playList.MediumName, BuildKey(routes.BuildMediumPicturePath, playList.MediumName), routes.PresetMediumPicture) }; if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(key) || name == presetName) diff --git a/backend/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs b/backend/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs index 6a577b4..dc41198 100644 --- a/backend/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs +++ b/backend/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs @@ -18,9 +18,9 @@ namespace Musify.Application.PlayLists.Responses playList.Id, playList.Name, playList.Description, - playList.SmallPictureName, - playList.MediumPictureName, - playList.LargePictureName, + playList.Pictures.SmallName, + playList.Pictures.MediumName, + playList.Pictures.LargeName, playList.CreatedAt, playList.UpdatedAt); } diff --git a/backend/Musify.Application/PlayLists/UpdatePlayList.cs b/backend/Musify.Application/PlayLists/UpdatePlayList.cs index 5586d3d..b5bcb25 100644 --- a/backend/Musify.Application/PlayLists/UpdatePlayList.cs +++ b/backend/Musify.Application/PlayLists/UpdatePlayList.cs @@ -68,7 +68,7 @@ namespace Musify.Application.PlayLists return validation.Errors; pictureIntent = validation.Value; - playListEntity.OriginalPictureName = pictureIntent.ObjectName; + playListEntity.Pictures.OriginalName = pictureIntent.ObjectName; finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); } diff --git a/backend/Musify.Application/Services/UploadIntentValidator.cs b/backend/Musify.Application/Services/UploadIntentValidator.cs index 239b9c1..78339e3 100644 --- a/backend/Musify.Application/Services/UploadIntentValidator.cs +++ b/backend/Musify.Application/Services/UploadIntentValidator.cs @@ -45,10 +45,10 @@ public sealed class UploadIntentValidator( if (intent is null || intent.UserId != userId) return Error.NotFound(description: "Upload intent not found or not accessible."); - if (intent.Status == UploadIntentStatus.Consumed) + if (intent.IsConsumed) return Error.Conflict(description: "Upload intent has already been consumed."); - if (intent.Status == UploadIntentStatus.Expired || intent.ExpiresAt < DateTime.UtcNow) + if (intent.IsExpired) return Error.Validation(description: "Upload intent has expired."); var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); diff --git a/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs b/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs index b516170..4299d90 100644 --- a/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs +++ b/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs @@ -50,7 +50,7 @@ public sealed class YouTubeTrackProvisioner( NormalizedTitle = title.ToUpperInvariant(), Source = TrackSource.YouTube, ExternalId = videoId, - Duration = song.DurationSeconds, + DurationSeconds = song.DurationSeconds, Pictures = new TrackPictures { SmallName = trackConfiguration.Routes.PresetSmallPicture, diff --git a/backend/Musify.Application/Tracks/DeleteTrack.cs b/backend/Musify.Application/Tracks/DeleteTrack.cs index f5f823e..0115b4c 100644 --- a/backend/Musify.Application/Tracks/DeleteTrack.cs +++ b/backend/Musify.Application/Tracks/DeleteTrack.cs @@ -34,8 +34,7 @@ namespace Musify.Application.Tracks return Error.Unauthorized(); } - if (track.Audio.TranscodeStatus == ProcessingStatus.Processing - || track.Audio.TranscodeStatus == ProcessingStatus.Pending) + if (track.Audio.IsInProgress) { 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/GetTrackStream.cs b/backend/Musify.Application/Tracks/GetTrackStream.cs index 03593b4..6643033 100644 --- a/backend/Musify.Application/Tracks/GetTrackStream.cs +++ b/backend/Musify.Application/Tracks/GetTrackStream.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts; using Musify.Application.Services; using Musify.Application.Tracks.Responses; -using Musify.Domain.ValueObjects; namespace Musify.Application.Tracks { @@ -22,20 +21,19 @@ namespace Musify.Application.Tracks { var track = await database.Tracks.AsNoTracking() .Where(t => t.Id == request.TrackId) - .Select(t => new { t.Id, t.Audio.TranscodeStatus, t.Audio.FolderName } ) + .Select(t => new { t.Id, t.Audio }) .SingleOrDefaultAsync(cancellationToken); if (track == null) return Error.NotFound(); - if (string.IsNullOrWhiteSpace(track.FolderName) - || track.TranscodeStatus != ProcessingStatus.Completed) + if (!track.Audio.IsProcessed) { 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.FolderName, request.UserId, cancellationToken); + var response = await streamIssuer.IssueAsync(track.Id, track.Audio.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 76937a8..e121c5c 100644 --- a/backend/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs +++ b/backend/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs @@ -25,7 +25,7 @@ namespace Musify.Application.Tracks.Responses track.Source, track.ExternalId, track.Audio.TranscodeStatus, - track.Duration, + track.DurationSeconds, listensCount, track.OwnerUserId, track.CreatedAt, diff --git a/backend/Musify.Domain/Entities/PlayList.cs b/backend/Musify.Domain/Entities/PlayList.cs index cf3d509..846e4fa 100644 --- a/backend/Musify.Domain/Entities/PlayList.cs +++ b/backend/Musify.Domain/Entities/PlayList.cs @@ -26,23 +26,9 @@ namespace Musify.Domain.Entities [Required] [MaxLength(256)] - public required string Description { get; set; } = "No description was provided."; + public string Description { get; set; } = "No description was provided."; - [Required] - [MaxLength(64)] - public required string OriginalPictureName { get; set; } - - [Required] - [MaxLength(64)] - public required string SmallPictureName { get; set; } - - [Required] - [MaxLength(64)] - public required string MediumPictureName { get; set; } - - [Required] - [MaxLength(64)] - public required string LargePictureName { get; set; } + public required PlayListPictures Pictures { get; set; } [Required] public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; diff --git a/backend/Musify.Domain/Entities/Track.cs b/backend/Musify.Domain/Entities/Track.cs index 4d001fe..3babae3 100644 --- a/backend/Musify.Domain/Entities/Track.cs +++ b/backend/Musify.Domain/Entities/Track.cs @@ -20,7 +20,7 @@ namespace Musify.Domain.Entities public required string NormalizedTitle { get; set; } [Required] - public int Duration { get; set; } + public int DurationSeconds { get; set; } public long? OwnerUserId { get; set; } @@ -43,7 +43,7 @@ namespace Musify.Domain.Entities public TrackAudio Audio { get; set; } = new(); - [ForeignKey(nameof(OwnerUserId))] + [ForeignKey(nameof(OwnerUserId)) ] public User? Owner { get; set; } public ICollection UserTracks { get; set; } = new List(); @@ -53,5 +53,8 @@ namespace Musify.Domain.Entities public ICollection ListeningHistories { get; set; } = new List(); public ICollection TrackArtists { get; set; } = new List(); + + [NotMapped] + public bool IsExternal => Source != TrackSource.Local; } } \ No newline at end of file diff --git a/backend/Musify.Domain/Entities/UploadIntent.cs b/backend/Musify.Domain/Entities/UploadIntent.cs index 4f1d15e..7150dd1 100644 --- a/backend/Musify.Domain/Entities/UploadIntent.cs +++ b/backend/Musify.Domain/Entities/UploadIntent.cs @@ -40,6 +40,12 @@ namespace Musify.Domain.Entities [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [NotMapped] + public bool IsConsumed => Status == UploadIntentStatus.Consumed; + + [NotMapped] + public bool IsExpired => Status == UploadIntentStatus.Expired || ExpiresAt < DateTime.UtcNow; + [ForeignKey(nameof(UserId)) ] public User User { get; set; } = null!; } diff --git a/backend/Musify.Domain/ValueObjects/PlayListPictures.cs b/backend/Musify.Domain/ValueObjects/PlayListPictures.cs new file mode 100644 index 0000000..4248b13 --- /dev/null +++ b/backend/Musify.Domain/ValueObjects/PlayListPictures.cs @@ -0,0 +1,13 @@ +namespace Musify.Domain.ValueObjects +{ + public class PlayListPictures + { + public required string OriginalName { get; set; } + + public required string SmallName { get; set; } + + public required string MediumName { get; set; } + + public required string LargeName { get; set; } + } +} diff --git a/backend/Musify.Domain/ValueObjects/TrackAudio.cs b/backend/Musify.Domain/ValueObjects/TrackAudio.cs index 41c1b05..2333465 100644 --- a/backend/Musify.Domain/ValueObjects/TrackAudio.cs +++ b/backend/Musify.Domain/ValueObjects/TrackAudio.cs @@ -19,5 +19,9 @@ namespace Musify.Domain.ValueObjects [MemberNotNullWhen(true, nameof(FolderName))] public bool IsProcessed => TranscodeStatus == ProcessingStatus.Completed; + + public bool IsFailed => TranscodeStatus == ProcessingStatus.Failed; + + public bool IsInProgress => TranscodeStatus is ProcessingStatus.Pending or ProcessingStatus.Processing; } } diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index 854961a..7d10e2b 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -53,10 +53,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio var log = new UpdateTrackAudioLog( track.Id, track.Audio.FolderName, - track.Duration); + track.DurationSeconds); track.Audio.FolderName = Path.GetFileName(audioFolderKey); - track.Duration = durationSeconds; + track.DurationSeconds = durationSeconds; track.Audio.TranscodeStatus = ProcessingStatus.Completed; await database.SaveChangesAsync(executeContext.CancellationToken); @@ -97,7 +97,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found"); track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName; - track.Duration = compensateContext.Log.PreviousDuration; + track.DurationSeconds = compensateContext.Log.PreviousDurationSeconds; track.Audio.TranscodeStatus = ProcessingStatus.Failed; await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs index 4d38041..8071ddf 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdatePlayListPictureActivity.cs @@ -39,15 +39,15 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures var log = new UpdatePlayListPictureLog( playList.Id, - playList.OriginalPictureName, - playList.SmallPictureName, - playList.MediumPictureName, - playList.LargePictureName); + playList.Pictures.OriginalName, + playList.Pictures.SmallName, + playList.Pictures.MediumName, + playList.Pictures.LargeName); - playList.OriginalPictureName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); - playList.SmallPictureName = Path.GetFileName(smallResizedVariable); - playList.MediumPictureName = Path.GetFileName(mediumResizedVariable); - playList.LargePictureName = Path.GetFileName(largeResizedVariable); + playList.Pictures.OriginalName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); + playList.Pictures.SmallName = Path.GetFileName(smallResizedVariable); + playList.Pictures.MediumName = Path.GetFileName(mediumResizedVariable); + playList.Pictures.LargeName = Path.GetFileName(largeResizedVariable); database.PlayLists.Update(playList); await database.SaveChangesAsync(executeContext.CancellationToken); @@ -82,10 +82,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures return compensateContext.Compensated(); } - playList.OriginalPictureName = compensateContext.Log.PreviousOriginalPictureKey; - playList.SmallPictureName = compensateContext.Log.PreviousSmallPictureKey; - playList.MediumPictureName = compensateContext.Log.PreviousMediumPictureKey; - playList.LargePictureName = compensateContext.Log.PreviousLargePictureKey; + playList.Pictures.OriginalName = compensateContext.Log.PreviousOriginalPictureKey; + playList.Pictures.SmallName = compensateContext.Log.PreviousSmallPictureKey; + playList.Pictures.MediumName = compensateContext.Log.PreviousMediumPictureKey; + playList.Pictures.LargeName = compensateContext.Log.PreviousLargePictureKey; database.PlayLists.Update(playList); await database.SaveChangesAsync(compensateContext.CancellationToken); diff --git a/backend/Musify.Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs b/backend/Musify.Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs index d84853c..5132975 100644 --- a/backend/Musify.Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs +++ b/backend/Musify.Infrastructure/MassTransit/Logs/UpdateTrackAudioLog.cs @@ -3,5 +3,5 @@ public record UpdateTrackAudioLog( Guid TrackId, string? PreviousAudioFolderName, - int PreviousDuration); + int PreviousDurationSeconds); } diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs index 4390061..f67b12c 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs @@ -42,44 +42,44 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders var bucket = storageConfiguration.Bucket; var routes = playListConfiguration.Routes; - if (playList.OriginalPictureName != routes.PresetOriginalPicture) + if (playList.Pictures.OriginalName != routes.PresetOriginalPicture) { routingSlipBuilder.AddActivity( ActivityNames.RemovePlayListOriginalPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - routes.BuildOriginalPicturePath(userId, playList.OriginalPictureName))); + routes.BuildOriginalPicturePath(userId, playList.Pictures.OriginalName))); } - if (playList.SmallPictureName != routes.PresetSmallPicture) + if (playList.Pictures.SmallName != routes.PresetSmallPicture) { routingSlipBuilder.AddActivity( ActivityNames.RemovePlayListSmallPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - routes.BuildSmallPicturePath(playList.SmallPictureName))); + routes.BuildSmallPicturePath(playList.Pictures.SmallName))); } - if (playList.MediumPictureName != routes.PresetMediumPicture) + if (playList.Pictures.MediumName != routes.PresetMediumPicture) { routingSlipBuilder.AddActivity( ActivityNames.RemovePlayListMediumPicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - routes.BuildMediumPicturePath(playList.MediumPictureName))); + routes.BuildMediumPicturePath(playList.Pictures.MediumName))); } - if (playList.LargePictureName != routes.PresetLargePicture) + if (playList.Pictures.LargeName != routes.PresetLargePicture) { routingSlipBuilder.AddActivity( ActivityNames.RemovePlayListLargePicture, EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), new RemoveFileFromBucketArguments( bucket, - routes.BuildLargePicturePath(playList.LargePictureName))); + routes.BuildLargePicturePath(playList.Pictures.LargeName))); } } diff --git a/backend/Musify.Infrastructure/Persistence/Database.cs b/backend/Musify.Infrastructure/Persistence/Database.cs index 3bd9ceb..3c2a268 100644 --- a/backend/Musify.Infrastructure/Persistence/Database.cs +++ b/backend/Musify.Infrastructure/Persistence/Database.cs @@ -96,6 +96,16 @@ namespace Musify.Infrastructure.Persistence .IsUnique() .HasFilter("\"ExternalId\" IS NULL AND \"UserId\" IS NULL"); + modelBuilder.Entity() + .OwnsOne(playList => playList.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); + }); + modelBuilder.Entity().Navigation(playList => playList.Pictures).IsRequired(); + var playListProcessing = modelBuilder.Entity(); playListProcessing.HasKey(state => state.CorrelationId); playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.Designer.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.Designer.cs new file mode 100644 index 0000000..73a8004 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.Designer.cs @@ -0,0 +1,885 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260717164632_MovePlayListPicturesToOwnedType")] + partial class MovePlayListPicturesToOwnedType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.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") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ListenedAt") + .HasColumnType("timestamp with time zone"); + + 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.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("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.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("integer"); + + b.Property("ExternalId") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + 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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("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.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") + .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.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") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + 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"); + + 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.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") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Navigation("TrackArtists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("TrackArtists"); + + 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/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.cs new file mode 100644 index 0000000..55b6c9f --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164632_MovePlayListPicturesToOwnedType.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class MovePlayListPicturesToOwnedType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.Designer.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.Designer.cs new file mode 100644 index 0000000..d44bde1 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.Designer.cs @@ -0,0 +1,885 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260717164812_RenameTrackDurationToDurationSeconds")] + partial class RenameTrackDurationToDurationSeconds + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.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") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ListenedAt") + .HasColumnType("timestamp with time zone"); + + 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.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("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.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("integer"); + + b.Property("ExternalId") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + 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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("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.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") + .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.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") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + 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"); + + 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.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") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Navigation("TrackArtists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("TrackArtists"); + + 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/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.cs new file mode 100644 index 0000000..d652ae4 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260717164812_RenameTrackDurationToDurationSeconds.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class RenameTrackDurationToDurationSeconds : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Duration", + table: "Tracks", + newName: "DurationSeconds"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "DurationSeconds", + table: "Tracks", + newName: "Duration"); + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 4a7e30a..982025a 100644 --- a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -272,19 +272,9 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("LargePictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - b.Property("LifeCycleStatus") .HasColumnType("integer"); - b.Property("MediumPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - b.Property("Name") .IsRequired() .HasMaxLength(50) @@ -295,16 +285,6 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("OriginalPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SmallPictureName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -351,66 +331,27 @@ namespace Musify.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AudioFolderName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AudioTranscodeProcessingStatus") - .HasColumnType("integer"); - b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("DownloadRequested") - .HasColumnType("boolean"); - - b.Property("Duration") + b.Property("DurationSeconds") .HasColumnType("integer"); b.Property("ExternalId") .HasMaxLength(16) .HasColumnType("character varying(16)"); - b.Property("LargePictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("LastRetryAt") - .HasColumnType("timestamp with time zone"); - b.Property("LifeCycleStatus") .HasColumnType("integer"); - b.Property("MediumPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - b.Property("NormalizedTitle") .IsRequired() .HasMaxLength(50) .HasColumnType("character varying(50)"); - b.Property("OriginalAudioName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("OriginalPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - b.Property("OwnerUserId") .HasColumnType("bigint"); - b.Property("PicturesProcessingStatus") - .HasColumnType("integer"); - - b.Property("RetryCount") - .HasColumnType("integer"); - - b.Property("SmallPictureName") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - b.Property("Source") .HasColumnType("integer"); @@ -703,6 +644,46 @@ namespace Musify.Infrastructure.Persistence.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 => + { + b1.Property("PlayListId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures") + .IsRequired(); + b.Navigation("User"); }); @@ -732,7 +713,89 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasForeignKey("OwnerUserId") .OnDelete(DeleteBehavior.Restrict); + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + 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"); + + 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.TrackArtist", b => diff --git a/backend/Musify.Worker/Musify.Worker.csproj b/backend/Musify.Worker/Musify.Worker.csproj index 7e13d15..7f0f4b4 100644 --- a/backend/Musify.Worker/Musify.Worker.csproj +++ b/backend/Musify.Worker/Musify.Worker.csproj @@ -6,7 +6,6 @@ - diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 48b6282..e00308c 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -30,7 +30,7 @@
diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index b6f8c2f..78e6637 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -30,11 +30,11 @@ const mainLinks = [ { href: '/', label: 'Inicio', icon: Home }, { href: '/explore', label: 'Buscar', icon: Search }, - { href: '/playlists', label: 'Mis listas', icon: ListMusic } + { href: '/playlists', label: 'Listas', icon: ListMusic } ]; const secondaryLinks = [ - { href: '/library', label: 'Tus canciones subidas', icon: Library }, + { href: '/library', label: 'Canciones subidas', icon: Library }, { href: '/upload', label: 'Subir música', icon: Upload } ]; @@ -65,7 +65,7 @@ diff --git a/frontend/src/routes/(app)/explore/+page.svelte b/frontend/src/routes/(app)/explore/+page.svelte index f5844db..3b97bfd 100644 --- a/frontend/src/routes/(app)/explore/+page.svelte +++ b/frontend/src/routes/(app)/explore/+page.svelte @@ -396,17 +396,16 @@
- - Añadir a una playlist + Añadir a una playlist
- {playlist.name} + {playlist.name} {/each} diff --git a/frontend/src/routes/(app)/library/+page.server.ts b/frontend/src/routes/(app)/library/+page.server.ts index 5f4bb59..39ddd42 100644 --- a/frontend/src/routes/(app)/library/+page.server.ts +++ b/frontend/src/routes/(app)/library/+page.server.ts @@ -1,5 +1,5 @@ -import type { PageServerLoad } from './$types'; -import { error } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { error, fail } from '@sveltejs/kit'; import { createApiClient } from '$lib/server/api'; const PAGE_SIZE = 50; @@ -16,3 +16,18 @@ export const load: PageServerLoad = async ({ locals, fetch }) => { return { tracks: data }; }; + +export const actions: Actions = { + deleteTrack: async ({ request, locals, fetch }) => { + if (!locals.accessToken) return fail(401, { message: 'Inicia sesión.' }); + const trackId = String((await request.formData()).get('trackId') ?? ''); + if (!trackId) return fail(400, { message: 'Falta la canción.' }); + + const api = createApiClient({ fetch, accessToken: locals.accessToken }); + const { error: err } = await api.DELETE('/tracks/{trackId}', { + params: { path: { trackId } } + }); + if (err) return fail(502, { message: 'No se pudo borrar la canción.' }); + return { deleted: true }; + } +}; diff --git a/frontend/src/routes/(app)/library/+page.svelte b/frontend/src/routes/(app)/library/+page.svelte index 1e6de6c..b6c124e 100644 --- a/frontend/src/routes/(app)/library/+page.svelte +++ b/frontend/src/routes/(app)/library/+page.svelte @@ -1,8 +1,9 @@ @@ -53,50 +54,66 @@ {#if items.length > 0}
+ # Título - Duración - Escuchas + Subida + Escuchas + Duración +
-
+
{#each items as track, i (track.id)}
+ - +
+ {dateFormatter.format(new Date(track.createdAt))} +
+ + {Number(track.listensCount)} + + {fmtTime(Number(track.duration))} -
- - {Number(track.listensCount)} -
+
async ({ update }) => update()}> + + +
{/each}
diff --git a/frontend/src/routes/(app)/playlists/+page.svelte b/frontend/src/routes/(app)/playlists/+page.svelte index b5f3102..5b9f229 100644 --- a/frontend/src/routes/(app)/playlists/+page.svelte +++ b/frontend/src/routes/(app)/playlists/+page.svelte @@ -130,14 +130,14 @@ if (e.key === 'Escape') creating = false; }} > -
+

Nueva lista

@@ -222,7 +222,7 @@ diff --git a/frontend/src/routes/(app)/playlists/[id]/+page.server.ts b/frontend/src/routes/(app)/playlists/[id]/+page.server.ts index dfd734a..f185d0d 100644 --- a/frontend/src/routes/(app)/playlists/[id]/+page.server.ts +++ b/frontend/src/routes/(app)/playlists/[id]/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => { params: { path: { playlistId: params.id }, query: { pageNumber: 1, pageSize: 200 } } }), api.GET('/tracks/users/{userId}', { - params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 200 } } + params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } } }) ]); diff --git a/frontend/src/routes/(app)/upload/+page.svelte b/frontend/src/routes/(app)/upload/+page.svelte index 68b519c..cd6282f 100644 --- a/frontend/src/routes/(app)/upload/+page.svelte +++ b/frontend/src/routes/(app)/upload/+page.svelte @@ -160,7 +160,7 @@ @@ -311,7 +311,7 @@