feat(backend): add Artist entity and track ownership

Track.Artist was a plain string snapshot with no identity, and
UserHasTrack doubled as both "in my library" and "I own this",
conflating collection membership with administrative ownership.

Introduce Artist/TrackArtist to give artist credits their own
identity (needed so YouTube tracks with multiple credited artists,
and future renames, can be modeled), and Track.OwnerUserId to
represent ownership explicitly (local tracks only, enforced by a
check constraint). CreateTrack now provisions the uploader's Artist
row and links it via TrackArtist; DeleteTrack authorizes against
OwnerUserId instead of any UserHasTrack row.
This commit is contained in:
2026-07-17 17:55:32 +02:00
parent fa45843079
commit cdf89dc632
10 changed files with 1160 additions and 7 deletions
+37
View File
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Musify.Domain.Abstractions;
namespace Musify.Domain.Entities
{
#pragma warning disable CS8618
public class Artist : IAuditable
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(200)]
public required string Name { get; set; }
[Required]
[MaxLength(200)]
public required string NormalizedName { get; set; }
public long? UserId { get; set; }
[MaxLength(64)]
public string? ExternalId { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(UserId)) ]
public User? User { get; set; }
public ICollection<TrackArtist> TrackArtists { get; set; } = new List<TrackArtist>();
}
}
+7
View File
@@ -26,6 +26,8 @@ namespace Musify.Domain.Entities
[MaxLength(200)]
public string? Artist { get; set; }
public long? OwnerUserId { get; set; }
[Required]
public TrackSource Source { get; set; } = TrackSource.Local;
@@ -73,12 +75,17 @@ namespace Musify.Domain.Entities
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(OwnerUserId))]
public User? Owner { get; set; }
public ICollection<UserHasTrack> UserTracks { get; set; } = new List<UserHasTrack>();
public ICollection<PlayListHasTrack> PlayListTracks { get; set; } = new List<PlayListHasTrack>();
public ICollection<ListeningHistory> ListeningHistories { get; set; } = new List<ListeningHistory>();
public ICollection<TrackArtist> TrackArtists { get; set; } = new List<TrackArtist>();
[NotMapped]
[MemberNotNullWhen(true, nameof(SmallPictureName)) ]
[MemberNotNullWhen(true, nameof(MediumPictureName)) ]
@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities
{
#pragma warning disable CS8618
[Table("TrackArtist")]
public class TrackArtist
{
[Required]
public required Guid TrackId { get; set; }
[Required]
public required Guid ArtistId { get; set; }
[Required]
public int Position { get; set; } = 0;
[ForeignKey(nameof(TrackId)) ]
public Track Track { get; set; }
[ForeignKey(nameof(ArtistId)) ]
public Artist Artist { get; set; }
}
}