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);
}
// 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
// touches the vault disk path or SQL directly. UnifiedTrackService owns the two-database write.
//
@@ -80,7 +80,7 @@ public class TrackController : ControllerBase
[HttpPost("upload")]
[RequestSizeLimit(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] string? trackName,
[FromForm] string? artist,
+6 -6
View File
@@ -1,7 +1,7 @@
using DeepDrftContent;
using DeepDrftContent.Constants;
using DeepDrftData;
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using NetBlocks.Models;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
@@ -34,10 +34,10 @@ public class UnifiedTrackService
/// <summary>
/// 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.
/// </summary>
public async Task<ResultContainer<TrackEntity>> UploadAsync(
public async Task<ResultContainer<TrackDto>> UploadAsync(
string tempFilePath,
string trackName,
string artist,
@@ -53,12 +53,12 @@ public class UnifiedTrackService
if (unpersisted is null)
{
_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;
var saveResult = await _sqlTrackService.Create(unpersisted);
var saveResult = await _sqlTrackService.Create(TrackConverter.Convert(unpersisted));
if (!saveResult.Success || saveResult.Value is null)
{
// Vault write succeeded, SQL persist failed — audio is orphaned in the tracks vault
@@ -67,7 +67,7 @@ public class UnifiedTrackService
_logger.LogError(
"Track persisted to vault but SQL save failed. Orphaned entry: {EntryKey}. Error: {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;
+2 -2
View File
@@ -164,8 +164,8 @@ public class CliService
return;
}
// Add track to SQL database
var result = await _webTrackService.Create(trackEntity);
// Add track to SQL database (service layer is DTO-typed)
var result = await _webTrackService.Create(DeepDrftData.TrackConverter.Convert(trackEntity));
if (result.Success && result.Value != null)
{
Console.WriteLine($"✓ Track added successfully!");
+8 -7
View File
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Terminal.Gui;
using DeepDrftModels.Entities;
using DeepDrftData;
using DeepDrftModels.DTOs;
using DeepDrftCli.Utils;
namespace DeepDrftCli.Services;
@@ -20,7 +21,7 @@ public class GuiService
private ListView? _trackListView;
private TextView? _statusView;
private FrameView? _legendFrame;
private List<TrackEntity> _tracks = new();
private List<TrackDto> _tracks = new();
public GuiService(
ILogger<GuiService> logger,
@@ -541,7 +542,7 @@ public class GuiService
/// <summary>
/// Delete the specified track from the database
/// </summary>
private async Task DeleteTrackAsync(TrackEntity trackToDelete)
private async Task DeleteTrackAsync(TrackDto trackToDelete)
{
try
{
@@ -650,8 +651,8 @@ public class GuiService
return false;
}
// Add to SQL database
var result = await _webTrackService.Create(trackEntity);
// Add to SQL database (service layer is DTO-typed)
var result = await _webTrackService.Create(TrackConverter.Convert(trackEntity));
if (result.Success && result.Value != null)
{
UpdateStatus($"✓ Track '{trackName}' by {artist} added successfully!");
@@ -676,7 +677,7 @@ public class GuiService
/// <summary>
/// Validate input and update existing track in database
/// </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)
{
try
@@ -709,7 +710,7 @@ public class GuiService
UpdateStatus("Updating track...");
// Create updated track entity
var updatedTrack = new TrackEntity
var updatedTrack = new TrackDto
{
Id = originalTrack.Id,
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 NetBlocks.Models;
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
{
Task<ResultContainer<TrackEntity?>> GetById(long id);
Task<ResultContainer<List<TrackEntity>>> GetAll();
Task<ResultContainer<PagedResult<TrackEntity>>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default);
Task<ResultContainer<TrackEntity>> Create(TrackEntity newTrack);
Task<ResultContainer<TrackEntity>> Update(TrackEntity track);
Task<ResultContainer<TrackDto?>> GetById(long id);
Task<ResultContainer<List<TrackDto>>> GetAll();
Task<ResultContainer<PagedResult<TrackDto>>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default);
Task<ResultContainer<TrackDto>> Create(TrackDto newTrack);
Task<ResultContainer<TrackDto>> Update(TrackDto track);
Task<Result> Delete(long id);
}
+36 -42
View File
@@ -9,14 +9,15 @@ using NetBlocks.Models;
namespace DeepDrftData;
/// <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 entity-shaped ITrackService surface retained for backward compatibility with the
/// web host, CMS, and CLI — all existing controllers and pages inject ITrackService and
/// expect TrackEntity-typed results. The two GetById overloads conflict on signature, so
/// ITrackService.GetById is implemented explicitly; the base Manager.Delete satisfies
/// both interfaces because the signatures align.
/// The base Manager&lt;&gt; surface does not line up with ITrackService by signature (base
/// Add vs Create, base Update→Result vs Update→DTO, base Get/GetPage vs GetAll/GetPaged,
/// base GetById→TDto vs GetById→TDto?), so the query and mutation methods are implemented
/// here over Repository + TrackConverter. Only Delete(long)→Result is inherited unchanged.
/// </summary>
public class TrackManager
: Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>, ITrackService
@@ -28,39 +29,38 @@ public class TrackManager
{
}
// --- ITrackService implementation (entity-space; calls Repository directly) ---
// Explicit interface implementation — IManager.GetById returns ResultContainer<TrackDto>
// (inherited from Manager<>), so this entity-typed overload cannot coexist as a public
// 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)
// Explicit impl: base GetById returns ResultContainer<TrackDto> (fails on miss); the
// service contract is ResultContainer<TrackDto?> (pass with null on miss). Return types
// differ, so this cannot be a public overload of the inherited member.
async Task<ResultContainer<TrackDto?>> ITrackService.GetById(long id)
{
try
{
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)
{
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
{
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)
{
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 pageSize,
string? sortColumn,
@@ -86,52 +86,46 @@ public class TrackManager
};
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)
{
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
{
var added = await Repository.AddAsync(newTrack);
return ResultContainer<TrackEntity>.CreatePassResult(added);
var added = await Repository.AddAsync(TrackConverter.Convert(newTrack));
return ResultContainer<TrackDto>.CreatePassResult(TrackConverter.Convert(added));
}
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
// entity-typed contract callers expect and returns the post-update entity for the
// existing CMS edit flow that reads back the persisted values.
/// <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)
// Explicit impl: base Update returns Result; the service contract returns the persisted
// DTO so the CMS edit flow reads back DB-authoritative values.
async Task<ResultContainer<TrackDto>> ITrackService.Update(TrackDto track)
{
try
{
await Repository.UpdateAsync(track);
await Repository.UpdateAsync(TrackConverter.Convert(track));
var updated = await Repository.GetByIdAsync(track.Id);
return updated is not null
? ResultContainer<TrackEntity>.CreatePassResult(updated)
: ResultContainer<TrackEntity>.CreateFailResult("Track not found after update.");
? ResultContainer<TrackDto>.CreatePassResult(TrackConverter.Convert(updated))
: ResultContainer<TrackDto>.CreateFailResult("Track not found after update.");
}
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
// satisfies ITrackService.Delete, and the base implementation performs the soft delete
// via Repository.DeleteAsync. No override needed.
// Delete(long) → Result is inherited from Manager<> and satisfies ITrackService.Delete
// by signature. No override.
}
@@ -90,7 +90,7 @@
@code {
[Parameter] public long Id { get; set; }
private TrackEntity? _track;
private TrackDto? _track;
private TrackEditForm _form = new();
private bool _loading = true;
private bool _busy;
@@ -199,7 +199,7 @@
public string? Genre { get; set; }
public DateTime? ReleaseDate { get; set; }
public static TrackEditForm From(TrackEntity track) => new()
public static TrackEditForm From(TrackDto track) => new()
{
TrackName = track.TrackName,
Artist = track.Artist,
@@ -20,7 +20,7 @@
</MudButton>
</MudStack>
<MudTable T="TrackEntity"
<MudTable T="TrackDto"
@ref="_table"
ServerData="LoadServerData"
Hover="true"
@@ -37,11 +37,11 @@
<MudText Typo="Typo.body1">Loading tracks…</MudText>
</LoadingContent>
<HeaderContent>
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackEntity" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackEntity">Artist</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackEntity">Album</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackEntity">Genre</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackEntity">Release Date</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackDto" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackDto">Artist</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackDto">Album</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh>
<MudTh>Entry Key</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
</HeaderContent>
@@ -74,9 +74,9 @@
</MudContainer>
@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 sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel;
@@ -88,18 +88,18 @@
{
var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown 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;
return new TableData<TrackEntity>
return new TableData<TrackDto>
{
Items = page.Items,
TotalItems = page.TotalCount
};
}
private async Task ConfirmAndDelete(TrackEntity track)
private async Task ConfirmAndDelete(TrackDto track)
{
var confirmed = await DialogService.ShowMessageBox(
title: "Delete track",
+1 -1
View File
@@ -11,7 +11,7 @@
@using AuthBlocksWeb.Components
@using DeepDrftManager
@using DeepDrftManager.Components
@using DeepDrftModels.Entities
@using DeepDrftModels.DTOs
@using Models.Common
@using AuthBlocksModels.SystemDefinitions
@using AuthBlocksWeb.HierarchicalAuthorize
+30 -30
View File
@@ -1,7 +1,7 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using Models.Common;
using NetBlocks.Models;
@@ -30,7 +30,7 @@ public class CmsTrackService : ICmsTrackService
_logger = logger;
}
public async Task<ResultContainer<TrackEntity>> UploadTrackAsync(
public async Task<ResultContainer<TrackDto>> UploadTrackAsync(
Stream wavStream,
string fileName,
string contentType,
@@ -67,7 +67,7 @@ public class CmsTrackService : ICmsTrackService
catch (Exception ex)
{
_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)
@@ -79,35 +79,35 @@ public class CmsTrackService : ICmsTrackService
if (statusCode >= 500)
{
_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.
_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);
}
// The Content API now owns the dual-database write, so the response is the persisted
// entity (Id > 0) — no SQL roundtrip here.
TrackEntity? persisted;
// track DTO (Id > 0) — no SQL roundtrip here.
TrackDto? persisted;
try
{
persisted = await response.Content.ReadFromJsonAsync<TrackEntity>(ct);
persisted = await response.Content.ReadFromJsonAsync<TrackDto>(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deserialize TrackEntity from Content API response");
return ResultContainer<TrackEntity>.CreateFailResult("Content API returned an unexpected response.");
_logger.LogError(ex, "Failed to deserialize TrackDto from Content API response");
return ResultContainer<TrackDto>.CreateFailResult("Content API returned an unexpected response.");
}
if (persisted is null)
{
_logger.LogError("Content API returned a null TrackEntity");
return ResultContainer<TrackEntity>.CreateFailResult("Content API returned an empty response.");
_logger.LogError("Content API returned a null TrackDto");
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,
CancellationToken ct = default)
{
@@ -163,7 +163,7 @@ public class CmsTrackService : ICmsTrackService
catch (Exception ex)
{
_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)
@@ -171,31 +171,31 @@ public class CmsTrackService : ICmsTrackService
if (!response.IsSuccessStatusCode)
{
_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
{
paged = await response.Content.ReadFromJsonAsync<PagedResult<TrackEntity>>(ct);
paged = await response.Content.ReadFromJsonAsync<PagedResult<TrackDto>>(ct);
}
catch (Exception ex)
{
_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)
{
_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);
@@ -207,34 +207,34 @@ public class CmsTrackService : ICmsTrackService
catch (Exception ex)
{
_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)
{
if (response.StatusCode == HttpStatusCode.NotFound)
{
return ResultContainer<TrackEntity?>.CreatePassResult(null);
return ResultContainer<TrackDto?>.CreatePassResult(null);
}
if (!response.IsSuccessStatusCode)
{
_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
{
track = await response.Content.ReadFromJsonAsync<TrackEntity>(ct);
track = await response.Content.ReadFromJsonAsync<TrackDto>(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deserialize TrackEntity from Content API response");
return ResultContainer<TrackEntity?>.CreateFailResult("Content API returned an unexpected response.");
_logger.LogError(ex, "Failed to deserialize TrackDto from Content API 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 NetBlocks.Models;
@@ -13,10 +13,10 @@ public interface ICmsTrackService
{
/// <summary>
/// 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.
/// </summary>
Task<ResultContainer<TrackEntity>> UploadTrackAsync(
Task<ResultContainer<TrackDto>> UploadTrackAsync(
Stream wavStream,
string fileName,
string contentType,
@@ -37,7 +37,7 @@ public interface ICmsTrackService
/// <summary>
/// Fetch a page of track metadata from the Content API's <c>GET api/track/page</c>.
/// </summary>
Task<ResultContainer<PagedResult<TrackEntity>>> GetPagedAsync(
Task<ResultContainer<PagedResult<TrackDto>>> GetPagedAsync(
int page, int pageSize, string? sortColumn, bool sortDescending,
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
/// passing result with a null value.
/// </summary>
Task<ResultContainer<TrackEntity?>> GetByIdAsync(long id, CancellationToken ct = default);
Task<ResultContainer<TrackDto?>> GetByIdAsync(long id, CancellationToken ct = default);
/// <summary>
/// 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 NetBlocks.Models;
using System.Text.Json;
@@ -16,7 +16,7 @@ public class TrackClient
_http = httpClientFactory.CreateClient("DeepDrft.API");
}
public async Task<ApiResult<PagedResult<TrackEntity>>> GetPage(
public async Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber,
int pageSize,
string? sortColumn = null,
@@ -38,11 +38,11 @@ public class TrackClient
var response = await _http.GetAsync($"api/track/page{query}");
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
});
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.ViewModels;
using Microsoft.AspNetCore.Components;
@@ -11,7 +11,7 @@ public partial class TracksView : ComponentBase
[Inject] public required TracksViewModel ViewModel { get; set; }
[CascadingParameter] public required IPlayerService PlayerService { get; set; }
private TrackEntity? _selectedTrack = null;
private TrackDto? _selectedTrack = null;
private int _clickCount = 0;
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);
if (result is { Success: true, Value: PagedResult<TrackEntity> pageResult })
if (result is { Success: true, Value: PagedResult<TrackDto> pageResult })
{
ViewModel.Page = pageResult;
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;
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Clients;
using Microsoft.AspNetCore.Components;
using NetBlocks.Models;
@@ -32,7 +32,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
/// <see cref="SelectTrack"/>/<see cref="Unload"/> path are responsible for managing
/// it themselves.
/// </summary>
public TrackEntity? CurrentTrack { get; protected set; }
public TrackDto? CurrentTrack { get; protected set; }
// Events
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();
@@ -87,7 +87,7 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
await NotifyStateChanged();
}
private async Task LoadTrack(TrackEntity track)
private async Task LoadTrack(TrackDto track)
{
try
{
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Components;
using NetBlocks.Models;
@@ -17,7 +17,7 @@ public interface IPlayerService
double Volume { get; }
double LoadProgress { get; }
string? ErrorMessage { get; }
TrackEntity? CurrentTrack { get; }
TrackDto? CurrentTrack { get; }
// Events for UI updates
EventCallback? OnStateChanged { get; set; }
@@ -25,7 +25,7 @@ public interface IPlayerService
// Control methods
Task InitializeAsync();
Task SelectTrack(TrackEntity track);
Task SelectTrack(TrackDto track);
Task Stop();
Task Unload();
Task TogglePlayPause();
@@ -43,5 +43,5 @@ public interface IStreamingPlayerService : IPlayerService
int BufferedChunks { get; }
// 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 NetBlocks.Models;
@@ -17,7 +17,7 @@ namespace DeepDrftPublic.Client.Services;
/// </summary>
public interface ITrackDataService
{
Task<ApiResult<PagedResult<TrackEntity>>> GetPage(
Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber,
int pageSize,
string? sortColumn = null,
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Clients;
using System.Buffers;
using Microsoft.Extensions.Logging;
@@ -41,12 +41,12 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
_logger = logger;
}
public override async Task SelectTrack(TrackEntity track)
public override async Task SelectTrack(TrackDto track)
{
await SelectTrackStreaming(track);
}
public async Task SelectTrackStreaming(TrackEntity track)
public async Task SelectTrackStreaming(TrackDto track)
{
await EnsureInitializedAsync();
@@ -59,7 +59,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
await NotifyStateChanged();
}
private async Task LoadTrackStreaming(TrackEntity track)
private async Task LoadTrackStreaming(TrackDto track)
{
// Always reset to clean state before loading new track. ResetToIdle
// 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 Models.Common;
using NetBlocks.Models;
@@ -19,7 +19,7 @@ public class TrackClientDataService : ITrackDataService
_trackClient = trackClient;
}
public Task<ApiResult<PagedResult<TrackEntity>>> GetPage(
public Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber,
int pageSize,
string? sortColumn = null,
@@ -1,4 +1,4 @@
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services;
using Models.Common;
@@ -15,7 +15,7 @@ public class TracksViewModel
get => Page?.PageSize ?? 15;
set
{
if (Page == null) throw new Exception();
if (Page == null) return;
if (value != Page.PageSize)
{
Page.PageSize = value;
@@ -24,7 +24,7 @@ public class TracksViewModel
}
public string SortBy { get; set; } = string.Empty;
public bool IsDescending { get; set; } = false;
public PagedResult<TrackEntity>? Page { get; set; } = null;
public PagedResult<TrackDto>? Page { get; set; } = null;
public TracksViewModel(ITrackDataService trackData)
{
@@ -1,5 +1,5 @@
using DeepDrftData;
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Mvc;
using Models.Common;
using NetBlocks.Models;
@@ -18,15 +18,15 @@ public class TrackController : ControllerBase
}
[HttpGet("page")]
public async Task<ActionResult<ApiResultDto<PagedResult<TrackEntity>>>> GetPage(
public async Task<ActionResult<ApiResultDto<PagedResult<TrackDto>>>> GetPage(
[FromQuery] int pageNumber,
[FromQuery] int pageSize,
[FromQuery] string? sortColumn = null,
[FromQuery] bool sortDescending = false)
{
var result = await _trackService.GetPaged(pageNumber, pageSize, sortColumn, sortDescending);
var apiResult = ApiResult<PagedResult<TrackEntity>>.From(result);
var dto = new ApiResultDto<PagedResult<TrackEntity>>(apiResult);
var apiResult = ApiResult<PagedResult<TrackDto>>.From(result);
var dto = new ApiResultDto<PagedResult<TrackDto>>(apiResult);
return result.Success ? Ok(dto) : StatusCode(500, dto);
}
@@ -1,5 +1,5 @@
using DeepDrftData;
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services;
using Models.Common;
using NetBlocks.Models;
@@ -21,13 +21,13 @@ public class TrackDirectDataService : ITrackDataService
_trackService = trackService;
}
public async Task<ApiResult<PagedResult<TrackEntity>>> GetPage(
public async Task<ApiResult<PagedResult<TrackDto>>> GetPage(
int pageNumber,
int pageSize,
string? sortColumn = null,
bool sortDescending = false)
{
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 DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
using MudBlazor;
namespace DeepDrftShared.Client.Components;
public partial class TrackCard : ComponentBase
{
[Parameter] public required TrackEntity TrackModel { get; set; }
[Parameter] public EventCallback<TrackEntity> OnPlay { get; set; }
[Parameter] public required TrackDto TrackModel { get; set; }
[Parameter] public EventCallback<TrackDto> OnPlay { get; set; }
[Parameter] public bool IsPlaying { get; set; } = false;
private string PlayPauseIcon => IsPlaying ? Icons.Material.Filled.MusicNote : Icons.Material.Filled.PlayArrow;
@@ -1,15 +1,15 @@
using Microsoft.AspNetCore.Components;
using DeepDrftModels.Entities;
using DeepDrftModels.DTOs;
namespace DeepDrftShared.Client.Components;
public partial class TracksGallery : ComponentBase
{
[Parameter] public IEnumerable<TrackEntity> Tracks { get; set; } = [];
[Parameter] public TrackEntity? SelectedTrack { get; set; }
[Parameter] public EventCallback<TrackEntity?> SelectedTrackChanged { get; set; }
[Parameter] public IEnumerable<TrackDto> Tracks { get; set; } = [];
[Parameter] public TrackDto? SelectedTrack { 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;
SelectedTrack = track;