diff --git a/backend/Core/Musify.Application/Tracks/CreateTrack.cs b/backend/Core/Musify.Application/Tracks/CreateTrack.cs index 1b4ff52..4e37204 100644 --- a/backend/Core/Musify.Application/Tracks/CreateTrack.cs +++ b/backend/Core/Musify.Application/Tracks/CreateTrack.cs @@ -18,7 +18,8 @@ namespace Musify.Application.Tracks string Title, Guid PictureIntentId, Guid AudioIntentId, - IReadOnlyCollection Tags) + IReadOnlyCollection Tags, + bool IsExplicit = false) : ICommand>; public class CreateTrackCommandHandler( @@ -83,6 +84,7 @@ namespace Musify.Application.Tracks NormalizedTitle = request.Title.ToUpperInvariant(), OwnerUserId = user.Id, Owner = user, + IsExplicit = request.IsExplicit, Pictures = new TrackPictures { OriginalName = pictureIntent.ObjectName, diff --git a/backend/Core/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs b/backend/Core/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs index 02f87de..2748ded 100644 --- a/backend/Core/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs +++ b/backend/Core/Musify.Application/Tracks/Responses/TrackApplicationResponse.cs @@ -15,7 +15,8 @@ namespace Musify.Application.Tracks.Responses DateTime CreatedAt, DateTime UpdatedAt, [property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId, - IReadOnlyCollection Tags) + IReadOnlyCollection Tags, + bool IsExplicit) { public static TrackApplicationResponse FromEntity(Track track, int listensCount) => new( @@ -28,6 +29,7 @@ namespace Musify.Application.Tracks.Responses track.CreatedAt, track.UpdatedAt, track.OwnerUserId, - track.Tags.Select(tag => tag.Tag).ToList()); + track.Tags.Select(tag => tag.Tag).ToList(), + track.IsExplicit); } } diff --git a/backend/Core/Musify.Domain/Entities/Track.cs b/backend/Core/Musify.Domain/Entities/Track.cs index 439970e..e892b48 100644 --- a/backend/Core/Musify.Domain/Entities/Track.cs +++ b/backend/Core/Musify.Domain/Entities/Track.cs @@ -31,6 +31,9 @@ namespace Musify.Domain.Entities [Required] public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; + [Required] + public bool IsExplicit { get; set; } + [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260923203135_AddTrackIsExplicit.Designer.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260923203135_AddTrackIsExplicit.Designer.cs new file mode 100644 index 0000000..8cea96c --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260923203135_AddTrackIsExplicit.Designer.cs @@ -0,0 +1,1167 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260923203135_AddTrackIsExplicit")] + partial class AddTrackIsExplicit + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Albums"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AlbumId") + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("TrackNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("AlbumId", "TrackId") + .IsUnique(); + + b.ToTable("AlbumHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("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.Mix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Position"); + + b.ToTable("Mixes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MixId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("MixId", "Position"); + + b.ToTable("MixItems"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("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("double precision"); + + b.Property("IsExplicit") + .HasColumnType("boolean"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId", "TrackId") + .IsUnique(); + + b.ToTable("TrackLikes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Tag") + .HasColumnType("genre"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TrackId", "Tag") + .IsUnique(); + + b.ToTable("TrackTags"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.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.UserFollow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FollowedId") + .HasColumnType("bigint"); + + b.Property("FollowerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FollowedId"); + + b.HasIndex("FollowerId", "FollowedId") + .IsUnique(); + + b.ToTable("UserFollows"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("AlbumProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.AlbumPictures", "Pictures", b1 => + { + b1.Property("AlbumId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .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("AlbumId"); + + b1.ToTable("Albums"); + + b1.WithOwner() + .HasForeignKey("AlbumId"); + }); + + b.Navigation("Owner"); + + b.Navigation("Pictures"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Album", "Album") + .WithMany("AlbumTracks") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("AlbumTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("ListeningHistories") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("ListeningHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.HasOne("Musify.Domain.Entities.Mix", "Mix") + .WithMany("Items") + .HasForeignKey("MixId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Mix"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 => + { + b1.Property("PlayListId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("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.TrackLike", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("Tags") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b => + { + b.HasOne("Musify.Domain.Entities.User", "Followed") + .WithMany() + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Follower") + .WithMany() + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Navigation("AlbumTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("AlbumTracks"); + + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("Tags"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260923203135_AddTrackIsExplicit.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260923203135_AddTrackIsExplicit.cs new file mode 100644 index 0000000..3cbf32a --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260923203135_AddTrackIsExplicit.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddTrackIsExplicit : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsExplicit", + table: "Tracks", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsExplicit", + table: "Tracks"); + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 91f3231..7d4af2d 100644 --- a/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -427,6 +427,9 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("DurationSeconds") .HasColumnType("double precision"); + b.Property("IsExplicit") + .HasColumnType("boolean"); + b.Property("LifeCycleStatus") .HasColumnType("integer"); diff --git a/backend/Hosts/Musify.Api/DataTransferObjects/Tracks/CreateTrackRequest.cs b/backend/Hosts/Musify.Api/DataTransferObjects/Tracks/CreateTrackRequest.cs index 6623318..5e95895 100644 --- a/backend/Hosts/Musify.Api/DataTransferObjects/Tracks/CreateTrackRequest.cs +++ b/backend/Hosts/Musify.Api/DataTransferObjects/Tracks/CreateTrackRequest.cs @@ -6,4 +6,5 @@ public record CreateTrackRequest( string Title, Guid PictureIntentId, Guid AudioIntentId, - IReadOnlyCollection Tags); + IReadOnlyCollection Tags, + bool IsExplicit = false); diff --git a/backend/Hosts/Musify.Api/Endpoints/TrackEndpoints.cs b/backend/Hosts/Musify.Api/Endpoints/TrackEndpoints.cs index b285245..e032de9 100644 --- a/backend/Hosts/Musify.Api/Endpoints/TrackEndpoints.cs +++ b/backend/Hosts/Musify.Api/Endpoints/TrackEndpoints.cs @@ -147,7 +147,7 @@ public static class TrackEndpoints CancellationToken cancellationToken) { var result = await mediator.Send( - new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId, request.Tags), + new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId, request.Tags, request.IsExplicit), cancellationToken); return result.ToCreatedResult(track => $"/tracks/{track.Id}"); diff --git a/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs b/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs index a708efa..dae34fd 100644 --- a/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs +++ b/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs @@ -17,6 +17,17 @@ namespace Musify.Application.Tests.Shared Assert.Equal("The Owner", response.Artist); Assert.Equal(owner.Id, response.OwnerUserId); Assert.Equal(3, response.ListensCount); + Assert.False(response.IsExplicit); + } + + [Fact] + public void FromEntity_CopiesExplicitFlag() + { + var track = TestEntities.Track(TestEntities.User(), isExplicit: true); + + var response = TrackApplicationResponse.FromEntity(track, listensCount: 0); + + Assert.True(response.IsExplicit); } } } diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs index 3dce496..e0850b8 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs @@ -56,7 +56,8 @@ namespace Musify.Application.Tests.TestSupport TrackPictures? pictures = null, TrackAudio? audio = null, LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active, - IEnumerable? tags = null) + IEnumerable? tags = null, + bool isExplicit = false) { var track = new Track { @@ -64,6 +65,7 @@ namespace Musify.Application.Tests.TestSupport NormalizedTitle = title.ToUpperInvariant(), DurationSeconds = durationSeconds, LifeCycleStatus = lifeCycleStatus, + IsExplicit = isExplicit, OwnerUserId = owner.Id, Owner = owner, Pictures = pictures ?? ProcessedPictures(), diff --git a/backend/Tests/Musify.Application.Tests/Tracks/CreateTrackCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Tracks/CreateTrackCommandHandlerTests.cs index de7ebbb..6b9a610 100644 --- a/backend/Tests/Musify.Application.Tests/Tracks/CreateTrackCommandHandlerTests.cs +++ b/backend/Tests/Musify.Application.Tests/Tracks/CreateTrackCommandHandlerTests.cs @@ -58,6 +58,26 @@ namespace Musify.Application.Tests.Tracks Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Handle_StoresExplicitFlag(bool isExplicit) + { + var user = TestEntities.User(); + var (picture, audio) = SeedIntents(user.Id); + await SeedAsync(user, picture, audio); + + var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit); + + var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(isExplicit, result.Value.IsExplicit); + var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken); + Assert.NotNull(stored); + Assert.Equal(isExplicit, stored.IsExplicit); + } + [Fact] public async Task Handle_NoTags_ReturnsValidationError() { diff --git a/web-player/src/lib/api/schema.d.ts b/web-player/src/lib/api/schema.d.ts index 0854e6d..4af5481 100644 --- a/web-player/src/lib/api/schema.d.ts +++ b/web-player/src/lib/api/schema.d.ts @@ -1,4 +1,7 @@ - +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ export interface paths { "/albums": { @@ -8,10 +11,10 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated Albums, Optionally Filtered By Title. */ get: operations["GetAlbums"]; put?: never; - + /** Create A New Album. */ post: operations["CreateAlbum"]; delete?: never; options?: never; @@ -26,7 +29,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get An Album By Id. */ get: operations["GetAlbumById"]; put?: never; post?: never; @@ -43,7 +46,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated Albums For A User. */ get: operations["GetAlbumsByUserId"]; put?: never; post?: never; @@ -60,7 +63,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get The Albums The Current User Listened To Most Recently. */ get: operations["GetRecentlyListenedAlbums"]; put?: never; post?: never; @@ -77,7 +80,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get The Tracks Of An Album Ordered By Track Number. */ get: operations["GetAlbumTracks"]; put?: never; post?: never; @@ -94,7 +97,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get An Album Cover Image. */ get: operations["GetAlbumCover"]; put?: never; post?: never; @@ -113,7 +116,7 @@ export interface paths { }; get?: never; put?: never; - + /** Request A Pre-Signed URL To Upload An Album Picture. */ post: operations["RequestAlbumPictureUpload"]; delete?: never; options?: never; @@ -129,10 +132,10 @@ export interface paths { cookie?: never; }; get?: never; - + /** Update An Album. */ put: operations["UpdateAlbum"]; post?: never; - + /** Delete An Album. */ delete: operations["DeleteAlbum"]; options?: never; head?: never; @@ -148,9 +151,9 @@ export interface paths { }; get?: never; put?: never; - + /** Add One Of Your Uploaded Tracks To An Album. */ post: operations["AddTrackToAlbum"]; - + /** Remove A Track From An Album. */ delete: operations["RemoveTrackFromAlbum"]; options?: never; head?: never; @@ -164,7 +167,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get The Public Playback Configuration (Whether Anonymous Listening Is Allowed). Works Anonymously. */ get: operations["GetPlaybackPublicConfig"]; put?: never; post?: never; @@ -181,7 +184,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get The Current User'S Liked Tracks. */ get: operations["GetLikedTracks"]; put?: never; post?: never; @@ -200,7 +203,7 @@ export interface paths { }; get?: never; put?: never; - + /** Like Or Unlike A Track By Id. */ post: operations["ToggleTrackLike"]; delete?: never; options?: never; @@ -215,7 +218,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get The Server-Generated Mixes Of The Current User. */ get: operations["GetMixes"]; put?: never; post?: never; @@ -232,7 +235,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A Mix With All Of Its Songs. */ get: operations["GetMixById"]; put?: never; post?: never; @@ -249,10 +252,10 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated PlayLists. */ get: operations["GetPlayLists"]; put?: never; - + /** Create A New PlayList. */ post: operations["CreatePlayList"]; delete?: never; options?: never; @@ -267,7 +270,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A PlayList By Id. */ get: operations["GetPlayListById"]; put?: never; post?: never; @@ -284,7 +287,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated PlayLists For A User. */ get: operations["GetPlayListsByUserId"]; put?: never; post?: never; @@ -301,10 +304,10 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated Tracks Of A PlayList. */ get: operations["GetPlayListTracks"]; put?: never; - + /** Add One Of Your Tracks To A PlayList. */ post: operations["AddPlayListTrack"]; delete?: never; options?: never; @@ -319,7 +322,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A PlayList Cover Image. */ get: operations["GetPlayListCover"]; put?: never; post?: never; @@ -337,10 +340,10 @@ export interface paths { cookie?: never; }; get?: never; - + /** Update An Existing PlayList. */ put: operations["UpdatePlayList"]; post?: never; - + /** Delete A PlayList. */ delete: operations["DeletePlayList"]; options?: never; head?: never; @@ -356,7 +359,7 @@ export interface paths { }; get?: never; put?: never; - + /** Request A Pre-Signed URL To Upload A PlayList Picture. */ post: operations["RequestPlayListPictureUpload"]; delete?: never; options?: never; @@ -374,7 +377,7 @@ export interface paths { get?: never; put?: never; post?: never; - + /** Remove A Track From A PlayList. */ delete: operations["RemoveTrackFromPlayList"]; options?: never; head?: never; @@ -388,10 +391,10 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated Tracks, Optionally Filtered By Name. */ get: operations["GetTracks"]; put?: never; - + /** Create A New Track. */ post: operations["CreateTrack"]; delete?: never; options?: never; @@ -406,7 +409,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A Track By Id. */ get: operations["GetTrackById"]; put?: never; post?: never; @@ -423,7 +426,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A Track Cover Image. */ get: operations["GetTrackCover"]; put?: never; post?: never; @@ -440,7 +443,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A Streaming Manifest URL And Ticket For A Track. Works Anonymously When The Playback Configuration Allows It. */ get: operations["GetTrackStream"]; put?: never; post?: never; @@ -457,7 +460,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated Tracks For A User. */ get: operations["GetTracksByUserId"]; put?: never; post?: never; @@ -477,7 +480,7 @@ export interface paths { get?: never; put?: never; post?: never; - + /** Delete A Track. */ delete: operations["DeleteTrack"]; options?: never; head?: never; @@ -493,7 +496,7 @@ export interface paths { }; get?: never; put?: never; - + /** Request Pre-Signed URLs To Upload Track Picture And Audio. */ post: operations["RequestTrackUploadUrls"]; delete?: never; options?: never; @@ -508,10 +511,10 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get Paginated Users. */ get: operations["GetUsers"]; put?: never; - + /** Create A New User. */ post: operations["CreateUser"]; delete?: never; options?: never; @@ -526,7 +529,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A User By Id. */ get: operations["GetUserById"]; put?: never; post?: never; @@ -543,7 +546,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A User'S Listening History. */ get: operations["GetListeningHistory"]; put?: never; post?: never; @@ -553,23 +556,6 @@ export interface paths { patch?: never; trace?: never; }; - "/users/{id}/last-listened-track": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - - get: operations["GetLastTrackListenedByUserId"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/users/{id}/listening-stats": { parameters: { query?: never; @@ -577,7 +563,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A User'S Listening Stats For The Current Week And Streak. */ get: operations["GetListeningStats"]; put?: never; post?: never; @@ -587,6 +573,23 @@ export interface paths { patch?: never; trace?: never; }; + "/users/{id}/last-listened-track": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get The Last Track A User Listened To. */ + get: operations["GetLastTrackListenedByUserId"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/users/{id}/profile": { parameters: { query?: never; @@ -594,7 +597,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Get A User'S Public Profile, Including Follower Counts. */ get: operations["GetUserProfile"]; put?: never; post?: never; @@ -611,7 +614,7 @@ export interface paths { path?: never; cookie?: never; }; - + /** Check Whether The Current User Follows Another User. */ get: operations["IsFollowingUser"]; put?: never; post?: never; @@ -630,9 +633,9 @@ export interface paths { }; get?: never; put?: never; - + /** Follow A User. */ post: operations["FollowUser"]; - + /** Unfollow A User. */ delete: operations["UnfollowUser"]; options?: never; head?: never; @@ -644,36 +647,36 @@ export type webhooks = Record; export interface components { schemas: { AddPlayListTrackRequest: { - + /** Format: uuid */ trackId: string; }; AlbumApplicationResponse: { - + /** Format: uuid */ id: string; title: string; description: null | string; - + /** Format: int32 */ releaseYear: null | number | string; ownerUserId: string; - + /** Format: int32 */ trackCount: number | string; smallImageKeyName: null | string; mediumImageKeyName: null | string; largeImageKeyName: null | string; - + /** Format: date-time */ createdAt: string; - + /** Format: date-time */ updatedAt: string; coverTrackIds: string[]; }; AlbumPictureUploadResponse: { - + /** Format: uuid */ intentId: string; bucket: string; key: string; pictureName: string; contentType: string; - + /** Format: int32 */ expiresInSeconds: number | string; uploadUrl: string; }; @@ -682,13 +685,13 @@ export interface components { }; AlbumsSearchResponse: { items: components["schemas"]["AlbumSearchItemResponse"][]; - + /** Format: int32 */ pageNumber: number | string; - + /** Format: int32 */ pageSize: number | string; - + /** Format: int32 */ pageCount: number | string; - + /** Format: int32 */ totalItemCount: number | string; hasPreviousPage: boolean; hasNextPage: boolean; @@ -696,38 +699,41 @@ export interface components { CreateAlbumRequest: { title: string; description: null | string; - + /** Format: int32 */ releaseYear: null | number | string; - + /** Format: uuid */ pictureIntentId: string; }; CreatePlayListRequest: { name: string; description: null | string; - + /** Format: uuid */ pictureIntentId: null | string; visibility?: components["schemas"]["PlaylistVisibility"]; }; CreateTrackRequest: { title: string; - + /** Format: uuid */ pictureIntentId: string; - + /** Format: uuid */ audioIntentId: string; tags: components["schemas"]["Genre"][]; + /** @default false */ + isExplicit: boolean; }; - Genre: "Pop" | "Rock" | "HipHop" | "RnB" | "Jazz" | "Blues" | "Classical" | "Electronic" | "House" | "Techno" | "Trance" | "Dubstep" | "DrumAndBass" | "Metal" | "Punk" | "Reggae" | "Reggaeton" | "Country" | "Folk" | "Indie" | "KPop" | "Latin" | "Soul" | "Funk" | "Ambient" | "Lofi"; CreateUserRequest: { - + /** Format: int64 */ id: number | string; name: string; firstName: null | string; secondName: null | string; }; + /** @enum {unknown} */ + Genre: "Pop" | "Rock" | "HipHop" | "RnB" | "Jazz" | "Blues" | "Classical" | "Electronic" | "House" | "Techno" | "Trance" | "Dubstep" | "DrumAndBass" | "Metal" | "Punk" | "Reggae" | "Reggaeton" | "Country" | "Folk" | "Indie" | "KPop" | "Latin" | "Soul" | "Funk" | "Ambient" | "Lofi"; HttpValidationProblemDetails: { type?: null | string; title?: null | string; - + /** Format: int32 */ status?: null | number | string; detail?: null | string; instance?: null | string; @@ -736,91 +742,91 @@ export interface components { }; }; ListeningStatsResponse: { - + /** Format: int32 */ tracksThisWeek: number | string; - + /** Format: int32 */ secondsThisWeek: number | string; - + /** Format: int32 */ streakDays: number | string; }; MixApplicationResponse: { - + /** Format: uuid */ id: string; title: string; subtitle: null | string; - + /** Format: int32 */ itemCount: number | string; items: components["schemas"]["MixItemApplicationResponse"][]; }; MixItemApplicationResponse: { - + /** Format: uuid */ trackId: string; title: string; artist: null | string; - + /** Format: double */ durationSeconds: number | string; - + /** Format: int32 */ listensCount: number | string; }; PaginatedResponseOfAlbumApplicationResponse: { items: components["schemas"]["AlbumApplicationResponse"][]; - + /** Format: int32 */ pageNumber: number | string; - + /** Format: int32 */ pageSize: number | string; - + /** Format: int32 */ pageCount: number | string; - + /** Format: int32 */ totalItemCount: number | string; hasNextPage: boolean; hasPreviousPage: boolean; }; PaginatedResponseOfPlayListApplicationResponse: { items: components["schemas"]["PlayListApplicationResponse"][]; - + /** Format: int32 */ pageNumber: number | string; - + /** Format: int32 */ pageSize: number | string; - + /** Format: int32 */ pageCount: number | string; - + /** Format: int32 */ totalItemCount: number | string; hasNextPage: boolean; hasPreviousPage: boolean; }; PaginatedResponseOfTrackApplicationResponse: { items: components["schemas"]["TrackApplicationResponse"][]; - + /** Format: int32 */ pageNumber: number | string; - + /** Format: int32 */ pageSize: number | string; - + /** Format: int32 */ pageCount: number | string; - + /** Format: int32 */ totalItemCount: number | string; hasNextPage: boolean; hasPreviousPage: boolean; }; PaginatedResponseOfUserApplicationResponse: { items: components["schemas"]["UserApplicationResponse"][]; - + /** Format: int32 */ pageNumber: number | string; - + /** Format: int32 */ pageSize: number | string; - + /** Format: int32 */ pageCount: number | string; - + /** Format: int32 */ totalItemCount: number | string; hasNextPage: boolean; hasPreviousPage: boolean; }; PlaybackPublicConfigResponse: { allowAnonymousListening: boolean; - + /** Format: int32 */ anonymousFragmentSeconds: number | string; }; PlayListApplicationResponse: { - + /** Format: uuid */ id: string; name: string; description: null | string; @@ -828,37 +834,37 @@ export interface components { mediumImageKeyName: null | string; largeImageKeyName: null | string; visibility: components["schemas"]["PlaylistVisibility"]; - + /** Format: date-time */ createdAt: string; - + /** Format: date-time */ updatedAt: string; coverTrackIds: string[]; }; PlayListPictureUploadResponse: { - + /** Format: uuid */ intentId: string; bucket: string; key: string; pictureName: string; contentType: string; - + /** Format: int32 */ expiresInSeconds: number | string; uploadUrl: string; }; - + /** @enum {unknown} */ PlaylistVisibility: "Private" | "Public"; - + /** @enum {unknown} */ ProcessingStatus: "Pending" | "Processing" | "Completed" | "Failed"; RequestAlbumPictureUploadRequest: { fileType: string; contentType: string; - + /** Format: int64 */ expectedSizeBytes?: null | number | string; }; RequestPlayListPictureUploadRequest: { fileType: string; contentType: string; - + /** Format: int64 */ expectedSizeBytes?: null | number | string; }; RequestTrackUploadUrlsRequest: { @@ -866,43 +872,45 @@ export interface components { pictureContentType: string; audioFileType: string; audioContentType: string; - + /** Format: int64 */ expectedPictureSizeBytes?: null | number | string; - + /** Format: int64 */ expectedAudioSizeBytes?: null | number | string; }; ToggleLikeRequest: { - + /** Format: uuid */ trackId: string; }; TrackApplicationResponse: { - + /** Format: uuid */ id: string; title: string; artist: null | string; audioStatus: components["schemas"]["ProcessingStatus"]; - + /** Format: double */ duration: number | string; - + /** Format: int32 */ listensCount: number | string; - + /** Format: date-time */ createdAt: string; - + /** Format: date-time */ updatedAt: string; ownerUserId: string; + tags: components["schemas"]["Genre"][]; + isExplicit: boolean; }; TrackSearchItemResponse: { track: components["schemas"]["TrackApplicationResponse"]; }; TracksSearchResponse: { items: components["schemas"]["TrackSearchItemResponse"][]; - + /** Format: int32 */ pageNumber: number | string; - + /** Format: int32 */ pageSize: number | string; - + /** Format: int32 */ pageCount: number | string; - + /** Format: int32 */ totalItemCount: number | string; hasPreviousPage: boolean; hasNextPage: boolean; @@ -910,13 +918,13 @@ export interface components { TrackStreamResponse: { manifestUrl: string; ticket: string; - + /** Format: int32 */ expiresInSeconds: number | string; }; TrackUploadUrlsResponse: { - + /** Format: uuid */ pictureIntentId: string; - + /** Format: uuid */ audioIntentId: string; bucket: string; pictureKey: string; @@ -927,21 +935,21 @@ export interface components { audioName: string; audioContentType: string; audioUploadUrl: string; - + /** Format: int32 */ expiresInSeconds: number | string; }; UpdateAlbumRequest: { newTitle: string; newDescription: null | string; - + /** Format: int32 */ newReleaseYear: null | number | string; - + /** Format: uuid */ newPictureIntentId: null | string; }; UpdatePlayListRequest: { newName: null | string; newDescription: null | string; - + /** Format: uuid */ newPictureIntentId: null | string; newVisibility?: null | components["schemas"]["PlaylistVisibility"]; }; @@ -951,9 +959,9 @@ export interface components { firstName: null | string; secondName: null | string; profilePictureUrl: null | string; - + /** Format: date-time */ createdAt: string; - + /** Format: date-time */ updatedAt: string; }; UserProfileResponse: { @@ -962,11 +970,11 @@ export interface components { firstName: null | string; secondName: null | string; profilePictureUrl: null | string; - + /** Format: date-time */ createdAt: string; - + /** Format: int32 */ followersCount: number | string; - + /** Format: int32 */ followingCount: number | string; }; }; @@ -991,7 +999,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1015,7 +1023,7 @@ export interface operations { }; }; responses: { - + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -1024,7 +1032,7 @@ export interface operations { "application/json": components["schemas"]["AlbumApplicationResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -1033,14 +1041,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1060,7 +1068,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1069,7 +1077,7 @@ export interface operations { "application/json": components["schemas"]["AlbumApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1092,7 +1100,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1101,7 +1109,7 @@ export interface operations { "application/json": components["schemas"]["PaginatedResponseOfAlbumApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1121,7 +1129,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1130,7 +1138,7 @@ export interface operations { "application/json": components["schemas"]["AlbumApplicationResponse"][]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; @@ -1153,7 +1161,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1162,7 +1170,7 @@ export interface operations { "application/json": components["schemas"]["PaginatedResponseOfTrackApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1184,14 +1192,14 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1213,7 +1221,7 @@ export interface operations { }; }; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1222,7 +1230,7 @@ export interface operations { "application/json": components["schemas"]["AlbumPictureUploadResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -1231,14 +1239,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1262,7 +1270,7 @@ export interface operations { }; }; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1271,7 +1279,7 @@ export interface operations { "application/json": components["schemas"]["AlbumApplicationResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -1280,14 +1288,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1307,21 +1315,21 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1342,28 +1350,28 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Conflict */ 409: { headers: { [name: string]: unknown; @@ -1384,21 +1392,21 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1416,7 +1424,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1439,7 +1447,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1448,7 +1456,7 @@ export interface operations { "application/json": components["schemas"]["PaginatedResponseOfTrackApplicationResponse"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; @@ -1470,7 +1478,7 @@ export interface operations { }; }; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1479,14 +1487,14 @@ export interface operations { "application/json": boolean; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1504,7 +1512,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1513,7 +1521,7 @@ export interface operations { "application/json": components["schemas"]["MixApplicationResponse"][]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; @@ -1533,7 +1541,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1542,14 +1550,14 @@ export interface operations { "application/json": components["schemas"]["MixApplicationResponse"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1570,7 +1578,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1594,7 +1602,7 @@ export interface operations { }; }; responses: { - + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -1603,7 +1611,7 @@ export interface operations { "application/json": components["schemas"]["PlayListApplicationResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -1612,14 +1620,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1639,7 +1647,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1648,7 +1656,7 @@ export interface operations { "application/json": components["schemas"]["PlayListApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1673,7 +1681,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1698,7 +1706,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1707,7 +1715,7 @@ export interface operations { "application/json": components["schemas"]["PaginatedResponseOfTrackApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1731,7 +1739,7 @@ export interface operations { }; }; responses: { - + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -1740,21 +1748,21 @@ export interface operations { "application/json": components["schemas"]["TrackApplicationResponse"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Conflict */ 409: { headers: { [name: string]: unknown; @@ -1776,14 +1784,14 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1807,7 +1815,7 @@ export interface operations { }; }; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1816,7 +1824,7 @@ export interface operations { "application/json": components["schemas"]["PlayListApplicationResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -1825,14 +1833,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1852,21 +1860,21 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1888,7 +1896,7 @@ export interface operations { }; }; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1897,7 +1905,7 @@ export interface operations { "application/json": components["schemas"]["PlayListPictureUploadResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -1906,14 +1914,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1934,21 +1942,21 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -1970,7 +1978,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -1994,7 +2002,7 @@ export interface operations { }; }; responses: { - + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -2003,7 +2011,7 @@ export interface operations { "application/json": components["schemas"]["TrackApplicationResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -2012,21 +2020,21 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Conflict */ 409: { headers: { [name: string]: unknown; @@ -2046,7 +2054,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2055,7 +2063,7 @@ export interface operations { "application/json": components["schemas"]["TrackApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2077,14 +2085,14 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2104,7 +2112,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2113,14 +2121,14 @@ export interface operations { "application/json": components["schemas"]["TrackStreamResponse"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2144,7 +2152,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2166,28 +2174,28 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Conflict */ 409: { headers: { [name: string]: unknown; @@ -2209,7 +2217,7 @@ export interface operations { }; }; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2218,7 +2226,7 @@ export interface operations { "application/json": components["schemas"]["TrackUploadUrlsResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -2227,21 +2235,21 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Conflict */ 409: { headers: { [name: string]: unknown; @@ -2263,7 +2271,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2287,7 +2295,7 @@ export interface operations { }; }; responses: { - + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -2296,7 +2304,7 @@ export interface operations { "application/json": components["schemas"]["UserApplicationResponse"]; }; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -2305,7 +2313,7 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Conflict */ 409: { headers: { [name: string]: unknown; @@ -2325,7 +2333,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2334,7 +2342,7 @@ export interface operations { "application/json": components["schemas"]["UserApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2354,7 +2362,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2363,7 +2371,7 @@ export interface operations { "application/json": components["schemas"]["TrackApplicationResponse"][]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2383,7 +2391,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2405,7 +2413,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2414,7 +2422,7 @@ export interface operations { "application/json": components["schemas"]["TrackApplicationResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2434,7 +2442,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2443,7 +2451,7 @@ export interface operations { "application/json": components["schemas"]["UserProfileResponse"]; }; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2463,7 +2471,7 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description OK */ 200: { headers: { [name: string]: unknown; @@ -2472,7 +2480,7 @@ export interface operations { "application/json": boolean; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; @@ -2492,14 +2500,14 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Bad Request */ 400: { headers: { [name: string]: unknown; @@ -2508,14 +2516,14 @@ export interface operations { "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; }; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Not Found */ 404: { headers: { [name: string]: unknown; @@ -2535,14 +2543,14 @@ export interface operations { }; requestBody?: never; responses: { - + /** @description No Content */ 204: { headers: { [name: string]: unknown; }; content?: never; }; - + /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; diff --git a/web-player/src/lib/components/player/Queue.svelte b/web-player/src/lib/components/player/Queue.svelte index ea997d5..2265456 100644 --- a/web-player/src/lib/components/player/Queue.svelte +++ b/web-player/src/lib/components/player/Queue.svelte @@ -61,6 +61,7 @@ title={player.current.title} artist={player.current.artist || '—'} ownerUserId={player.current.ownerUserId} + explicit={player.current.explicit} active /> @@ -123,6 +124,7 @@ title={track.title} subtitle={track.artist || '—'} subtitleHref={track.ownerUserId} + explicit={track.explicit} trackId={track.id} class="p-2" > diff --git a/web-player/src/lib/components/ui/media/ExplicitBadge.svelte b/web-player/src/lib/components/ui/media/ExplicitBadge.svelte index 99e7728..9522fb8 100644 --- a/web-player/src/lib/components/ui/media/ExplicitBadge.svelte +++ b/web-player/src/lib/components/ui/media/ExplicitBadge.svelte @@ -7,7 +7,7 @@ E diff --git a/web-player/src/routes/(app)/upload/+page.server.ts b/web-player/src/routes/(app)/upload/+page.server.ts index 53a7bdd..1809b7f 100644 --- a/web-player/src/routes/(app)/upload/+page.server.ts +++ b/web-player/src/routes/(app)/upload/+page.server.ts @@ -25,6 +25,7 @@ export const actions: Actions = { const audio = form.get('audio'); const cover = form.get('cover'); const rawTags = form.getAll('tags').map(String); + const isExplicit = form.get('isExplicit') === 'on'; if (title === '' || title.length > MAX_TITLE) { return fail(400, { message: 'El título es obligatorio (máx. 100 caracteres).' }); @@ -90,7 +91,8 @@ export const actions: Actions = { title, pictureIntentId: urls.pictureIntentId, audioIntentId: urls.audioIntentId, - tags + tags, + isExplicit } }); diff --git a/web-player/src/routes/(app)/upload/+page.svelte b/web-player/src/routes/(app)/upload/+page.svelte index 625e593..ba9b3da 100644 --- a/web-player/src/routes/(app)/upload/+page.svelte +++ b/web-player/src/routes/(app)/upload/+page.svelte @@ -13,6 +13,7 @@ import Input from '$lib/components/ui/primitives/Input.svelte'; import Checkbox from '$lib/components/ui/primitives/Checkbox.svelte'; import Button from '$lib/components/ui/primitives/Button.svelte'; + import ExplicitBadge from '$lib/components/ui/media/ExplicitBadge.svelte'; import Chip from '$lib/components/ui/primitives/Chip.svelte'; import { TRACK_GENRES, areCompatible, type TrackGenre } from '$lib/data/trackGenres'; @@ -29,6 +30,7 @@ let dragging = $state(false); let status = $state('idle'); let acceptedTerms = $state(false); + let isExplicit = $state(false); let errorMsg = $state(''); let errorDetail = $state(''); let tags = $state([]); @@ -101,6 +103,7 @@ coverResetToken += 1; status = 'idle'; acceptedTerms = false; + isExplicit = false; errorMsg = ''; errorDetail = ''; tags = []; @@ -277,6 +280,13 @@ {tags.length} elegidos {/snippet} + + + + Contenido explícito + + + diff --git a/web-player/src/routes/+page.svelte b/web-player/src/routes/+page.svelte index 8075739..906c6e6 100644 --- a/web-player/src/routes/+page.svelte +++ b/web-player/src/routes/+page.svelte @@ -195,6 +195,7 @@ title={item.title} subtitle={item.artist} subtitleHref={item.ownerUserId} + explicit={item.explicit} trackId={item.id} class="animate-enter" style="--i:{i}" @@ -252,6 +253,7 @@ title={track.title} subtitle={track.artist} subtitleHref={track.ownerUserId} + explicit={track.isExplicit} > {#snippet leading()}