feat(release): add plain-text Description field plumbed CMS->DTO->release (11.G)
New nullable Description column (max 4000) on ReleaseEntity, rides the Genre write channel through upload + edit; multiline CMS input. Migration authored, not applied.
This commit is contained in:
@@ -189,6 +189,7 @@ public class TrackController : ControllerBase
|
||||
[FromForm] string? artist,
|
||||
[FromForm] string? album,
|
||||
[FromForm] string? genre,
|
||||
[FromForm] string? description,
|
||||
[FromForm] string? releaseDate,
|
||||
[FromForm] string? originalFileName,
|
||||
[FromForm] long createdByUserId,
|
||||
@@ -283,6 +284,7 @@ public class TrackController : ControllerBase
|
||||
artist,
|
||||
string.IsNullOrWhiteSpace(album) ? null : album,
|
||||
string.IsNullOrWhiteSpace(genre) ? null : genre,
|
||||
string.IsNullOrWhiteSpace(description) ? null : description,
|
||||
parsedReleaseDate,
|
||||
createdByUserId,
|
||||
string.IsNullOrWhiteSpace(originalFileName) ? null : originalFileName,
|
||||
@@ -412,6 +414,7 @@ public class TrackController : ControllerBase
|
||||
release.Artist = request.Artist;
|
||||
release.Title = request.Album ?? string.Empty;
|
||||
release.Genre = request.Genre;
|
||||
release.Description = request.Description;
|
||||
release.ReleaseDate = request.ReleaseDate;
|
||||
|
||||
// ImagePath is tri-state: null = no change, "" = clear, value = set.
|
||||
|
||||
@@ -16,6 +16,7 @@ public record UpdateTrackMetadataRequest(
|
||||
string Artist,
|
||||
string? Album,
|
||||
string? Genre,
|
||||
string? Description,
|
||||
DateOnly? ReleaseDate,
|
||||
string? ImagePath = null,
|
||||
ReleaseType? ReleaseType = null,
|
||||
|
||||
@@ -57,6 +57,7 @@ public class UnifiedTrackService
|
||||
string artist,
|
||||
string? album,
|
||||
string? genre,
|
||||
string? description,
|
||||
DateOnly? releaseDate,
|
||||
long createdByUserId,
|
||||
string? originalFileName,
|
||||
@@ -106,8 +107,8 @@ public class UnifiedTrackService
|
||||
|
||||
// Resolve the release FK before persisting the track. An upload with an album lands on the
|
||||
// shared release (created on first sighting); an upload without one stays a loose track with
|
||||
// a null ReleaseId. Release-cardinal metadata (artist/genre/releaseDate/type/uploader) rides
|
||||
// on the release, not the track.
|
||||
// a null ReleaseId. Release-cardinal metadata (artist/genre/description/releaseDate/type/uploader)
|
||||
// rides on the release, not the track.
|
||||
long? releaseId = null;
|
||||
if (!string.IsNullOrWhiteSpace(album))
|
||||
{
|
||||
@@ -116,6 +117,7 @@ public class UnifiedTrackService
|
||||
Title = album,
|
||||
Artist = artist,
|
||||
Genre = genre,
|
||||
Description = description,
|
||||
ReleaseDate = releaseDate,
|
||||
ReleaseType = releaseType,
|
||||
Medium = medium,
|
||||
|
||||
@@ -35,6 +35,11 @@ public class ReleaseConfiguration : BaseEntityConfiguration<ReleaseEntity>
|
||||
.HasMaxLength(100)
|
||||
.HasColumnName("genre");
|
||||
|
||||
// Plain-text prose blurb. Generous ceiling for a paragraph; nullable (no data migration).
|
||||
builder.Property(e => e.Description)
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnName("description");
|
||||
|
||||
builder.Property(e => e.ReleaseDate)
|
||||
.HasColumnName("release_date");
|
||||
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DeepDrftData.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DeepDrftData.Migrations
|
||||
{
|
||||
[DbContext(typeof(DeepDrftContext))]
|
||||
[Migration("20260616035252_AddReleaseDescription")]
|
||||
partial class AddReleaseDescription
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.MixMetadata", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<long>("ReleaseId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("release_id");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("WaveformEntryKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("waveform_entry_key");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsDeleted")
|
||||
.HasDatabaseName("IX_mix_metadata_is_deleted");
|
||||
|
||||
b.HasIndex("ReleaseId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_mix_metadata_release_id");
|
||||
|
||||
b.ToTable("mix_metadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.ReleaseEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Artist")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("artist");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long?>("CreatedByUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("created_by_user_id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Genre")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("genre");
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("image_path");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<string>("Medium")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Cut")
|
||||
.HasColumnName("medium");
|
||||
|
||||
b.Property<DateOnly?>("ReleaseDate")
|
||||
.HasColumnType("date")
|
||||
.HasColumnName("release_date");
|
||||
|
||||
b.Property<string>("ReleaseType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Single")
|
||||
.HasColumnName("release_type");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsDeleted")
|
||||
.HasDatabaseName("IX_release_is_deleted");
|
||||
|
||||
b.HasIndex("Title", "Artist")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_release_title_artist")
|
||||
.HasFilter("\"is_deleted\" = false");
|
||||
|
||||
b.ToTable("release", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.SessionMetadata", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("HeroImageEntryKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("hero_image_entry_key");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<long>("ReleaseId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("release_id");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsDeleted")
|
||||
.HasDatabaseName("IX_session_metadata_is_deleted");
|
||||
|
||||
b.HasIndex("ReleaseId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_session_metadata_release_id");
|
||||
|
||||
b.ToTable("session_metadata", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.TrackEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("EntryKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("entry_key");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<string>("OriginalFileName")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("original_file_name");
|
||||
|
||||
b.Property<long?>("ReleaseId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("release_id");
|
||||
|
||||
b.Property<string>("TrackName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("track_name");
|
||||
|
||||
b.Property<int>("TrackNumber")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("track_number");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsDeleted")
|
||||
.HasDatabaseName("IX_track_is_deleted");
|
||||
|
||||
b.HasIndex("ReleaseId");
|
||||
|
||||
b.ToTable("track", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.MixMetadata", b =>
|
||||
{
|
||||
b.HasOne("DeepDrftModels.Entities.ReleaseEntity", "Release")
|
||||
.WithOne("MixMetadata")
|
||||
.HasForeignKey("DeepDrftModels.Entities.MixMetadata", "ReleaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Release");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.SessionMetadata", b =>
|
||||
{
|
||||
b.HasOne("DeepDrftModels.Entities.ReleaseEntity", "Release")
|
||||
.WithOne("SessionMetadata")
|
||||
.HasForeignKey("DeepDrftModels.Entities.SessionMetadata", "ReleaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Release");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.TrackEntity", b =>
|
||||
{
|
||||
b.HasOne("DeepDrftModels.Entities.ReleaseEntity", "Release")
|
||||
.WithMany("Tracks")
|
||||
.HasForeignKey("ReleaseId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Release");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DeepDrftModels.Entities.ReleaseEntity", b =>
|
||||
{
|
||||
b.Navigation("MixMetadata");
|
||||
|
||||
b.Navigation("SessionMetadata");
|
||||
|
||||
b.Navigation("Tracks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DeepDrftData.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddReleaseDescription : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "description",
|
||||
table: "release",
|
||||
type: "character varying(4000)",
|
||||
maxLength: 4000,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "description",
|
||||
table: "release");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,11 @@ namespace DeepDrftData.Migrations
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("created_by_user_id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Genre")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
|
||||
@@ -24,6 +24,7 @@ public class TrackConverter : IEntityToModelConverter<TrackEntity, TrackDto>
|
||||
Title = entity.Title,
|
||||
Artist = entity.Artist,
|
||||
Genre = entity.Genre,
|
||||
Description = entity.Description,
|
||||
ReleaseDate = entity.ReleaseDate,
|
||||
ImagePath = entity.ImagePath,
|
||||
Medium = entity.Medium,
|
||||
@@ -55,6 +56,7 @@ public class TrackConverter : IEntityToModelConverter<TrackEntity, TrackDto>
|
||||
Title = dto.Title,
|
||||
Artist = dto.Artist,
|
||||
Genre = dto.Genre,
|
||||
Description = dto.Description,
|
||||
ReleaseDate = dto.ReleaseDate,
|
||||
ImagePath = dto.ImagePath,
|
||||
Medium = dto.Medium,
|
||||
|
||||
@@ -281,6 +281,7 @@ public class TrackManager
|
||||
releaseEntity.Title = release.Title;
|
||||
releaseEntity.Artist = release.Artist;
|
||||
releaseEntity.Genre = release.Genre;
|
||||
releaseEntity.Description = release.Description;
|
||||
releaseEntity.ReleaseDate = release.ReleaseDate;
|
||||
releaseEntity.ImagePath = release.ImagePath;
|
||||
releaseEntity.Medium = release.Medium;
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
T="string" Label="Release Date (YYYY-MM-DD)" Placeholder="2024-01-15"
|
||||
Variant="Variant.Outlined" Disabled="Disabled" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudTextField Value="Description" ValueChanged="@((string v) => DescriptionChanged.InvokeAsync(v))"
|
||||
T="string" Label="Description" Lines="4" MaxLength="4000"
|
||||
Variant="Variant.Outlined" Disabled="Disabled" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudField Label="Cover Art" Variant="Variant.Outlined" InnerPadding="false">
|
||||
<MudStack Spacing="3">
|
||||
@@ -79,6 +84,8 @@
|
||||
[Parameter] public EventCallback<string> ArtistChanged { get; set; }
|
||||
[Parameter] public string Genre { get; set; } = string.Empty;
|
||||
[Parameter] public EventCallback<string> GenreChanged { get; set; }
|
||||
[Parameter] public string Description { get; set; } = string.Empty;
|
||||
[Parameter] public EventCallback<string> DescriptionChanged { get; set; }
|
||||
[Parameter] public string ReleaseDate { get; set; } = string.Empty;
|
||||
[Parameter] public EventCallback<string> ReleaseDateChanged { get; set; }
|
||||
[Parameter] public ReleaseType ReleaseType { get; set; } = ReleaseType.Single;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<AlbumHeaderFields @bind-AlbumName="_albumName"
|
||||
@bind-Artist="_artist"
|
||||
@bind-Genre="_genre"
|
||||
@bind-Description="_description"
|
||||
@bind-ReleaseDate="_releaseDate"
|
||||
@bind-ReleaseType="_releaseType"
|
||||
Medium="_medium"
|
||||
@@ -137,6 +138,7 @@
|
||||
private string _albumName = string.Empty;
|
||||
private string _artist = string.Empty;
|
||||
private string _genre = string.Empty;
|
||||
private string _description = string.Empty;
|
||||
private string _releaseDate = string.Empty;
|
||||
private ReleaseType _releaseType = ReleaseType.Single;
|
||||
private ReleaseMedium _medium = ReleaseMedium.Cut;
|
||||
@@ -211,6 +213,7 @@
|
||||
_albumName = albumName;
|
||||
_artist = release?.Artist ?? string.Empty;
|
||||
_genre = release?.Genre ?? string.Empty;
|
||||
_description = release?.Description ?? string.Empty;
|
||||
_releaseDate = release?.ReleaseDate?.ToString("yyyy-MM-dd") ?? string.Empty;
|
||||
_releaseType = release?.ReleaseType ?? ReleaseType.Single;
|
||||
_medium = release?.Medium ?? ReleaseMedium.Cut;
|
||||
@@ -382,6 +385,7 @@
|
||||
: DateOnly.ParseExact(_releaseDate, "yyyy-MM-dd");
|
||||
var album = string.IsNullOrWhiteSpace(_albumName) ? null : _albumName;
|
||||
var genre = string.IsNullOrWhiteSpace(_genre) ? null : _genre;
|
||||
var description = string.IsNullOrWhiteSpace(_description) ? null : _description;
|
||||
|
||||
// For single-track media (Session/Mix) the track name is derived from the Release Name —
|
||||
// no separate Track Name editor is shown. Sync here so changes to the Release Name always
|
||||
@@ -446,6 +450,7 @@
|
||||
_artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
releaseDate,
|
||||
imagePathForUpdate,
|
||||
_releaseType,
|
||||
@@ -480,6 +485,7 @@
|
||||
_artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate,
|
||||
row.WavFile.Name,
|
||||
createdByUserId,
|
||||
@@ -508,6 +514,7 @@
|
||||
_artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
releaseDate,
|
||||
linkPath,
|
||||
_releaseType,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<AlbumHeaderFields @bind-AlbumName="_albumName"
|
||||
@bind-Artist="_artist"
|
||||
@bind-Genre="_genre"
|
||||
@bind-Description="_description"
|
||||
@bind-ReleaseDate="_releaseDate"
|
||||
@bind-ReleaseType="_releaseType"
|
||||
Medium="_medium"
|
||||
@@ -126,6 +127,7 @@
|
||||
private string _albumName = string.Empty;
|
||||
private string _artist = string.Empty;
|
||||
private string _genre = string.Empty;
|
||||
private string _description = string.Empty;
|
||||
private string _releaseDate = string.Empty;
|
||||
private ReleaseType _releaseType = ReleaseType.Single;
|
||||
private ReleaseMedium _medium = ReleaseMedium.Cut;
|
||||
@@ -342,6 +344,7 @@
|
||||
_artist,
|
||||
string.IsNullOrWhiteSpace(_albumName) ? null : _albumName,
|
||||
string.IsNullOrWhiteSpace(_genre) ? null : _genre,
|
||||
string.IsNullOrWhiteSpace(_description) ? null : _description,
|
||||
string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate,
|
||||
row.WavFile.Name,
|
||||
createdByUserId,
|
||||
@@ -369,6 +372,7 @@
|
||||
_artist,
|
||||
string.IsNullOrWhiteSpace(_albumName) ? null : _albumName,
|
||||
string.IsNullOrWhiteSpace(_genre) ? null : _genre,
|
||||
string.IsNullOrWhiteSpace(_description) ? null : _description,
|
||||
string.IsNullOrWhiteSpace(_releaseDate) ? null : (DateOnly?)DateOnly.ParseExact(_releaseDate, "yyyy-MM-dd"),
|
||||
imgPath,
|
||||
_releaseType,
|
||||
|
||||
@@ -39,6 +39,7 @@ public class CmsTrackService : ICmsTrackService
|
||||
string artist,
|
||||
string? album,
|
||||
string? genre,
|
||||
string? description,
|
||||
string? releaseDate,
|
||||
string? originalFileName,
|
||||
long createdByUserId,
|
||||
@@ -58,6 +59,7 @@ public class CmsTrackService : ICmsTrackService
|
||||
multipart.Add(new StringContent(artist), "artist");
|
||||
if (!string.IsNullOrWhiteSpace(album)) multipart.Add(new StringContent(album), "album");
|
||||
if (!string.IsNullOrWhiteSpace(genre)) multipart.Add(new StringContent(genre), "genre");
|
||||
if (!string.IsNullOrWhiteSpace(description)) multipart.Add(new StringContent(description), "description");
|
||||
if (!string.IsNullOrWhiteSpace(releaseDate)) multipart.Add(new StringContent(releaseDate), "releaseDate");
|
||||
// Explicit field — decouples the admin-visible display name from the WAV part's content-disposition filename.
|
||||
if (!string.IsNullOrWhiteSpace(originalFileName)) multipart.Add(new StringContent(originalFileName), "originalFileName");
|
||||
@@ -371,7 +373,7 @@ public class CmsTrackService : ICmsTrackService
|
||||
|
||||
public async Task<Result> UpdateAsync(
|
||||
long id, string trackName, string artist,
|
||||
string? album, string? genre, DateOnly? releaseDate,
|
||||
string? album, string? genre, string? description, DateOnly? releaseDate,
|
||||
string? imagePath = null,
|
||||
ReleaseType? releaseType = null,
|
||||
ReleaseMedium? medium = null,
|
||||
@@ -385,6 +387,7 @@ public class CmsTrackService : ICmsTrackService
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
releaseDate,
|
||||
imagePath,
|
||||
releaseType = releaseType.HasValue ? (int?)releaseType.Value : null,
|
||||
|
||||
@@ -30,6 +30,7 @@ public interface ICmsTrackService
|
||||
string artist,
|
||||
string? album,
|
||||
string? genre,
|
||||
string? description,
|
||||
string? releaseDate,
|
||||
string? originalFileName,
|
||||
long createdByUserId,
|
||||
@@ -85,7 +86,7 @@ public interface ICmsTrackService
|
||||
/// </summary>
|
||||
Task<Result> UpdateAsync(
|
||||
long id, string trackName, string artist,
|
||||
string? album, string? genre, DateOnly? releaseDate,
|
||||
string? album, string? genre, string? description, DateOnly? releaseDate,
|
||||
string? imagePath = null,
|
||||
ReleaseType? releaseType = null,
|
||||
ReleaseMedium? medium = null,
|
||||
|
||||
@@ -12,6 +12,7 @@ public class ReleaseDto : BaseModel
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Artist { get; set; } = string.Empty;
|
||||
public string? Genre { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DateOnly? ReleaseDate { get; set; }
|
||||
public string? ImagePath { get; set; }
|
||||
public ReleaseMedium Medium { get; set; } = ReleaseMedium.Cut;
|
||||
|
||||
@@ -15,6 +15,10 @@ public class ReleaseEntity : BaseEntity, IEntity
|
||||
public required string Title { get; set; }
|
||||
public required string Artist { get; set; }
|
||||
public string? Genre { get; set; }
|
||||
// Free-text prose blurb describing the release. Uniform across media (Cut/Session/Mix), so it
|
||||
// lives on the base table alongside Genre rather than in a per-medium satellite. Plain text,
|
||||
// max 4000 (configured in ReleaseConfiguration); nullable so existing rows migrate as NULL.
|
||||
public string? Description { get; set; }
|
||||
public DateOnly? ReleaseDate { get; set; }
|
||||
public string? ImagePath { get; set; }
|
||||
public ReleaseType ReleaseType { get; set; } = ReleaseType.Single;
|
||||
|
||||
@@ -161,6 +161,82 @@ public class MediumWritePathTests
|
||||
Assert.That(dto.ReleaseType, Is.Null);
|
||||
}
|
||||
|
||||
// 11.G — Description round-trips through both converter directions verbatim (no medium dance,
|
||||
// unlike ReleaseType): entity → DTO preserves the prose, and DTO → entity carries it back.
|
||||
[Test]
|
||||
public void Convert_Description_RoundTripsBothDirections()
|
||||
{
|
||||
const string prose = "A late-night set\nrecorded at the Vault.";
|
||||
var entity = new ReleaseEntity
|
||||
{
|
||||
Title = "Live at the Vault", Artist = "Artist A",
|
||||
Medium = ReleaseMedium.Session, Description = prose,
|
||||
};
|
||||
|
||||
var dto = TrackConverter.Convert(entity);
|
||||
Assert.That(dto.Description, Is.EqualTo(prose), "entity → DTO preserves Description");
|
||||
|
||||
var back = TrackConverter.Convert(dto);
|
||||
Assert.That(back.Description, Is.EqualTo(prose), "DTO → entity preserves Description");
|
||||
}
|
||||
|
||||
// 11.G — a null Description round-trips as null in both directions (existing rows migrate as NULL).
|
||||
[Test]
|
||||
public void Convert_NullDescription_RoundTripsAsNull()
|
||||
{
|
||||
var entity = new ReleaseEntity { Title = "Studio Album", Artist = "Artist C", Description = null };
|
||||
|
||||
var dto = TrackConverter.Convert(entity);
|
||||
Assert.That(dto.Description, Is.Null);
|
||||
|
||||
var back = TrackConverter.Convert(dto);
|
||||
Assert.That(back.Description, Is.Null);
|
||||
}
|
||||
|
||||
// 11.G — Description rides the release-cardinal write channel onto the persisted release row,
|
||||
// exactly as Genre does. FindOrCreateRelease is the upload-path projection point.
|
||||
[Test]
|
||||
public async Task FindOrCreateRelease_NewRelease_PersistsDescription()
|
||||
{
|
||||
const string prose = "Three cuts pressed for the summer.";
|
||||
var manager = CreateManager(CreateRepository());
|
||||
|
||||
var data = ReleaseData("Studio Album", "Artist C", ReleaseMedium.Cut);
|
||||
data.Description = prose;
|
||||
|
||||
var result = await manager.FindOrCreateRelease("Studio Album", "Artist C", data);
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value!.Description, Is.EqualTo(prose));
|
||||
|
||||
var stored = await CreateRepository().GetReleaseByIdAsync(result.Value.Id);
|
||||
Assert.That(stored!.Description, Is.EqualTo(prose));
|
||||
}
|
||||
|
||||
// 11.C — editing a track's linked release sets the Description on the persisted release row,
|
||||
// mirroring the PUT api/track/meta apply (release.Description = request.Description).
|
||||
[Test]
|
||||
public async Task Update_SetsReleaseDescription_PersistsDescription()
|
||||
{
|
||||
const string prose = "Now with a proper blurb.";
|
||||
var repo = CreateRepository();
|
||||
ITrackService manager = CreateManager(repo);
|
||||
|
||||
var release = new ReleaseEntity { Title = "Studio Album", Artist = "Artist C", Medium = ReleaseMedium.Cut };
|
||||
var track = new TrackEntity { EntryKey = "ek-1", TrackName = "Track", Release = release };
|
||||
_context.Tracks.Add(track);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var loaded = (await manager.GetById(track.Id)).Value!;
|
||||
loaded.Release!.Description = prose;
|
||||
|
||||
var result = await manager.Update(loaded);
|
||||
Assert.That(result.Success, Is.True);
|
||||
|
||||
var stored = await CreateRepository().GetReleaseByIdAsync(release.Id);
|
||||
Assert.That(stored!.Description, Is.EqualTo(prose));
|
||||
}
|
||||
|
||||
// 9.5.C — releaseId filter returns only the tracks of the given release. Built on the repository
|
||||
// directly to assert the WHERE release_id predicate in isolation.
|
||||
[Test]
|
||||
|
||||
Reference in New Issue
Block a user