feat: capture and display original upload filename for tracks

This commit is contained in:
daniel-c-harvey
2026-06-07 09:00:17 -04:00
parent 6dfb3a2f23
commit 3de88c786a
14 changed files with 171 additions and 7 deletions
+4 -2
View File
@@ -124,11 +124,12 @@ public class TrackController : ControllerBase
[FromForm] string? album, [FromForm] string? album,
[FromForm] string? genre, [FromForm] string? genre,
[FromForm] string? releaseDate, [FromForm] string? releaseDate,
[FromForm] string? originalFileName,
[FromForm] long createdByUserId, [FromForm] long createdByUserId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
_logger.LogInformation("UploadTrack called: trackName={TrackName}, artist={Artist}, size={Size}", _logger.LogInformation("UploadTrack called: trackName={TrackName}, artist={Artist}, fileName={FileName}, size={Size}",
trackName, artist, wav?.Length); trackName, artist, originalFileName, wav?.Length);
if (wav is null || wav.Length == 0) if (wav is null || wav.Length == 0)
{ {
@@ -182,6 +183,7 @@ public class TrackController : ControllerBase
string.IsNullOrWhiteSpace(genre) ? null : genre, string.IsNullOrWhiteSpace(genre) ? null : genre,
parsedReleaseDate, parsedReleaseDate,
createdByUserId, createdByUserId,
string.IsNullOrWhiteSpace(originalFileName) ? null : originalFileName,
cancellationToken); cancellationToken);
if (!result.Success || result.Value is null) if (!result.Success || result.Value is null)
+2 -1
View File
@@ -49,10 +49,11 @@ public class UnifiedTrackService
string? genre, string? genre,
DateOnly? releaseDate, DateOnly? releaseDate,
long createdByUserId, long createdByUserId,
string? originalFileName,
CancellationToken ct) CancellationToken ct)
{ {
var unpersisted = await _contentTrackContentService.AddTrackFromWavAsync( var unpersisted = await _contentTrackContentService.AddTrackFromWavAsync(
tempFilePath, trackName, artist, album, genre, releaseDate); tempFilePath, trackName, artist, album, genre, releaseDate, originalFileName: originalFileName);
if (unpersisted is null) if (unpersisted is null)
{ {
+5 -2
View File
@@ -29,6 +29,7 @@ public class TrackContentService
/// <param name="album">Optional album name</param> /// <param name="album">Optional album name</param>
/// <param name="genre">Optional genre</param> /// <param name="genre">Optional genre</param>
/// <param name="releaseDate">Optional release date</param> /// <param name="releaseDate">Optional release date</param>
/// <param name="originalFileName">Optional original browser filename captured at upload time</param>
/// <returns>The track entity with generated ID and media path</returns> /// <returns>The track entity with generated ID and media path</returns>
public async Task<TrackEntity?> AddTrackFromWavAsync( public async Task<TrackEntity?> AddTrackFromWavAsync(
string wavFilePath, string wavFilePath,
@@ -36,7 +37,8 @@ public class TrackContentService
string artist, string artist,
string? album = null, string? album = null,
string? genre = null, string? genre = null,
DateOnly? releaseDate = null) DateOnly? releaseDate = null,
string? originalFileName = null)
{ {
try try
{ {
@@ -71,7 +73,8 @@ public class TrackContentService
Artist = artist, Artist = artist,
Album = album, Album = album,
Genre = genre, Genre = genre,
ReleaseDate = releaseDate ReleaseDate = releaseDate,
OriginalFileName = originalFileName
}; };
return trackEntity; return trackEntity;
@@ -53,6 +53,10 @@ public class TrackConfiguration : BaseEntityConfiguration<TrackEntity>
builder.Property(e => e.CreatedByUserId) builder.Property(e => e.CreatedByUserId)
.HasColumnName("created_by_user_id"); .HasColumnName("created_by_user_id");
builder.Property(e => e.OriginalFileName)
.HasMaxLength(500)
.HasColumnName("original_file_name");
// Names the is_deleted index explicitly. BaseEntityConfiguration.Configure already // Names the is_deleted index explicitly. BaseEntityConfiguration.Configure already
// calls HasIndex(e => e.IsDeleted); this adds HasDatabaseName so EF always uses // calls HasIndex(e => e.IsDeleted); this adds HasDatabaseName so EF always uses
// "IX_track_is_deleted" regardless of auto-naming conventions. // "IX_track_is_deleted" regardless of auto-naming conventions.
@@ -0,0 +1,107 @@
// <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("20260607124422_AddOriginalFileName")]
partial class AddOriginalFileName
{
/// <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.TrackEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Album")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("album");
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>("EntryKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("entry_key");
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>("OriginalFileName")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("original_file_name");
b.Property<DateOnly?>("ReleaseDate")
.HasColumnType("date")
.HasColumnName("release_date");
b.Property<string>("TrackName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("track_name");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("IsDeleted")
.HasDatabaseName("IX_track_is_deleted");
b.ToTable("track", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DeepDrftData.Migrations
{
/// <inheritdoc />
public partial class AddOriginalFileName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "original_file_name",
table: "track",
type: "character varying(500)",
maxLength: 500,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "original_file_name",
table: "track");
}
}
}
@@ -72,6 +72,11 @@ namespace DeepDrftData.Migrations
.HasDefaultValue(false) .HasDefaultValue(false)
.HasColumnName("is_deleted"); .HasColumnName("is_deleted");
b.Property<string>("OriginalFileName")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("original_file_name");
b.Property<DateOnly?>("ReleaseDate") b.Property<DateOnly?>("ReleaseDate")
.HasColumnType("date") .HasColumnType("date")
.HasColumnName("release_date"); .HasColumnName("release_date");
+4 -2
View File
@@ -24,7 +24,8 @@ public class TrackConverter : IEntityToModelConverter<TrackEntity, TrackDto>
Genre = entity.Genre, Genre = entity.Genre,
ReleaseDate = entity.ReleaseDate, ReleaseDate = entity.ReleaseDate,
ImagePath = entity.ImagePath, ImagePath = entity.ImagePath,
CreatedByUserId = entity.CreatedByUserId CreatedByUserId = entity.CreatedByUserId,
OriginalFileName = entity.OriginalFileName
}; };
public static TrackEntity Convert(TrackDto model) => new() public static TrackEntity Convert(TrackDto model) => new()
@@ -39,6 +40,7 @@ public class TrackConverter : IEntityToModelConverter<TrackEntity, TrackDto>
Genre = model.Genre, Genre = model.Genre,
ReleaseDate = model.ReleaseDate, ReleaseDate = model.ReleaseDate,
ImagePath = model.ImagePath, ImagePath = model.ImagePath,
CreatedByUserId = model.CreatedByUserId CreatedByUserId = model.CreatedByUserId,
OriginalFileName = model.OriginalFileName
}; };
} }
@@ -47,6 +47,7 @@
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh>
<MudTh>Entry Key</MudTh> <MudTh>Entry Key</MudTh>
<MudTh>File Name</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh> <MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
@@ -56,6 +57,7 @@
<MudTd DataLabel="Genre">@(context.Genre ?? "—")</MudTd> <MudTd DataLabel="Genre">@(context.Genre ?? "—")</MudTd>
<MudTd DataLabel="Release Date">@(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—")</MudTd> <MudTd DataLabel="Release Date">@(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—")</MudTd>
<MudTd DataLabel="Entry Key"><MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText></MudTd> <MudTd DataLabel="Entry Key"><MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText></MudTd>
<MudTd DataLabel="File Name"><MudText Typo="Typo.caption" Style="font-family: monospace;">@(context.OriginalFileName ?? "—")</MudText></MudTd>
<MudTd DataLabel="Actions"> <MudTd DataLabel="Actions">
<MudTooltip Text="Edit"> <MudTooltip Text="Edit">
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudIconButton Icon="@Icons.Material.Filled.Edit"
@@ -144,6 +144,7 @@
string.IsNullOrWhiteSpace(_album) ? null : _album, string.IsNullOrWhiteSpace(_album) ? null : _album,
string.IsNullOrWhiteSpace(_genre) ? null : _genre, string.IsNullOrWhiteSpace(_genre) ? null : _genre,
string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate, string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate,
_selectedFile.Name,
createdByUserId); createdByUserId);
if (result.Success) if (result.Success)
@@ -39,6 +39,7 @@ public class CmsTrackService : ICmsTrackService
string? album, string? album,
string? genre, string? genre,
string? releaseDate, string? releaseDate,
string? originalFileName,
long createdByUserId, long createdByUserId,
CancellationToken ct = default) CancellationToken ct = default)
{ {
@@ -54,6 +55,8 @@ public class CmsTrackService : ICmsTrackService
if (!string.IsNullOrWhiteSpace(album)) multipart.Add(new StringContent(album), "album"); if (!string.IsNullOrWhiteSpace(album)) multipart.Add(new StringContent(album), "album");
if (!string.IsNullOrWhiteSpace(genre)) multipart.Add(new StringContent(genre), "genre"); if (!string.IsNullOrWhiteSpace(genre)) multipart.Add(new StringContent(genre), "genre");
if (!string.IsNullOrWhiteSpace(releaseDate)) multipart.Add(new StringContent(releaseDate), "releaseDate"); 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");
multipart.Add(new StringContent(createdByUserId.ToString()), "createdByUserId"); multipart.Add(new StringContent(createdByUserId.ToString()), "createdByUserId");
var client = _httpClientFactory.CreateClient(ContentCmsClientName); var client = _httpClientFactory.CreateClient(ContentCmsClientName);
@@ -15,6 +15,8 @@ public interface ICmsTrackService
/// Proxy a WAV upload to DeepDrftAPI. The Content API owns the dual-database write and /// Proxy a WAV upload to DeepDrftAPI. The Content API owns the dual-database write and
/// returns the persisted track carrying the SQL-assigned <c>Id</c>. A vault-without-SQL /// returns the persisted track carrying the SQL-assigned <c>Id</c>. A vault-without-SQL
/// orphan is handled and logged server-side; here it surfaces as a failed result. /// orphan is handled and logged server-side; here it surfaces as a failed result.
/// <paramref name="originalFileName"/> is the browser's filename, captured at upload time and
/// stored as metadata; it is not user-editable afterwards.
/// </summary> /// </summary>
Task<ResultContainer<TrackDto>> UploadTrackAsync( Task<ResultContainer<TrackDto>> UploadTrackAsync(
Stream wavStream, Stream wavStream,
@@ -25,6 +27,7 @@ public interface ICmsTrackService
string? album, string? album,
string? genre, string? genre,
string? releaseDate, string? releaseDate,
string? originalFileName,
long createdByUserId, long createdByUserId,
CancellationToken ct = default); CancellationToken ct = default);
+1
View File
@@ -18,4 +18,5 @@ public class TrackDto : BaseModel
public DateOnly? ReleaseDate { get; set; } public DateOnly? ReleaseDate { get; set; }
public string? ImagePath { get; set; } public string? ImagePath { get; set; }
public long? CreatedByUserId { get; set; } public long? CreatedByUserId { get; set; }
public string? OriginalFileName { get; set; }
} }
+1
View File
@@ -15,4 +15,5 @@ public class TrackEntity : BaseEntity, IEntity
public DateOnly? ReleaseDate { get; set; } public DateOnly? ReleaseDate { get; set; }
public string? ImagePath { get; set; } public string? ImagePath { get; set; }
public long? CreatedByUserId { get; set; } public long? CreatedByUserId { get; set; }
public string? OriginalFileName { get; set; }
} }