A lot of changes

This commit is contained in:
2026-07-17 18:56:23 +02:00
parent aac9224c2d
commit 4fed9bd059
35 changed files with 2127 additions and 185 deletions
@@ -49,10 +49,13 @@ namespace Musify.Application.PlayLists
return Error.Conflict(description: "Track is already in the playlist."); return Error.Conflict(description: "Track is already in the playlist.");
} }
var nextPosition = await database.PlayListHasTracks var playListTracksQuery = database.PlayListHasTracks
.Where(plt => plt.PlayListId == request.PlayListId) .Where(plt => plt.PlayListId == request.PlayListId);
.Select(plt => (int?)plt.Position)
.MaxAsync(cancellationToken) + 1 ?? 0; var hasTracks = await playListTracksQuery.AnyAsync(cancellationToken);
var nextPosition = hasTracks
? await playListTracksQuery.MaxAsync(plt => plt.Position, cancellationToken) + 1
: 0;
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
{ {
@@ -51,7 +51,7 @@ namespace Musify.Application.PlayLists
var track = provisionResult.Value.Track; 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; var neverQueued = !track.Audio.DownloadRequested && !track.Audio.IsProcessed;
if (failed || neverQueued) if (failed || neverQueued)
{ {
@@ -82,10 +82,13 @@ namespace Musify.Application.PlayLists
return Error.Conflict(description: "Track is already in the playlist."); return Error.Conflict(description: "Track is already in the playlist.");
} }
var nextPosition = await database.PlayListHasTracks var playListTracksQuery = database.PlayListHasTracks
.Where(plt => plt.PlayListId == request.PlayListId) .Where(plt => plt.PlayListId == request.PlayListId);
.Select(plt => (int?)plt.Position)
.MaxAsync(cancellationToken) + 1 ?? 0; var hasTracks = await playListTracksQuery.AnyAsync(cancellationToken);
var nextPosition = hasTracks
? await playListTracksQuery.MaxAsync(plt => plt.Position, cancellationToken) + 1
: 0;
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
{ {
@@ -8,6 +8,7 @@ using Musify.Application.Events;
using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Responses;
using Musify.Application.Services; using Musify.Application.Services;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using Musify.Application.Contracts; using Musify.Application.Contracts;
namespace Musify.Application.PlayLists namespace Musify.Application.PlayLists
@@ -65,10 +66,13 @@ namespace Musify.Application.PlayLists
Name = request.Name, Name = request.Name,
NormalizedName = request.Name.Trim().ToUpperInvariant(), NormalizedName = request.Name.Trim().ToUpperInvariant(),
Description = request.Description, Description = request.Description,
OriginalPictureName = originalPictureName, Pictures = new PlayListPictures
SmallPictureName = playListConfiguration.Routes.PresetSmallPicture, {
MediumPictureName = playListConfiguration.Routes.PresetMediumPicture, OriginalName = originalPictureName,
LargePictureName = playListConfiguration.Routes.PresetLargePicture SmallName = playListConfiguration.Routes.PresetSmallPicture,
MediumName = playListConfiguration.Routes.PresetMediumPicture,
LargeName = playListConfiguration.Routes.PresetLargePicture
}
}; };
await database.PlayLists.AddAsync(playList, cancellationToken); await database.PlayLists.AddAsync(playList, cancellationToken);
@@ -22,7 +22,7 @@ namespace Musify.Application.PlayLists
{ {
var playList = await database.PlayLists.AsNoTracking() var playList = await database.PlayLists.AsNoTracking()
.Where(p => p.Id == request.PlayListId) .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); .SingleOrDefaultAsync(cancellationToken);
if (playList == null) if (playList == null)
@@ -31,9 +31,9 @@ namespace Musify.Application.PlayLists
var routes = playListConfiguration.Routes; var routes = playListConfiguration.Routes;
var (name, key, presetName) = request.Size.ToLowerInvariant() switch var (name, key, presetName) = request.Size.ToLowerInvariant() switch
{ {
"small" => (playList.SmallPictureName, BuildKey(routes.BuildSmallPicturePath, playList.SmallPictureName), routes.PresetSmallPicture), "small" => (playList.SmallName, BuildKey(routes.BuildSmallPicturePath, playList.SmallName), routes.PresetSmallPicture),
"large" => (playList.LargePictureName, BuildKey(routes.BuildLargePicturePath, playList.LargePictureName), routes.PresetLargePicture), "large" => (playList.LargeName, BuildKey(routes.BuildLargePicturePath, playList.LargeName), routes.PresetLargePicture),
_ => (playList.MediumPictureName, BuildKey(routes.BuildMediumPicturePath, playList.MediumPictureName), routes.PresetMediumPicture) _ => (playList.MediumName, BuildKey(routes.BuildMediumPicturePath, playList.MediumName), routes.PresetMediumPicture)
}; };
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(key) || name == presetName) if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(key) || name == presetName)
@@ -18,9 +18,9 @@ namespace Musify.Application.PlayLists.Responses
playList.Id, playList.Id,
playList.Name, playList.Name,
playList.Description, playList.Description,
playList.SmallPictureName, playList.Pictures.SmallName,
playList.MediumPictureName, playList.Pictures.MediumName,
playList.LargePictureName, playList.Pictures.LargeName,
playList.CreatedAt, playList.CreatedAt,
playList.UpdatedAt); playList.UpdatedAt);
} }
@@ -68,7 +68,7 @@ namespace Musify.Application.PlayLists
return validation.Errors; return validation.Errors;
pictureIntent = validation.Value; pictureIntent = validation.Value;
playListEntity.OriginalPictureName = pictureIntent.ObjectName; playListEntity.Pictures.OriginalName = pictureIntent.ObjectName;
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
} }
@@ -45,10 +45,10 @@ public sealed class UploadIntentValidator(
if (intent is null || intent.UserId != userId) if (intent is null || intent.UserId != userId)
return Error.NotFound(description: "Upload intent not found or not accessible."); 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."); 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."); return Error.Validation(description: "Upload intent has expired.");
var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken);
@@ -50,7 +50,7 @@ public sealed class YouTubeTrackProvisioner(
NormalizedTitle = title.ToUpperInvariant(), NormalizedTitle = title.ToUpperInvariant(),
Source = TrackSource.YouTube, Source = TrackSource.YouTube,
ExternalId = videoId, ExternalId = videoId,
Duration = song.DurationSeconds, DurationSeconds = song.DurationSeconds,
Pictures = new TrackPictures Pictures = new TrackPictures
{ {
SmallName = trackConfiguration.Routes.PresetSmallPicture, SmallName = trackConfiguration.Routes.PresetSmallPicture,
@@ -34,8 +34,7 @@ namespace Musify.Application.Tracks
return Error.Unauthorized(); return Error.Unauthorized();
} }
if (track.Audio.TranscodeStatus == ProcessingStatus.Processing if (track.Audio.IsInProgress)
|| track.Audio.TranscodeStatus == ProcessingStatus.Pending)
{ {
logger.LogWarning("Track {TrackId} is currently being processed and cannot be deleted", request.TrackId); 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"); return Error.Conflict(description: "Track is currently being processed and cannot be deleted");
@@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Services; using Musify.Application.Services;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Domain.ValueObjects;
namespace Musify.Application.Tracks namespace Musify.Application.Tracks
{ {
@@ -22,20 +21,19 @@ namespace Musify.Application.Tracks
{ {
var track = await database.Tracks.AsNoTracking() var track = await database.Tracks.AsNoTracking()
.Where(t => t.Id == request.TrackId) .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); .SingleOrDefaultAsync(cancellationToken);
if (track == null) if (track == null)
return Error.NotFound(); return Error.NotFound();
if (string.IsNullOrWhiteSpace(track.FolderName) if (!track.Audio.IsProcessed)
|| track.TranscodeStatus != ProcessingStatus.Completed)
{ {
logger.LogInformation("Stream requested for track {TrackId} but audio is not ready", request.TrackId); 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."); 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); logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId);
@@ -25,7 +25,7 @@ namespace Musify.Application.Tracks.Responses
track.Source, track.Source,
track.ExternalId, track.ExternalId,
track.Audio.TranscodeStatus, track.Audio.TranscodeStatus,
track.Duration, track.DurationSeconds,
listensCount, listensCount,
track.OwnerUserId, track.OwnerUserId,
track.CreatedAt, track.CreatedAt,
+2 -16
View File
@@ -26,23 +26,9 @@ namespace Musify.Domain.Entities
[Required] [Required]
[MaxLength(256)] [MaxLength(256)]
public required string Description { get; set; } = "No description was provided."; public string Description { get; set; } = "No description was provided.";
[Required] public required PlayListPictures Pictures { get; set; }
[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; }
[Required] [Required]
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
+5 -2
View File
@@ -20,7 +20,7 @@ namespace Musify.Domain.Entities
public required string NormalizedTitle { get; set; } public required string NormalizedTitle { get; set; }
[Required] [Required]
public int Duration { get; set; } public int DurationSeconds { get; set; }
public long? OwnerUserId { get; set; } public long? OwnerUserId { get; set; }
@@ -43,7 +43,7 @@ namespace Musify.Domain.Entities
public TrackAudio Audio { get; set; } = new(); public TrackAudio Audio { get; set; } = new();
[ForeignKey(nameof(OwnerUserId))] [ForeignKey(nameof(OwnerUserId)) ]
public User? Owner { get; set; } public User? Owner { get; set; }
public ICollection<UserHasTrack> UserTracks { get; set; } = new List<UserHasTrack>(); public ICollection<UserHasTrack> UserTracks { get; set; } = new List<UserHasTrack>();
@@ -53,5 +53,8 @@ namespace Musify.Domain.Entities
public ICollection<ListeningHistory> ListeningHistories { get; set; } = new List<ListeningHistory>(); public ICollection<ListeningHistory> ListeningHistories { get; set; } = new List<ListeningHistory>();
public ICollection<TrackArtist> TrackArtists { get; set; } = new List<TrackArtist>(); public ICollection<TrackArtist> TrackArtists { get; set; } = new List<TrackArtist>();
[NotMapped]
public bool IsExternal => Source != TrackSource.Local;
} }
} }
@@ -40,6 +40,12 @@ namespace Musify.Domain.Entities
[Required] [Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; 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)) ] [ForeignKey(nameof(UserId)) ]
public User User { get; set; } = null!; public User User { get; set; } = null!;
} }
@@ -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; }
}
}
@@ -19,5 +19,9 @@ namespace Musify.Domain.ValueObjects
[MemberNotNullWhen(true, nameof(FolderName))] [MemberNotNullWhen(true, nameof(FolderName))]
public bool IsProcessed => TranscodeStatus == ProcessingStatus.Completed; public bool IsProcessed => TranscodeStatus == ProcessingStatus.Completed;
public bool IsFailed => TranscodeStatus == ProcessingStatus.Failed;
public bool IsInProgress => TranscodeStatus is ProcessingStatus.Pending or ProcessingStatus.Processing;
} }
} }
@@ -53,10 +53,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio
var log = new UpdateTrackAudioLog( var log = new UpdateTrackAudioLog(
track.Id, track.Id,
track.Audio.FolderName, track.Audio.FolderName,
track.Duration); track.DurationSeconds);
track.Audio.FolderName = Path.GetFileName(audioFolderKey); track.Audio.FolderName = Path.GetFileName(audioFolderKey);
track.Duration = durationSeconds; track.DurationSeconds = durationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Completed; track.Audio.TranscodeStatus = ProcessingStatus.Completed;
await database.SaveChangesAsync(executeContext.CancellationToken); 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"); throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found");
track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName; track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName;
track.Duration = compensateContext.Log.PreviousDuration; track.DurationSeconds = compensateContext.Log.PreviousDurationSeconds;
track.Audio.TranscodeStatus = ProcessingStatus.Failed; track.Audio.TranscodeStatus = ProcessingStatus.Failed;
await database.SaveChangesAsync(compensateContext.CancellationToken); await database.SaveChangesAsync(compensateContext.CancellationToken);
@@ -39,15 +39,15 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
var log = new UpdatePlayListPictureLog( var log = new UpdatePlayListPictureLog(
playList.Id, playList.Id,
playList.OriginalPictureName, playList.Pictures.OriginalName,
playList.SmallPictureName, playList.Pictures.SmallName,
playList.MediumPictureName, playList.Pictures.MediumName,
playList.LargePictureName); playList.Pictures.LargeName);
playList.OriginalPictureName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey); playList.Pictures.OriginalName = Path.GetFileName(executeContext.Arguments.OriginalPictureKey);
playList.SmallPictureName = Path.GetFileName(smallResizedVariable); playList.Pictures.SmallName = Path.GetFileName(smallResizedVariable);
playList.MediumPictureName = Path.GetFileName(mediumResizedVariable); playList.Pictures.MediumName = Path.GetFileName(mediumResizedVariable);
playList.LargePictureName = Path.GetFileName(largeResizedVariable); playList.Pictures.LargeName = Path.GetFileName(largeResizedVariable);
database.PlayLists.Update(playList); database.PlayLists.Update(playList);
await database.SaveChangesAsync(executeContext.CancellationToken); await database.SaveChangesAsync(executeContext.CancellationToken);
@@ -82,10 +82,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
return compensateContext.Compensated(); return compensateContext.Compensated();
} }
playList.OriginalPictureName = compensateContext.Log.PreviousOriginalPictureKey; playList.Pictures.OriginalName = compensateContext.Log.PreviousOriginalPictureKey;
playList.SmallPictureName = compensateContext.Log.PreviousSmallPictureKey; playList.Pictures.SmallName = compensateContext.Log.PreviousSmallPictureKey;
playList.MediumPictureName = compensateContext.Log.PreviousMediumPictureKey; playList.Pictures.MediumName = compensateContext.Log.PreviousMediumPictureKey;
playList.LargePictureName = compensateContext.Log.PreviousLargePictureKey; playList.Pictures.LargeName = compensateContext.Log.PreviousLargePictureKey;
database.PlayLists.Update(playList); database.PlayLists.Update(playList);
await database.SaveChangesAsync(compensateContext.CancellationToken); await database.SaveChangesAsync(compensateContext.CancellationToken);
@@ -3,5 +3,5 @@
public record UpdateTrackAudioLog( public record UpdateTrackAudioLog(
Guid TrackId, Guid TrackId,
string? PreviousAudioFolderName, string? PreviousAudioFolderName,
int PreviousDuration); int PreviousDurationSeconds);
} }
@@ -42,44 +42,44 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
var bucket = storageConfiguration.Bucket; var bucket = storageConfiguration.Bucket;
var routes = playListConfiguration.Routes; var routes = playListConfiguration.Routes;
if (playList.OriginalPictureName != routes.PresetOriginalPicture) if (playList.Pictures.OriginalName != routes.PresetOriginalPicture)
{ {
routingSlipBuilder.AddActivity( routingSlipBuilder.AddActivity(
ActivityNames.RemovePlayListOriginalPicture, ActivityNames.RemovePlayListOriginalPicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments( new RemoveFileFromBucketArguments(
bucket, 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( routingSlipBuilder.AddActivity(
ActivityNames.RemovePlayListSmallPicture, ActivityNames.RemovePlayListSmallPicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments( new RemoveFileFromBucketArguments(
bucket, bucket,
routes.BuildSmallPicturePath(playList.SmallPictureName))); routes.BuildSmallPicturePath(playList.Pictures.SmallName)));
} }
if (playList.MediumPictureName != routes.PresetMediumPicture) if (playList.Pictures.MediumName != routes.PresetMediumPicture)
{ {
routingSlipBuilder.AddActivity( routingSlipBuilder.AddActivity(
ActivityNames.RemovePlayListMediumPicture, ActivityNames.RemovePlayListMediumPicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments( new RemoveFileFromBucketArguments(
bucket, bucket,
routes.BuildMediumPicturePath(playList.MediumPictureName))); routes.BuildMediumPicturePath(playList.Pictures.MediumName)));
} }
if (playList.LargePictureName != routes.PresetLargePicture) if (playList.Pictures.LargeName != routes.PresetLargePicture)
{ {
routingSlipBuilder.AddActivity( routingSlipBuilder.AddActivity(
ActivityNames.RemovePlayListLargePicture, ActivityNames.RemovePlayListLargePicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments( new RemoveFileFromBucketArguments(
bucket, bucket,
routes.BuildLargePicturePath(playList.LargePictureName))); routes.BuildLargePicturePath(playList.Pictures.LargeName)));
} }
} }
@@ -96,6 +96,16 @@ namespace Musify.Infrastructure.Persistence
.IsUnique() .IsUnique()
.HasFilter("\"ExternalId\" IS NULL AND \"UserId\" IS NULL"); .HasFilter("\"ExternalId\" IS NULL AND \"UserId\" IS NULL");
modelBuilder.Entity<PlayList>()
.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<PlayList>().Navigation(playList => playList.Pictures).IsRequired();
var playListProcessing = modelBuilder.Entity<PlayListProcessingState>(); var playListProcessing = modelBuilder.Entity<PlayListProcessingState>();
playListProcessing.HasKey(state => state.CorrelationId); playListProcessing.HasKey(state => state.CorrelationId);
playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever();
@@ -0,0 +1,885 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("Consumed")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("ConsumerId")
.HasColumnType("uuid");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<int>("ReceiveCount")
.HasColumnType("integer");
b.Property<DateTime>("Received")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("Id");
b.HasIndex("Delivered");
b.ToTable("InboxState");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.Property<long>("SequenceNumber")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceNumber"));
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid?>("ConversationId")
.HasColumnType("uuid");
b.Property<Guid?>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("DestinationAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("EnqueueTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("FaultAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Headers")
.HasColumnType("text");
b.Property<Guid?>("InboxConsumerId")
.HasColumnType("uuid");
b.Property<Guid?>("InboxMessageId")
.HasColumnType("uuid");
b.Property<Guid?>("InitiatorId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("OutboxId")
.HasColumnType("uuid");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<Guid?>("RequestId")
.HasColumnType("uuid");
b.Property<string>("ResponseAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("SentTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("OutboxId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("OutboxId");
b.HasIndex("Created");
b.ToTable("OutboxState");
});
modelBuilder.Entity("Musify.Domain.Entities.Artist", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExternalId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ListenedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("TrackId");
b.HasIndex("UserId");
b.ToTable("ListeningHistory");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PlayLists");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("AddedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("PlayListId")
.HasColumnType("uuid");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PlayListId");
b.HasIndex("TrackId");
b.ToTable("PlayListHasTrack");
});
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("ExternalId")
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
b.Property<string>("NormalizedTitle")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<long?>("OwnerUserId")
.HasColumnType("bigint");
b.Property<int>("Source")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("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<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<Guid>("ArtistId")
.HasColumnType("uuid");
b.Property<int>("Position")
.HasColumnType("integer");
b.HasKey("TrackId", "ArtistId");
b.HasIndex("ArtistId");
b.ToTable("TrackArtist");
});
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Bucket")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<int>("State")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Upload");
});
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Bucket")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long?>("ExpectedSizeBytes")
.HasColumnType("bigint");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("ObjectName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<int>("Purpose")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UploadIntents");
});
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FirstName")
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("ProfilePictureUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("SecondName")
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("User");
});
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<long>("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<Guid>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("Bucket")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CurrentState")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("PictureKey")
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("CorrelationId");
b.ToTable("PlayListProcessingState");
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b =>
{
b.Property<Guid>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("AudioKey")
.HasColumnType("text");
b.Property<bool>("AudioProcessed")
.HasColumnType("boolean");
b.Property<string>("Bucket")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CurrentState")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("PictureKey")
.HasColumnType("text");
b.Property<bool>("PictureProcessed")
.HasColumnType("boolean");
b.Property<DateTime>("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<Guid>("PlayListId")
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<string>("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<Guid>("TrackId")
.HasColumnType("uuid");
b1.Property<bool>("DownloadRequested")
.HasColumnType("boolean")
.HasColumnName("DownloadRequested");
b1.Property<string>("FolderName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("AudioFolderName");
b1.Property<DateTime?>("LastRetryAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("LastRetryAt");
b1.Property<string>("OriginalName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalAudioName");
b1.Property<int>("RetryCount")
.HasColumnType("integer")
.HasColumnName("RetryCount");
b1.Property<int>("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<Guid>("TrackId")
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<int>("ProcessingStatus")
.HasColumnType("integer")
.HasColumnName("PicturesProcessingStatus");
b1.Property<string>("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
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MovePlayListPicturesToOwnedType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -0,0 +1,885 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("Consumed")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("ConsumerId")
.HasColumnType("uuid");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<int>("ReceiveCount")
.HasColumnType("integer");
b.Property<DateTime>("Received")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("Id");
b.HasIndex("Delivered");
b.ToTable("InboxState");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.Property<long>("SequenceNumber")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceNumber"));
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid?>("ConversationId")
.HasColumnType("uuid");
b.Property<Guid?>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("DestinationAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("EnqueueTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("FaultAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Headers")
.HasColumnType("text");
b.Property<Guid?>("InboxConsumerId")
.HasColumnType("uuid");
b.Property<Guid?>("InboxMessageId")
.HasColumnType("uuid");
b.Property<Guid?>("InitiatorId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("OutboxId")
.HasColumnType("uuid");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<Guid?>("RequestId")
.HasColumnType("uuid");
b.Property<string>("ResponseAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("SentTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("OutboxId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("OutboxId");
b.HasIndex("Created");
b.ToTable("OutboxState");
});
modelBuilder.Entity("Musify.Domain.Entities.Artist", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExternalId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ListenedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("TrackId");
b.HasIndex("UserId");
b.ToTable("ListeningHistory");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PlayLists");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("AddedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("PlayListId")
.HasColumnType("uuid");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PlayListId");
b.HasIndex("TrackId");
b.ToTable("PlayListHasTrack");
});
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("DurationSeconds")
.HasColumnType("integer");
b.Property<string>("ExternalId")
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
b.Property<string>("NormalizedTitle")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<long?>("OwnerUserId")
.HasColumnType("bigint");
b.Property<int>("Source")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("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<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<Guid>("ArtistId")
.HasColumnType("uuid");
b.Property<int>("Position")
.HasColumnType("integer");
b.HasKey("TrackId", "ArtistId");
b.HasIndex("ArtistId");
b.ToTable("TrackArtist");
});
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Bucket")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<int>("State")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Upload");
});
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Bucket")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long?>("ExpectedSizeBytes")
.HasColumnType("bigint");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("ObjectName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<int>("Purpose")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UploadIntents");
});
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FirstName")
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("ProfilePictureUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("SecondName")
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("User");
});
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<long>("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<Guid>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("Bucket")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CurrentState")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("PictureKey")
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("CorrelationId");
b.ToTable("PlayListProcessingState");
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b =>
{
b.Property<Guid>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("AudioKey")
.HasColumnType("text");
b.Property<bool>("AudioProcessed")
.HasColumnType("boolean");
b.Property<string>("Bucket")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CurrentState")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("PictureKey")
.HasColumnType("text");
b.Property<bool>("PictureProcessed")
.HasColumnType("boolean");
b.Property<DateTime>("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<Guid>("PlayListId")
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<string>("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<Guid>("TrackId")
.HasColumnType("uuid");
b1.Property<bool>("DownloadRequested")
.HasColumnType("boolean")
.HasColumnName("DownloadRequested");
b1.Property<string>("FolderName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("AudioFolderName");
b1.Property<DateTime?>("LastRetryAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("LastRetryAt");
b1.Property<string>("OriginalName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalAudioName");
b1.Property<int>("RetryCount")
.HasColumnType("integer")
.HasColumnName("RetryCount");
b1.Property<int>("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<Guid>("TrackId")
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<int>("ProcessingStatus")
.HasColumnType("integer")
.HasColumnName("PicturesProcessingStatus");
b1.Property<string>("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
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RenameTrackDurationToDurationSeconds : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Duration",
table: "Tracks",
newName: "DurationSeconds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "DurationSeconds",
table: "Tracks",
newName: "Duration");
}
}
}
@@ -272,19 +272,9 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<string>("LargePictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("LifeCycleStatus") b.Property<int>("LifeCycleStatus")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("MediumPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
@@ -295,16 +285,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("character varying(50)"); .HasColumnType("character varying(50)");
b.Property<string>("OriginalPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("SmallPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@@ -351,66 +331,27 @@ namespace Musify.Infrastructure.Persistence.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<string>("AudioFolderName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("AudioTranscodeProcessingStatus")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<bool>("DownloadRequested") b.Property<int>("DurationSeconds")
.HasColumnType("boolean");
b.Property<int>("Duration")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("ExternalId") b.Property<string>("ExternalId")
.HasMaxLength(16) .HasMaxLength(16)
.HasColumnType("character varying(16)"); .HasColumnType("character varying(16)");
b.Property<string>("LargePictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime?>("LastRetryAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("LifeCycleStatus") b.Property<int>("LifeCycleStatus")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("MediumPictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("NormalizedTitle") b.Property<string>("NormalizedTitle")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("character varying(50)"); .HasColumnType("character varying(50)");
b.Property<string>("OriginalAudioName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("OriginalPictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<long?>("OwnerUserId") b.Property<long?>("OwnerUserId")
.HasColumnType("bigint"); .HasColumnType("bigint");
b.Property<int>("PicturesProcessingStatus")
.HasColumnType("integer");
b.Property<int>("RetryCount")
.HasColumnType("integer");
b.Property<string>("SmallPictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("Source") b.Property<int>("Source")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -703,6 +644,46 @@ namespace Musify.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 =>
{
b1.Property<Guid>("PlayListId")
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<string>("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"); b.Navigation("User");
}); });
@@ -732,7 +713,89 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasForeignKey("OwnerUserId") .HasForeignKey("OwnerUserId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 =>
{
b1.Property<Guid>("TrackId")
.HasColumnType("uuid");
b1.Property<bool>("DownloadRequested")
.HasColumnType("boolean")
.HasColumnName("DownloadRequested");
b1.Property<string>("FolderName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("AudioFolderName");
b1.Property<DateTime?>("LastRetryAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("LastRetryAt");
b1.Property<string>("OriginalName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalAudioName");
b1.Property<int>("RetryCount")
.HasColumnType("integer")
.HasColumnName("RetryCount");
b1.Property<int>("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<Guid>("TrackId")
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<int>("ProcessingStatus")
.HasColumnType("integer")
.HasColumnName("PicturesProcessingStatus");
b1.Property<string>("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("Owner");
b.Navigation("Pictures")
.IsRequired();
}); });
modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b =>
@@ -6,7 +6,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AWSSDK.S3" /> <PackageReference Include="AWSSDK.S3" />
<PackageReference Include="DispatchR.Mediator" />
<PackageReference Include="MassTransit" /> <PackageReference Include="MassTransit" />
<PackageReference Include="MassTransit.RabbitMQ" /> <PackageReference Include="MassTransit.RabbitMQ" />
<PackageReference Include="Microsoft.EntityFrameworkCore" /> <PackageReference Include="Microsoft.EntityFrameworkCore" />
+1 -1
View File
@@ -30,7 +30,7 @@
</a> </a>
<form method="POST" action="/logout"> <form method="POST" action="/logout">
<button <button
class="rounded-full border border-white/20 px-4 py-2 text-sm font-semibold text-white backdrop-blur-md transition hover:bg-white/5" class="rounded-full border border-white/10 px-4 py-2 text-sm text-white/65 transition hover:bg-white/2.5"
> >
Salir Salir
</button> </button>
+3 -3
View File
@@ -30,11 +30,11 @@
const mainLinks = [ const mainLinks = [
{ href: '/', label: 'Inicio', icon: Home }, { href: '/', label: 'Inicio', icon: Home },
{ href: '/explore', label: 'Buscar', icon: Search }, { href: '/explore', label: 'Buscar', icon: Search },
{ href: '/playlists', label: 'Mis listas', icon: ListMusic } { href: '/playlists', label: 'Listas', icon: ListMusic }
]; ];
const secondaryLinks = [ 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 } { href: '/upload', label: 'Subir música', icon: Upload }
]; ];
@@ -65,7 +65,7 @@
<a <a
href={link.href} href={link.href}
data-active={active} data-active={active}
class="relative flex items-center gap-3 rounded-[10px] px-3 py-2.5 text-[14px] font-semibold transition-colors duration-100 {active class="relative flex items-center gap-3 rounded-[10px] px-3 py-2.5 font-semibold transition-colors duration-100 {active
? 'text-[var(--mf-text)]' ? 'text-[var(--mf-text)]'
: 'text-[var(--mf-text-2)] hover:bg-white/5 hover:text-[var(--mf-text)]'}" : 'text-[var(--mf-text-2)] hover:bg-white/5 hover:text-[var(--mf-text)]'}"
> >
@@ -396,17 +396,16 @@
<button <button
type="button" type="button"
onclick={() => addToQueue(contextMenu)} onclick={() => addToQueue(contextMenu)}
class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-semibold text-[var(--mf-text-2)] transition hover:bg-white/5" class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-sm text-[var(--mf-text-2)] transition hover:bg-white/5"
> >
<ListPlus class="h-[18px] w-[18px] shrink-0" strokeWidth={2} /> <ListPlus class="h-6 w-6 shrink-0" strokeWidth={2} />
<span class="text-base">Añadir a la cola</span> <span class="text-base">Añadir a la cola</span>
</button> </button>
<div class="group/addmenu relative"> <div class="group/addmenu relative">
<div <div
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-semibold text-[var(--mf-text-2)] transition group-hover/addmenu:bg-white/5" class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-sm text-[var(--mf-text-2)] transition group-hover/addmenu:bg-white/5"
> >
<ListMusic class="h-[18px] w-[18px] shrink-0" strokeWidth={2} /> <span class="flex-1 text-base cursor-default">Añadir a una playlist</span>
<span class="flex-1">Añadir a una playlist</span>
<ChevronRight class="h-4 w-4 shrink-0 text-[var(--mf-text-3)]" /> <ChevronRight class="h-4 w-4 shrink-0 text-[var(--mf-text-3)]" />
</div> </div>
<div <div
@@ -445,9 +444,9 @@
trackIds={[]} trackIds={[]}
hue={hueFor(playlist.id)} hue={hueFor(playlist.id)}
size="small" size="small"
class="h-11 w-11 flex-shrink-0 rounded-lg" class="h-8 w-8 flex-shrink-0 rounded-lg"
/> />
<span class="truncate">{playlist.name}</span> <span class="truncate text-base text-[var(--mf-text-2)]">{playlist.name}</span>
</button> </button>
</form> </form>
{/each} {/each}
@@ -1,5 +1,5 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { error } from '@sveltejs/kit'; import { error, fail } from '@sveltejs/kit';
import { createApiClient } from '$lib/server/api'; import { createApiClient } from '$lib/server/api';
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
@@ -16,3 +16,18 @@ export const load: PageServerLoad = async ({ locals, fetch }) => {
return { tracks: data }; 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 };
}
};
+47 -30
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms';
import Upload from '@lucide/svelte/icons/upload'; import Upload from '@lucide/svelte/icons/upload';
import Headphones from '@lucide/svelte/icons/headphones'; import Play from '@lucide/svelte/icons/play';
import Clock from '@lucide/svelte/icons/clock'; import X from '@lucide/svelte/icons/x';
import { player } from '$lib/player/player.svelte'; import { player, queueIdForTrack, toQueueItems } from '$lib/player/player.svelte';
import { HUES, fmtTime } from '$lib/theme/color'; import { HUES, fmtTime } from '$lib/theme/color';
import Cover from '$lib/components/ui/Cover.svelte'; import Cover from '$lib/components/ui/Cover.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte'; import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
@@ -22,8 +23,8 @@
function togglePlay(index: number) { function togglePlay(index: number) {
const track = items[index]; const track = items[index];
if (player.current.id === track.id) player.toggle(); if (player.current.id === queueIdForTrack(track)) player.toggle();
else player.playQueue(items, index); else player.playQueue(toQueueItems(items), index);
} }
</script> </script>
@@ -53,50 +54,66 @@
{#if items.length > 0} {#if items.length > 0}
<div class="mt-8"> <div class="mt-8">
<div <div
class="grid grid-cols-[1fr_120px_160px] items-center gap-6 border-b border-[var(--mf-border)] px-3 pb-3 text-xs font-semibold uppercase tracking-wide text-[var(--mf-text-3)]" class="grid grid-cols-[32px_1fr_100px_100px_64px_36px] items-center gap-8 border-b border-white/10 px-3 pb-2 text-xs uppercase tracking-wide text-neutral-500"
> >
<span class="text-center">#</span>
<span>Título</span> <span>Título</span>
<span class="flex items-center justify-end gap-1.5"><Clock class="h-3.5 w-3.5" /> Duración</span> <span class="text-center">Subida</span>
<span class="flex items-center gap-1.5"><Headphones class="h-3.5 w-3.5" /> Escuchas</span> <span class="text-center">Escuchas</span>
<span class="text-center">Duración</span>
<span></span>
</div> </div>
<div class="mt-2 flex flex-col"> <div class="mt-1 flex flex-col">
{#each items as track, i (track.id)} {#each items as track, i (track.id)}
<div <div
class="group grid animate-enter grid-cols-[1fr_120px_160px] items-center gap-6 rounded-[10px] px-3 py-4 transition duration-100 hover:bg-[var(--mf-surface-hover)]" class="group grid grid-cols-[32px_1fr_100px_100px_64px_36px] items-center gap-8 rounded-[10px] px-3 py-[9px] transition hover:bg-neutral-900/60"
style="animation-delay:{i * 35}ms"
> >
<button <button
type="button" type="button"
onclick={() => togglePlay(i)} onclick={() => togglePlay(i)}
class="flex min-w-0 items-center gap-3.5 text-left active:scale-[0.99]" aria-label={player.current.id === track.id && player.playing ? 'Pausar' : 'Reproducir'}
class="relative grid h-8 w-8 place-items-center overflow-hidden rounded text-sm text-neutral-500"
>
{#if player.current.id === track.id}
<NowPlaying paused={!player.playing} />
{:else}
<span class="group-hover:hidden">{i + 1}</span>
<Play class="hidden h-3.5 w-3.5 text-white group-hover:block" fill="currentColor" />
{/if}
</button>
<button
type="button"
onclick={() => togglePlay(i)}
class="flex min-w-0 items-center gap-3.5 text-left"
> >
<Cover <Cover
trackId={track.id} trackId={track.id}
hue={hueFor(track.id)} hue={hueFor(track.id)}
size="small" size="small"
alt={track.title} alt={track.title}
class="h-[46px] w-[46px] flex-shrink-0 rounded-md" class="h-[42px] w-[42px] flex-shrink-0 rounded-md"
> />
{#if player.current.id === track.id} <div class="min-w-0 truncate text-[14.5px] font-semibold text-neutral-100">{track.title}</div>
<NowPlaying paused={!player.playing} /> </button>
{/if} <div class="truncate text-center text-[12.5px] text-neutral-500">
</Cover>
<div class="min-w-0 flex-1">
<div class="truncate text-[14.5px] font-semibold text-[var(--mf-text)]">
{track.title}
</div>
<div class="truncate text-[12.5px] text-[var(--mf-text-3)]">
{dateFormatter.format(new Date(track.createdAt))} {dateFormatter.format(new Date(track.createdAt))}
</div> </div>
</div> <span class="text-center text-[12.5px] tabular-nums text-neutral-500">
</button> {Number(track.listensCount)}
<span class="text-right text-[12.5px] tabular-nums text-[var(--mf-text-3)]"> </span>
<span class="text-center text-[12.5px] tabular-nums text-neutral-500">
{fmtTime(Number(track.duration))} {fmtTime(Number(track.duration))}
</span> </span>
<div class="flex items-center gap-1.5 text-[12.5px] tabular-nums text-[var(--mf-text-3)]"> <form method="POST" action="?/deleteTrack" use:enhance={() => async ({ update }) => update()}>
<Headphones class="h-3.5 w-3.5" /> <input type="hidden" name="trackId" value={track.id} />
{Number(track.listensCount)} <button
</div> type="submit"
aria-label="Borrar canción"
class="grid h-7 w-7 place-items-center rounded-lg text-neutral-500 opacity-0 transition hover:bg-white/5 hover:text-white group-hover:opacity-100"
>
<X class="h-3.5 w-3.5" />
</button>
</form>
</div> </div>
{/each} {/each}
</div> </div>
@@ -130,14 +130,14 @@
if (e.key === 'Escape') creating = false; if (e.key === 'Escape') creating = false;
}} }}
> >
<div class="animate-pop w-full max-w-2xl rounded-2xl bg-[var(--mf-bg)] p-6 shadow-2xl"> <div class="animate-pop w-full max-w-2xl rounded-lg bg-[var(--mf-bg)] p-6 shadow-2xl">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-2xl font-bold tracking-tight">Nueva lista</h2> <h2 class="text-2xl font-bold tracking-tight">Nueva lista</h2>
<button <button
type="button" type="button"
onclick={() => (creating = false)} onclick={() => (creating = false)}
aria-label="Cerrar" aria-label="Cerrar"
class="grid h-8 w-8 place-items-center rounded-full text-neutral-400 transition hover:bg-white/5 hover:text-white" class="grid h-8 w-8 place-items-center rounded-lg text-neutral-400 transition hover:bg-white/5 hover:text-white"
> >
<X class="h-4 w-4" /> <X class="h-4 w-4" />
</button> </button>
@@ -222,7 +222,7 @@
<button <button
type="button" type="button"
onclick={() => (creating = false)} onclick={() => (creating = false)}
class="rounded-lg border border-white/15 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-white/5" class="rounded-lg border border-white/10 px-5 py-2.5 text-sm text-white/65 transition hover:bg-white/2.5"
> >
Cancelar Cancelar
</button> </button>
@@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => {
params: { path: { playlistId: params.id }, query: { pageNumber: 1, pageSize: 200 } } params: { path: { playlistId: params.id }, query: { pageNumber: 1, pageSize: 200 } }
}), }),
api.GET('/tracks/users/{userId}', { 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 } }
}) })
]); ]);
@@ -160,7 +160,7 @@
<button <button
type="button" type="button"
onclick={reset} onclick={reset}
class="rounded-lg border border-white/20 px-8 py-3.5 text-base font-semibold text-white backdrop-blur-md transition hover:bg-white/5" class="rounded-lg border border-white/10 px-8 py-3.5 text-base text-white/65 transition hover:bg-white/2.5"
> >
Subir otra Subir otra
</button> </button>
@@ -311,7 +311,7 @@
<div class="flex items-center justify-end gap-4"> <div class="flex items-center justify-end gap-4">
<a <a
href="/explore" href="/explore"
class="rounded-lg border border-white/20 px-8 py-3.5 text-base font-semibold text-white backdrop-blur-md transition hover:bg-white/5" class="rounded-lg border border-white/10 px-8 py-3.5 text-base text-white/65 transition hover:bg-white/2.5"
> >
Cancelar Cancelar
</a> </a>