Flip ITrackService/TrackManager to DTO output; TrackConverter is the sole entity<->DTO path across all consumers

This commit is contained in:
Daniel Harvey
2026-05-25 11:35:04 -04:00
parent 81fc87391b
commit 4351302a25
23 changed files with 156 additions and 156 deletions
+2 -2
View File
@@ -68,7 +68,7 @@ public class TrackController : ControllerBase
return Ok(result.Value); return Ok(result.Value);
} }
// POST api/track/upload: raw WAV in (multipart/form-data) + metadata → persisted TrackEntity out. // POST api/track/upload: raw WAV in (multipart/form-data) + metadata → persisted TrackDto out.
// Used by the CMS upload flow on DeepDrftManager; that host proxies the upload here so it never // Used by the CMS upload flow on DeepDrftManager; that host proxies the upload here so it never
// touches the vault disk path or SQL directly. UnifiedTrackService owns the two-database write. // touches the vault disk path or SQL directly. UnifiedTrackService owns the two-database write.
// //
@@ -80,7 +80,7 @@ public class TrackController : ControllerBase
[HttpPost("upload")] [HttpPost("upload")]
[RequestSizeLimit(1_073_741_824)] [RequestSizeLimit(1_073_741_824)]
[RequestFormLimits(MultipartBodyLengthLimit = 1_073_741_824)] [RequestFormLimits(MultipartBodyLengthLimit = 1_073_741_824)]
public async Task<ActionResult<DeepDrftModels.Entities.TrackEntity>> UploadTrack( public async Task<ActionResult<DeepDrftModels.DTOs.TrackDto>> UploadTrack(
[FromForm] IFormFile? wav, [FromForm] IFormFile? wav,
[FromForm] string? trackName, [FromForm] string? trackName,
[FromForm] string? artist, [FromForm] string? artist,
+6 -6
View File
@@ -1,7 +1,7 @@
using DeepDrftContent; using DeepDrftContent;
using DeepDrftContent.Constants; using DeepDrftContent.Constants;
using DeepDrftData; using DeepDrftData;
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using NetBlocks.Models; using NetBlocks.Models;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase; using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
@@ -34,10 +34,10 @@ public class UnifiedTrackService
/// <summary> /// <summary>
/// Process a WAV into the vault, then persist its metadata to SQL. On success the returned /// Process a WAV into the vault, then persist its metadata to SQL. On success the returned
/// entity carries the SQL-assigned Id. If the vault write succeeds but the SQL persist fails, /// DTO carries the SQL-assigned Id. If the vault write succeeds but the SQL persist fails,
/// the audio is orphaned under EntryKey — logged loudly so it is recoverable manually. /// the audio is orphaned under EntryKey — logged loudly so it is recoverable manually.
/// </summary> /// </summary>
public async Task<ResultContainer<TrackEntity>> UploadAsync( public async Task<ResultContainer<TrackDto>> UploadAsync(
string tempFilePath, string tempFilePath,
string trackName, string trackName,
string artist, string artist,
@@ -53,12 +53,12 @@ public class UnifiedTrackService
if (unpersisted is null) if (unpersisted is null)
{ {
_logger.LogWarning("UploadAsync: content TrackContentService returned null for {TrackName}", trackName); _logger.LogWarning("UploadAsync: content TrackContentService returned null for {TrackName}", trackName);
return ResultContainer<TrackEntity>.CreateFailResult("Failed to process and store WAV."); return ResultContainer<TrackDto>.CreateFailResult("Failed to process and store WAV.");
} }
unpersisted.CreatedByUserId = createdByUserId; unpersisted.CreatedByUserId = createdByUserId;
var saveResult = await _sqlTrackService.Create(unpersisted); var saveResult = await _sqlTrackService.Create(TrackConverter.Convert(unpersisted));
if (!saveResult.Success || saveResult.Value is null) if (!saveResult.Success || saveResult.Value is null)
{ {
// Vault write succeeded, SQL persist failed — audio is orphaned in the tracks vault // Vault write succeeded, SQL persist failed — audio is orphaned in the tracks vault
@@ -67,7 +67,7 @@ public class UnifiedTrackService
_logger.LogError( _logger.LogError(
"Track persisted to vault but SQL save failed. Orphaned entry: {EntryKey}. Error: {Error}", "Track persisted to vault but SQL save failed. Orphaned entry: {EntryKey}. Error: {Error}",
unpersisted.EntryKey, error); unpersisted.EntryKey, error);
return ResultContainer<TrackEntity>.CreateFailResult($"Track was uploaded but could not be saved: {error}"); return ResultContainer<TrackDto>.CreateFailResult($"Track was uploaded but could not be saved: {error}");
} }
return saveResult; return saveResult;
+2 -2
View File
@@ -164,8 +164,8 @@ public class CliService
return; return;
} }
// Add track to SQL database // Add track to SQL database (service layer is DTO-typed)
var result = await _webTrackService.Create(trackEntity); var result = await _webTrackService.Create(DeepDrftData.TrackConverter.Convert(trackEntity));
if (result.Success && result.Value != null) if (result.Success && result.Value != null)
{ {
Console.WriteLine($"✓ Track added successfully!"); Console.WriteLine($"✓ Track added successfully!");
+8 -7
View File
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Terminal.Gui; using Terminal.Gui;
using DeepDrftModels.Entities; using DeepDrftData;
using DeepDrftModels.DTOs;
using DeepDrftCli.Utils; using DeepDrftCli.Utils;
namespace DeepDrftCli.Services; namespace DeepDrftCli.Services;
@@ -20,7 +21,7 @@ public class GuiService
private ListView? _trackListView; private ListView? _trackListView;
private TextView? _statusView; private TextView? _statusView;
private FrameView? _legendFrame; private FrameView? _legendFrame;
private List<TrackEntity> _tracks = new(); private List<TrackDto> _tracks = new();
public GuiService( public GuiService(
ILogger<GuiService> logger, ILogger<GuiService> logger,
@@ -541,7 +542,7 @@ public class GuiService
/// <summary> /// <summary>
/// Delete the specified track from the database /// Delete the specified track from the database
/// </summary> /// </summary>
private async Task DeleteTrackAsync(TrackEntity trackToDelete) private async Task DeleteTrackAsync(TrackDto trackToDelete)
{ {
try try
{ {
@@ -650,8 +651,8 @@ public class GuiService
return false; return false;
} }
// Add to SQL database // Add to SQL database (service layer is DTO-typed)
var result = await _webTrackService.Create(trackEntity); var result = await _webTrackService.Create(TrackConverter.Convert(trackEntity));
if (result.Success && result.Value != null) if (result.Success && result.Value != null)
{ {
UpdateStatus($"✓ Track '{trackName}' by {artist} added successfully!"); UpdateStatus($"✓ Track '{trackName}' by {artist} added successfully!");
@@ -676,7 +677,7 @@ public class GuiService
/// <summary> /// <summary>
/// Validate input and update existing track in database /// Validate input and update existing track in database
/// </summary> /// </summary>
private async Task<bool> ValidateAndUpdateTrackAsync(TrackEntity originalTrack, string trackName, private async Task<bool> ValidateAndUpdateTrackAsync(TrackDto originalTrack, string trackName,
string artist, string album, string genre, string releaseDate) string artist, string album, string genre, string releaseDate)
{ {
try try
@@ -709,7 +710,7 @@ public class GuiService
UpdateStatus("Updating track..."); UpdateStatus("Updating track...");
// Create updated track entity // Create updated track entity
var updatedTrack = new TrackEntity var updatedTrack = new TrackDto
{ {
Id = originalTrack.Id, Id = originalTrack.Id,
EntryKey = originalTrack.EntryKey, // Keep original entry key EntryKey = originalTrack.EntryKey, // Keep original entry key
+11 -6
View File
@@ -1,15 +1,20 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
namespace DeepDrftData; namespace DeepDrftData;
/// <summary>
/// SQL-side track service. Repository outputs entities; this service outputs DTOs via
/// TrackConverter. In-process consumers (UnifiedTrackService, CLI, DeepDrftPublic) all
/// receive DTOs.
/// </summary>
public interface ITrackService public interface ITrackService
{ {
Task<ResultContainer<TrackEntity?>> GetById(long id); Task<ResultContainer<TrackDto?>> GetById(long id);
Task<ResultContainer<List<TrackEntity>>> GetAll(); Task<ResultContainer<List<TrackDto>>> GetAll();
Task<ResultContainer<PagedResult<TrackEntity>>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default); Task<ResultContainer<PagedResult<TrackDto>>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default);
Task<ResultContainer<TrackEntity>> Create(TrackEntity newTrack); Task<ResultContainer<TrackDto>> Create(TrackDto newTrack);
Task<ResultContainer<TrackEntity>> Update(TrackEntity track); Task<ResultContainer<TrackDto>> Update(TrackDto track);
Task<Result> Delete(long id); Task<Result> Delete(long id);
} }
+36 -42
View File
@@ -9,14 +9,15 @@ using NetBlocks.Models;
namespace DeepDrftData; namespace DeepDrftData;
/// <summary> /// <summary>
/// SQL-side track orchestrator built on the BlazorBlocks Manager base. Two surfaces coexist: /// SQL-side track service built on the BlazorBlocks Manager base. The layer boundary:
/// TrackRepository outputs entities; this service outputs DTOs via TrackConverter — the
/// single authoritative entity↔DTO conversion path. The ITrackService surface is DTO-typed
/// throughout; the entity never escapes the service layer.
/// ///
/// - The DTO-shaped IManager surface (GetById → TrackDto, etc.) inherited from Manager&lt;&gt;. /// The base Manager&lt;&gt; surface does not line up with ITrackService by signature (base
/// - The entity-shaped ITrackService surface retained for backward compatibility with the /// Add vs Create, base Update→Result vs Update→DTO, base Get/GetPage vs GetAll/GetPaged,
/// web host, CMS, and CLI — all existing controllers and pages inject ITrackService and /// base GetById→TDto vs GetById→TDto?), so the query and mutation methods are implemented
/// expect TrackEntity-typed results. The two GetById overloads conflict on signature, so /// here over Repository + TrackConverter. Only Delete(long)→Result is inherited unchanged.
/// ITrackService.GetById is implemented explicitly; the base Manager.Delete satisfies
/// both interfaces because the signatures align.
/// </summary> /// </summary>
public class TrackManager public class TrackManager
: Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>, ITrackService : Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>, ITrackService
@@ -28,39 +29,38 @@ public class TrackManager
{ {
} }
// --- ITrackService implementation (entity-space; calls Repository directly) --- // Explicit impl: base GetById returns ResultContainer<TrackDto> (fails on miss); the
// service contract is ResultContainer<TrackDto?> (pass with null on miss). Return types
// Explicit interface implementation — IManager.GetById returns ResultContainer<TrackDto> // differ, so this cannot be a public overload of the inherited member.
// (inherited from Manager<>), so this entity-typed overload cannot coexist as a public async Task<ResultContainer<TrackDto?>> ITrackService.GetById(long id)
// member with the same name. Callers always inject ITrackService, so the explicit impl
// resolves correctly at the call site.
async Task<ResultContainer<TrackEntity?>> ITrackService.GetById(long id)
{ {
try try
{ {
var entity = await Repository.GetByIdAsync(id); var entity = await Repository.GetByIdAsync(id);
return ResultContainer<TrackEntity?>.CreatePassResult(entity); return ResultContainer<TrackDto?>.CreatePassResult(
entity is null ? null : TrackConverter.Convert(entity));
} }
catch (Exception e) catch (Exception e)
{ {
return ResultContainer<TrackEntity?>.CreateFailResult(e.Message); return ResultContainer<TrackDto?>.CreateFailResult(e.Message);
} }
} }
public async Task<ResultContainer<List<TrackEntity>>> GetAll() public async Task<ResultContainer<List<TrackDto>>> GetAll()
{ {
try try
{ {
var entities = await Repository.GetAllAsync(); var entities = await Repository.GetAllAsync();
return ResultContainer<List<TrackEntity>>.CreatePassResult(entities.ToList()); return ResultContainer<List<TrackDto>>.CreatePassResult(
entities.Select(TrackConverter.Convert).ToList());
} }
catch (Exception e) catch (Exception e)
{ {
return ResultContainer<List<TrackEntity>>.CreateFailResult(e.Message); return ResultContainer<List<TrackDto>>.CreateFailResult(e.Message);
} }
} }
public async Task<ResultContainer<PagedResult<TrackEntity>>> GetPaged( public async Task<ResultContainer<PagedResult<TrackDto>>> GetPaged(
int pageNumber, int pageNumber,
int pageSize, int pageSize,
string? sortColumn, string? sortColumn,
@@ -86,52 +86,46 @@ public class TrackManager
}; };
var page = await Repository.GetPagedAsync(parameters); var page = await Repository.GetPagedAsync(parameters);
return ResultContainer<PagedResult<TrackEntity>>.CreatePassResult(page); var dtoPage = PagedResult<TrackDto>.From(page, page.Items.Select(TrackConverter.Convert));
return ResultContainer<PagedResult<TrackDto>>.CreatePassResult(dtoPage);
} }
catch (Exception e) catch (Exception e)
{ {
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult(e.Message); return ResultContainer<PagedResult<TrackDto>>.CreateFailResult(e.Message);
} }
} }
public async Task<ResultContainer<TrackEntity>> Create(TrackEntity newTrack) public async Task<ResultContainer<TrackDto>> Create(TrackDto newTrack)
{ {
try try
{ {
var added = await Repository.AddAsync(newTrack); var added = await Repository.AddAsync(TrackConverter.Convert(newTrack));
return ResultContainer<TrackEntity>.CreatePassResult(added); return ResultContainer<TrackDto>.CreatePassResult(TrackConverter.Convert(added));
} }
catch (Exception e) catch (Exception e)
{ {
return ResultContainer<TrackEntity>.CreateFailResult(e.Message); return ResultContainer<TrackDto>.CreateFailResult(e.Message);
} }
} }
// Manager<>.Update takes TrackDto and returns Result; this Update keeps the // Explicit impl: base Update returns Result; the service contract returns the persisted
// entity-typed contract callers expect and returns the post-update entity for the // DTO so the CMS edit flow reads back DB-authoritative values.
// existing CMS edit flow that reads back the persisted values. async Task<ResultContainer<TrackDto>> ITrackService.Update(TrackDto track)
/// <summary>
/// Updates the track's metadata fields and returns the DB-authoritative entity.
/// The caller's <paramref name="track"/> object has its <c>UpdatedAt</c> field
/// mutated in place by <see cref="TrackRepository.UpdateAsync"/>; do not reuse it.
/// </summary>
public async Task<ResultContainer<TrackEntity>> Update(TrackEntity track)
{ {
try try
{ {
await Repository.UpdateAsync(track); await Repository.UpdateAsync(TrackConverter.Convert(track));
var updated = await Repository.GetByIdAsync(track.Id); var updated = await Repository.GetByIdAsync(track.Id);
return updated is not null return updated is not null
? ResultContainer<TrackEntity>.CreatePassResult(updated) ? ResultContainer<TrackDto>.CreatePassResult(TrackConverter.Convert(updated))
: ResultContainer<TrackEntity>.CreateFailResult("Track not found after update."); : ResultContainer<TrackDto>.CreateFailResult("Track not found after update.");
} }
catch (Exception e) catch (Exception e)
{ {
return ResultContainer<TrackEntity>.CreateFailResult(e.Message); return ResultContainer<TrackDto>.CreateFailResult(e.Message);
} }
} }
// Delete(long) is inherited from Manager<> — its Task<Result> signature already // Delete(long) → Result is inherited from Manager<> and satisfies ITrackService.Delete
// satisfies ITrackService.Delete, and the base implementation performs the soft delete // by signature. No override.
// via Repository.DeleteAsync. No override needed.
} }
@@ -90,7 +90,7 @@
@code { @code {
[Parameter] public long Id { get; set; } [Parameter] public long Id { get; set; }
private TrackEntity? _track; private TrackDto? _track;
private TrackEditForm _form = new(); private TrackEditForm _form = new();
private bool _loading = true; private bool _loading = true;
private bool _busy; private bool _busy;
@@ -199,7 +199,7 @@
public string? Genre { get; set; } public string? Genre { get; set; }
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public static TrackEditForm From(TrackEntity track) => new() public static TrackEditForm From(TrackDto track) => new()
{ {
TrackName = track.TrackName, TrackName = track.TrackName,
Artist = track.Artist, Artist = track.Artist,
@@ -20,7 +20,7 @@
</MudButton> </MudButton>
</MudStack> </MudStack>
<MudTable T="TrackEntity" <MudTable T="TrackDto"
@ref="_table" @ref="_table"
ServerData="LoadServerData" ServerData="LoadServerData"
Hover="true" Hover="true"
@@ -37,11 +37,11 @@
<MudText Typo="Typo.body1">Loading tracks…</MudText> <MudText Typo="Typo.body1">Loading tracks…</MudText>
</LoadingContent> </LoadingContent>
<HeaderContent> <HeaderContent>
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackEntity" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackDto" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackEntity">Artist</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="Artist" T="TrackDto">Artist</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackEntity">Album</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="Album" T="TrackDto">Album</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackEntity">Genre</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackEntity">Release Date</MudTableSortLabel></MudTh> <MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh>
<MudTh>Entry Key</MudTh> <MudTh>Entry Key</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh> <MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
</HeaderContent> </HeaderContent>
@@ -74,9 +74,9 @@
</MudContainer> </MudContainer>
@code { @code {
private MudTable<TrackEntity>? _table; private MudTable<TrackDto>? _table;
private async Task<TableData<TrackEntity>> LoadServerData(TableState state, CancellationToken cancellationToken) private async Task<TableData<TrackDto>> LoadServerData(TableState state, CancellationToken cancellationToken)
{ {
var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based. var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based.
var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel; var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel;
@@ -88,18 +88,18 @@
{ {
var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error); Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error);
return new TableData<TrackEntity> { Items = Array.Empty<TrackEntity>(), TotalItems = 0 }; return new TableData<TrackDto> { Items = Array.Empty<TrackDto>(), TotalItems = 0 };
} }
var page = result.Value; var page = result.Value;
return new TableData<TrackEntity> return new TableData<TrackDto>
{ {
Items = page.Items, Items = page.Items,
TotalItems = page.TotalCount TotalItems = page.TotalCount
}; };
} }
private async Task ConfirmAndDelete(TrackEntity track) private async Task ConfirmAndDelete(TrackDto track)
{ {
var confirmed = await DialogService.ShowMessageBox( var confirmed = await DialogService.ShowMessageBox(
title: "Delete track", title: "Delete track",
+1 -1
View File
@@ -11,7 +11,7 @@
@using AuthBlocksWeb.Components @using AuthBlocksWeb.Components
@using DeepDrftManager @using DeepDrftManager
@using DeepDrftManager.Components @using DeepDrftManager.Components
@using DeepDrftModels.Entities @using DeepDrftModels.DTOs
@using Models.Common @using Models.Common
@using AuthBlocksModels.SystemDefinitions @using AuthBlocksModels.SystemDefinitions
@using AuthBlocksWeb.HierarchicalAuthorize @using AuthBlocksWeb.HierarchicalAuthorize
+30 -30
View File
@@ -1,7 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
@@ -30,7 +30,7 @@ public class CmsTrackService : ICmsTrackService
_logger = logger; _logger = logger;
} }
public async Task<ResultContainer<TrackEntity>> UploadTrackAsync( public async Task<ResultContainer<TrackDto>> UploadTrackAsync(
Stream wavStream, Stream wavStream,
string fileName, string fileName,
string contentType, string contentType,
@@ -67,7 +67,7 @@ public class CmsTrackService : ICmsTrackService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Content API call failed for upload of {TrackName}", trackName); _logger.LogError(ex, "Content API call failed for upload of {TrackName}", trackName);
return ResultContainer<TrackEntity>.CreateFailResult("Content API is unreachable."); return ResultContainer<TrackDto>.CreateFailResult("Content API is unreachable.");
} }
using (response) using (response)
@@ -79,35 +79,35 @@ public class CmsTrackService : ICmsTrackService
if (statusCode >= 500) if (statusCode >= 500)
{ {
_logger.LogError("Content API returned {Status} for upload of {TrackName}: {Body}", statusCode, trackName, body); _logger.LogError("Content API returned {Status} for upload of {TrackName}: {Body}", statusCode, trackName, body);
return ResultContainer<TrackEntity>.CreateFailResult("Upload failed on the content server. Please try again."); return ResultContainer<TrackDto>.CreateFailResult("Upload failed on the content server. Please try again.");
} }
// 4xx: body is user-friendly validation text from DeepDrftAPI — relay as-is. // 4xx: body is user-friendly validation text from DeepDrftAPI — relay as-is.
_logger.LogWarning("Content API rejected upload: {Status} {Body}", statusCode, body); _logger.LogWarning("Content API rejected upload: {Status} {Body}", statusCode, body);
return ResultContainer<TrackEntity>.CreateFailResult( return ResultContainer<TrackDto>.CreateFailResult(
string.IsNullOrWhiteSpace(body) ? $"Upload rejected ({statusCode})." : body); string.IsNullOrWhiteSpace(body) ? $"Upload rejected ({statusCode})." : body);
} }
// The Content API now owns the dual-database write, so the response is the persisted // The Content API now owns the dual-database write, so the response is the persisted
// entity (Id > 0) — no SQL roundtrip here. // track DTO (Id > 0) — no SQL roundtrip here.
TrackEntity? persisted; TrackDto? persisted;
try try
{ {
persisted = await response.Content.ReadFromJsonAsync<TrackEntity>(ct); persisted = await response.Content.ReadFromJsonAsync<TrackDto>(ct);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to deserialize TrackEntity from Content API response"); _logger.LogError(ex, "Failed to deserialize TrackDto from Content API response");
return ResultContainer<TrackEntity>.CreateFailResult("Content API returned an unexpected response."); return ResultContainer<TrackDto>.CreateFailResult("Content API returned an unexpected response.");
} }
if (persisted is null) if (persisted is null)
{ {
_logger.LogError("Content API returned a null TrackEntity"); _logger.LogError("Content API returned a null TrackDto");
return ResultContainer<TrackEntity>.CreateFailResult("Content API returned an empty response."); return ResultContainer<TrackDto>.CreateFailResult("Content API returned an empty response.");
} }
return ResultContainer<TrackEntity>.CreatePassResult(persisted); return ResultContainer<TrackDto>.CreatePassResult(persisted);
} }
} }
@@ -144,7 +144,7 @@ public class CmsTrackService : ICmsTrackService
} }
} }
public async Task<ResultContainer<PagedResult<TrackEntity>>> GetPagedAsync( public async Task<ResultContainer<PagedResult<TrackDto>>> GetPagedAsync(
int page, int pageSize, string? sortColumn, bool sortDescending, int page, int pageSize, string? sortColumn, bool sortDescending,
CancellationToken ct = default) CancellationToken ct = default)
{ {
@@ -163,7 +163,7 @@ public class CmsTrackService : ICmsTrackService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Content API call failed for track page"); _logger.LogError(ex, "Content API call failed for track page");
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult("Content API is unreachable."); return ResultContainer<PagedResult<TrackDto>>.CreateFailResult("Content API is unreachable.");
} }
using (response) using (response)
@@ -171,31 +171,31 @@ public class CmsTrackService : ICmsTrackService
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
_logger.LogError("Content API track page failed: {Status}", (int)response.StatusCode); _logger.LogError("Content API track page failed: {Status}", (int)response.StatusCode);
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult("Failed to load tracks."); return ResultContainer<PagedResult<TrackDto>>.CreateFailResult("Failed to load tracks.");
} }
PagedResult<TrackEntity>? paged; PagedResult<TrackDto>? paged;
try try
{ {
paged = await response.Content.ReadFromJsonAsync<PagedResult<TrackEntity>>(ct); paged = await response.Content.ReadFromJsonAsync<PagedResult<TrackDto>>(ct);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to deserialize PagedResult from Content API response"); _logger.LogError(ex, "Failed to deserialize PagedResult from Content API response");
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult("Content API returned an unexpected response."); return ResultContainer<PagedResult<TrackDto>>.CreateFailResult("Content API returned an unexpected response.");
} }
if (paged is null) if (paged is null)
{ {
_logger.LogError("Content API returned a null PagedResult"); _logger.LogError("Content API returned a null PagedResult");
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult("Content API returned an empty response."); return ResultContainer<PagedResult<TrackDto>>.CreateFailResult("Content API returned an empty response.");
} }
return ResultContainer<PagedResult<TrackEntity>>.CreatePassResult(paged); return ResultContainer<PagedResult<TrackDto>>.CreatePassResult(paged);
} }
} }
public async Task<ResultContainer<TrackEntity?>> GetByIdAsync(long id, CancellationToken ct = default) public async Task<ResultContainer<TrackDto?>> GetByIdAsync(long id, CancellationToken ct = default)
{ {
var client = _httpClientFactory.CreateClient(ContentCmsClientName); var client = _httpClientFactory.CreateClient(ContentCmsClientName);
@@ -207,34 +207,34 @@ public class CmsTrackService : ICmsTrackService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Content API call failed for track {TrackId}", id); _logger.LogError(ex, "Content API call failed for track {TrackId}", id);
return ResultContainer<TrackEntity?>.CreateFailResult("Content API is unreachable."); return ResultContainer<TrackDto?>.CreateFailResult("Content API is unreachable.");
} }
using (response) using (response)
{ {
if (response.StatusCode == HttpStatusCode.NotFound) if (response.StatusCode == HttpStatusCode.NotFound)
{ {
return ResultContainer<TrackEntity?>.CreatePassResult(null); return ResultContainer<TrackDto?>.CreatePassResult(null);
} }
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
_logger.LogError("Content API track lookup failed for {TrackId}: {Status}", id, (int)response.StatusCode); _logger.LogError("Content API track lookup failed for {TrackId}: {Status}", id, (int)response.StatusCode);
return ResultContainer<TrackEntity?>.CreateFailResult("Failed to load track."); return ResultContainer<TrackDto?>.CreateFailResult("Failed to load track.");
} }
TrackEntity? track; TrackDto? track;
try try
{ {
track = await response.Content.ReadFromJsonAsync<TrackEntity>(ct); track = await response.Content.ReadFromJsonAsync<TrackDto>(ct);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to deserialize TrackEntity from Content API response"); _logger.LogError(ex, "Failed to deserialize TrackDto from Content API response");
return ResultContainer<TrackEntity?>.CreateFailResult("Content API returned an unexpected response."); return ResultContainer<TrackDto?>.CreateFailResult("Content API returned an unexpected response.");
} }
return ResultContainer<TrackEntity?>.CreatePassResult(track); return ResultContainer<TrackDto?>.CreatePassResult(track);
} }
} }
+5 -5
View File
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
@@ -13,10 +13,10 @@ public interface ICmsTrackService
{ {
/// <summary> /// <summary>
/// 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 entity 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.
/// </summary> /// </summary>
Task<ResultContainer<TrackEntity>> UploadTrackAsync( Task<ResultContainer<TrackDto>> UploadTrackAsync(
Stream wavStream, Stream wavStream,
string fileName, string fileName,
string contentType, string contentType,
@@ -37,7 +37,7 @@ public interface ICmsTrackService
/// <summary> /// <summary>
/// Fetch a page of track metadata from the Content API's <c>GET api/track/page</c>. /// Fetch a page of track metadata from the Content API's <c>GET api/track/page</c>.
/// </summary> /// </summary>
Task<ResultContainer<PagedResult<TrackEntity>>> GetPagedAsync( Task<ResultContainer<PagedResult<TrackDto>>> GetPagedAsync(
int page, int pageSize, string? sortColumn, bool sortDescending, int page, int pageSize, string? sortColumn, bool sortDescending,
CancellationToken ct = default); CancellationToken ct = default);
@@ -45,7 +45,7 @@ public interface ICmsTrackService
/// Fetch a single track's metadata from <c>GET api/track/meta/{id}</c>. A 404 returns a /// Fetch a single track's metadata from <c>GET api/track/meta/{id}</c>. A 404 returns a
/// passing result with a null value. /// passing result with a null value.
/// </summary> /// </summary>
Task<ResultContainer<TrackEntity?>> GetByIdAsync(long id, CancellationToken ct = default); Task<ResultContainer<TrackDto?>> GetByIdAsync(long id, CancellationToken ct = default);
/// <summary> /// <summary>
/// Update a track's metadata via <c>PUT api/track/meta/{id}</c>. EntryKey is immutable and /// Update a track's metadata via <c>PUT api/track/meta/{id}</c>. EntryKey is immutable and
+4 -4
View File
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
using System.Text.Json; using System.Text.Json;
@@ -16,7 +16,7 @@ public class TrackClient
_http = httpClientFactory.CreateClient("DeepDrft.API"); _http = httpClientFactory.CreateClient("DeepDrft.API");
} }
public async Task<ApiResult<PagedResult<TrackEntity>>> GetPage( public async Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber, int pageNumber,
int pageSize, int pageSize,
string? sortColumn = null, string? sortColumn = null,
@@ -38,11 +38,11 @@ public class TrackClient
var response = await _http.GetAsync($"api/track/page{query}"); var response = await _http.GetAsync($"api/track/page{query}");
var json = await response.Content.ReadAsStringAsync(); var json = await response.Content.ReadAsStringAsync();
var dto = JsonSerializer.Deserialize<ApiResultDto<PagedResult<TrackEntity>>>(json, new JsonSerializerOptions var dto = JsonSerializer.Deserialize<ApiResultDto<PagedResult<TrackDto>>>(json, new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
}); });
return dto?.From() ?? ApiResult<PagedResult<TrackEntity>>.CreateFailResult("Failed to deserialize response"); return dto?.From() ?? ApiResult<PagedResult<TrackDto>>.CreateFailResult("Failed to deserialize response");
} }
} }
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services; using DeepDrftPublic.Client.Services;
using DeepDrftPublic.Client.ViewModels; using DeepDrftPublic.Client.ViewModels;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -11,7 +11,7 @@ public partial class TracksView : ComponentBase
[Inject] public required TracksViewModel ViewModel { get; set; } [Inject] public required TracksViewModel ViewModel { get; set; }
[CascadingParameter] public required IPlayerService PlayerService { get; set; } [CascadingParameter] public required IPlayerService PlayerService { get; set; }
private TrackEntity? _selectedTrack = null; private TrackDto? _selectedTrack = null;
private int _clickCount = 0; private int _clickCount = 0;
private string _lifecycleStatus = "Not initialized"; private string _lifecycleStatus = "Not initialized";
@@ -40,14 +40,14 @@ public partial class TracksView : ComponentBase
{ {
var result = await ViewModel.TrackData.GetPage(newPage, ViewModel.PageSize, ViewModel.SortBy, ViewModel.IsDescending); var result = await ViewModel.TrackData.GetPage(newPage, ViewModel.PageSize, ViewModel.SortBy, ViewModel.IsDescending);
if (result is { Success: true, Value: PagedResult<TrackEntity> pageResult }) if (result is { Success: true, Value: PagedResult<TrackDto> pageResult })
{ {
ViewModel.Page = pageResult; ViewModel.Page = pageResult;
ViewModel.PageSize = pageResult.PageSize; ViewModel.PageSize = pageResult.PageSize;
} }
} }
private async Task PlayTrack(TrackEntity? track) private async Task PlayTrack(TrackDto? track)
{ {
if (track == null && _selectedTrack == null || track?.Id == _selectedTrack?.Id) return; if (track == null && _selectedTrack == null || track?.Id == _selectedTrack?.Id) return;
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Clients; using DeepDrftPublic.Client.Clients;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using NetBlocks.Models; using NetBlocks.Models;
@@ -32,7 +32,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
/// <see cref="SelectTrack"/>/<see cref="Unload"/> path are responsible for managing /// <see cref="SelectTrack"/>/<see cref="Unload"/> path are responsible for managing
/// it themselves. /// it themselves.
/// </summary> /// </summary>
public TrackEntity? CurrentTrack { get; protected set; } public TrackDto? CurrentTrack { get; protected set; }
// Events // Events
public EventCallback? OnStateChanged { get; set; } public EventCallback? OnStateChanged { get; set; }
@@ -74,7 +74,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
} }
} }
public virtual async Task SelectTrack(TrackEntity track) public virtual async Task SelectTrack(TrackDto track)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
@@ -87,7 +87,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
await NotifyStateChanged(); await NotifyStateChanged();
} }
private async Task LoadTrack(TrackEntity track) private async Task LoadTrack(TrackDto track)
{ {
try try
{ {
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using NetBlocks.Models; using NetBlocks.Models;
@@ -17,7 +17,7 @@ public interface IPlayerService
double Volume { get; } double Volume { get; }
double LoadProgress { get; } double LoadProgress { get; }
string? ErrorMessage { get; } string? ErrorMessage { get; }
TrackEntity? CurrentTrack { get; } TrackDto? CurrentTrack { get; }
// Events for UI updates // Events for UI updates
EventCallback? OnStateChanged { get; set; } EventCallback? OnStateChanged { get; set; }
@@ -25,7 +25,7 @@ public interface IPlayerService
// Control methods // Control methods
Task InitializeAsync(); Task InitializeAsync();
Task SelectTrack(TrackEntity track); Task SelectTrack(TrackDto track);
Task Stop(); Task Stop();
Task Unload(); Task Unload();
Task TogglePlayPause(); Task TogglePlayPause();
@@ -43,5 +43,5 @@ public interface IStreamingPlayerService : IPlayerService
int BufferedChunks { get; } int BufferedChunks { get; }
// Streaming control methods // Streaming control methods
Task SelectTrackStreaming(TrackEntity track); Task SelectTrackStreaming(TrackDto track);
} }
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
@@ -17,7 +17,7 @@ namespace DeepDrftPublic.Client.Services;
/// </summary> /// </summary>
public interface ITrackDataService public interface ITrackDataService
{ {
Task<ApiResult<PagedResult<TrackEntity>>> GetPage( Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber, int pageNumber,
int pageSize, int pageSize,
string? sortColumn = null, string? sortColumn = null,
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Clients; using DeepDrftPublic.Client.Clients;
using System.Buffers; using System.Buffers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -41,12 +41,12 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
_logger = logger; _logger = logger;
} }
public override async Task SelectTrack(TrackEntity track) public override async Task SelectTrack(TrackDto track)
{ {
await SelectTrackStreaming(track); await SelectTrackStreaming(track);
} }
public async Task SelectTrackStreaming(TrackEntity track) public async Task SelectTrackStreaming(TrackDto track)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
@@ -59,7 +59,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
await NotifyStateChanged(); await NotifyStateChanged();
} }
private async Task LoadTrackStreaming(TrackEntity track) private async Task LoadTrackStreaming(TrackDto track)
{ {
// Always reset to clean state before loading new track. ResetToIdle // Always reset to clean state before loading new track. ResetToIdle
// both cancels and awaits any in-flight streaming loop, so by the time // both cancels and awaits any in-flight streaming loop, so by the time
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Clients; using DeepDrftPublic.Client.Clients;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
@@ -19,7 +19,7 @@ public class TrackClientDataService : ITrackDataService
_trackClient = trackClient; _trackClient = trackClient;
} }
public Task<ApiResult<PagedResult<TrackEntity>>> GetPage( public Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber, int pageNumber,
int pageSize, int pageSize,
string? sortColumn = null, string? sortColumn = null,
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services; using DeepDrftPublic.Client.Services;
using Models.Common; using Models.Common;
@@ -15,7 +15,7 @@ public class TracksViewModel
get => Page?.PageSize ?? 15; get => Page?.PageSize ?? 15;
set set
{ {
if (Page == null) throw new Exception(); if (Page == null) return;
if (value != Page.PageSize) if (value != Page.PageSize)
{ {
Page.PageSize = value; Page.PageSize = value;
@@ -24,7 +24,7 @@ public class TracksViewModel
} }
public string SortBy { get; set; } = string.Empty; public string SortBy { get; set; } = string.Empty;
public bool IsDescending { get; set; } = false; public bool IsDescending { get; set; } = false;
public PagedResult<TrackEntity>? Page { get; set; } = null; public PagedResult<TrackDto>? Page { get; set; } = null;
public TracksViewModel(ITrackDataService trackData) public TracksViewModel(ITrackDataService trackData)
{ {
@@ -1,5 +1,5 @@
using DeepDrftData; using DeepDrftData;
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
@@ -18,15 +18,15 @@ public class TrackController : ControllerBase
} }
[HttpGet("page")] [HttpGet("page")]
public async Task<ActionResult<ApiResultDto<PagedResult<TrackEntity>>>> GetPage( public async Task<ActionResult<ApiResultDto<PagedResult<TrackDto>>>> GetPage(
[FromQuery] int pageNumber, [FromQuery] int pageNumber,
[FromQuery] int pageSize, [FromQuery] int pageSize,
[FromQuery] string? sortColumn = null, [FromQuery] string? sortColumn = null,
[FromQuery] bool sortDescending = false) [FromQuery] bool sortDescending = false)
{ {
var result = await _trackService.GetPaged(pageNumber, pageSize, sortColumn, sortDescending); var result = await _trackService.GetPaged(pageNumber, pageSize, sortColumn, sortDescending);
var apiResult = ApiResult<PagedResult<TrackEntity>>.From(result); var apiResult = ApiResult<PagedResult<TrackDto>>.From(result);
var dto = new ApiResultDto<PagedResult<TrackEntity>>(apiResult); var dto = new ApiResultDto<PagedResult<TrackDto>>(apiResult);
return result.Success ? Ok(dto) : StatusCode(500, dto); return result.Success ? Ok(dto) : StatusCode(500, dto);
} }
@@ -1,5 +1,5 @@
using DeepDrftData; using DeepDrftData;
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services; using DeepDrftPublic.Client.Services;
using Models.Common; using Models.Common;
using NetBlocks.Models; using NetBlocks.Models;
@@ -21,13 +21,13 @@ public class TrackDirectDataService : ITrackDataService
_trackService = trackService; _trackService = trackService;
} }
public async Task<ApiResult<PagedResult<TrackEntity>>> GetPage( public async Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber, int pageNumber,
int pageSize, int pageSize,
string? sortColumn = null, string? sortColumn = null,
bool sortDescending = false) bool sortDescending = false)
{ {
var result = await _trackService.GetPaged(pageNumber, pageSize, sortColumn, sortDescending); var result = await _trackService.GetPaged(pageNumber, pageSize, sortColumn, sortDescending);
return ApiResult<PagedResult<TrackEntity>>.From(result); return ApiResult<PagedResult<TrackDto>>.From(result);
} }
} }
@@ -1,13 +1,13 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
using MudBlazor; using MudBlazor;
namespace DeepDrftShared.Client.Components; namespace DeepDrftShared.Client.Components;
public partial class TrackCard : ComponentBase public partial class TrackCard : ComponentBase
{ {
[Parameter] public required TrackEntity TrackModel { get; set; } [Parameter] public required TrackDto TrackModel { get; set; }
[Parameter] public EventCallback<TrackEntity> OnPlay { get; set; } [Parameter] public EventCallback<TrackDto> OnPlay { get; set; }
[Parameter] public bool IsPlaying { get; set; } = false; [Parameter] public bool IsPlaying { get; set; } = false;
private string PlayPauseIcon => IsPlaying ? Icons.Material.Filled.MusicNote : Icons.Material.Filled.PlayArrow; private string PlayPauseIcon => IsPlaying ? Icons.Material.Filled.MusicNote : Icons.Material.Filled.PlayArrow;
@@ -1,15 +1,15 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DeepDrftModels.Entities; using DeepDrftModels.DTOs;
namespace DeepDrftShared.Client.Components; namespace DeepDrftShared.Client.Components;
public partial class TracksGallery : ComponentBase public partial class TracksGallery : ComponentBase
{ {
[Parameter] public IEnumerable<TrackEntity> Tracks { get; set; } = []; [Parameter] public IEnumerable<TrackDto> Tracks { get; set; } = [];
[Parameter] public TrackEntity? SelectedTrack { get; set; } [Parameter] public TrackDto? SelectedTrack { get; set; }
[Parameter] public EventCallback<TrackEntity?> SelectedTrackChanged { get; set; } [Parameter] public EventCallback<TrackDto?> SelectedTrackChanged { get; set; }
private async Task HandlePlayClick(TrackEntity track) private async Task HandlePlayClick(TrackDto track)
{ {
if (SelectedTrack == track) return; if (SelectedTrack == track) return;
SelectedTrack = track; SelectedTrack = track;