Fix 9 design majors: ITrackService interface, IsDescending, ArrayPool base, CliUtils, sort sentinel cleanup, content controller via TrackService, Skip property

This commit is contained in:
Daniel Harvey
2026-05-17 16:10:56 -04:00
parent fc5b8de81a
commit 4c9bf0ca8d
15 changed files with 97 additions and 71 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ builder.Services.AddSingleton<FileDatabase>(provider =>
// Add services
builder.Services.AddScoped<TrackRepository>();
builder.Services.AddScoped<DeepDrftWeb.Services.TrackService>();
builder.Services.AddScoped<DeepDrftWeb.Services.ITrackService, DeepDrftWeb.Services.TrackService>();
builder.Services.AddScoped<AudioProcessor>();
builder.Services.AddScoped<DeepDrftContent.Services.TrackService>();
builder.Services.AddScoped<CliService>();
+4 -18
View File
@@ -1,9 +1,9 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using DeepDrftWeb.Services.Repositories;
using DeepDrftContent.Services;
using DeepDrftModels.Entities;
using NetBlocks.Models;
using DeepDrftCli.Utils;
namespace DeepDrftCli.Services;
@@ -13,18 +13,15 @@ namespace DeepDrftCli.Services;
public class CliService
{
private readonly ILogger<CliService> _logger;
private readonly TrackRepository _trackRepository;
private readonly DeepDrftWeb.Services.TrackService _webTrackService;
private readonly DeepDrftWeb.Services.ITrackService _webTrackService;
private readonly DeepDrftContent.Services.TrackService _contentTrackService;
public CliService(
ILogger<CliService> logger,
TrackRepository trackRepository,
DeepDrftWeb.Services.TrackService webTrackService,
DeepDrftWeb.Services.ITrackService webTrackService,
DeepDrftContent.Services.TrackService contentTrackService)
{
_logger = logger;
_trackRepository = trackRepository;
_webTrackService = webTrackService;
_contentTrackService = contentTrackService;
}
@@ -232,7 +229,7 @@ public class CliService
foreach (var track in tracks)
{
Console.WriteLine($"{track.Id,-5} {TruncateString(track.TrackName, 25),-25} {TruncateString(track.Artist, 20),-20} {TruncateString(track.Album ?? "", 15),-15} {TruncateString(track.Genre ?? "", 10),-10}");
Console.WriteLine($"{track.Id,-5} {CliUtils.TruncateString(track.TrackName, 25),-25} {CliUtils.TruncateString(track.Artist, 20),-20} {CliUtils.TruncateString(track.Album ?? "", 15),-15} {CliUtils.TruncateString(track.Genre ?? "", 10),-10}");
}
}
catch (Exception ex)
@@ -285,17 +282,6 @@ public class CliService
Console.WriteLine(" - Use * to indicate required fields in interactive mode");
}
/// <summary>
/// Truncates a string to fit within specified length
/// </summary>
private string TruncateString(string input, int maxLength)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "...";
}
/// <summary>
/// Prompts user for track metadata interactively
/// </summary>
+4 -13
View File
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Terminal.Gui;
using DeepDrftModels.Entities;
using DeepDrftCli.Utils;
namespace DeepDrftCli.Services;
@@ -10,7 +11,7 @@ namespace DeepDrftCli.Services;
public class GuiService
{
private readonly ILogger<GuiService> _logger;
private readonly DeepDrftWeb.Services.TrackService _webTrackService;
private readonly DeepDrftWeb.Services.ITrackService _webTrackService;
private readonly DeepDrftContent.Services.TrackService _contentTrackService;
// GUI Components
@@ -23,7 +24,7 @@ public class GuiService
public GuiService(
ILogger<GuiService> logger,
DeepDrftWeb.Services.TrackService webTrackService,
DeepDrftWeb.Services.ITrackService webTrackService,
DeepDrftContent.Services.TrackService contentTrackService)
{
_logger = logger;
@@ -785,7 +786,7 @@ public class GuiService
// Create display items for the list view
var displayItems = _tracks.Select(t =>
$"{t.Id,4} │ {TruncateString(t.TrackName, 25),25} │ {TruncateString(t.Artist, 20),20} │ {TruncateString(t.Album ?? "", 15),15} │ {TruncateString(t.Genre ?? "", 10),10}"
$"{t.Id,4} │ {CliUtils.TruncateString(t.TrackName, 25),25} │ {CliUtils.TruncateString(t.Artist, 20),20} │ {CliUtils.TruncateString(t.Album ?? "", 15),15} │ {CliUtils.TruncateString(t.Genre ?? "", 10),10}"
).ToArray();
_trackListView?.SetSource(displayItems);
@@ -894,14 +895,4 @@ public class GuiService
UpdateStatus("Ready");
}
/// <summary>
/// Truncate string to fit display width
/// </summary>
private string TruncateString(string input, int maxLength)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "...";
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace DeepDrftCli.Utils;
internal static class CliUtils
{
/// <summary>
/// Truncates a string to fit within the specified column width,
/// appending "..." when the string is longer.
/// </summary>
internal static string TruncateString(string input, int maxLength)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "...";
}
}
+14 -4
View File
@@ -1,5 +1,4 @@
using DeepDrftContent.Services.Audio;
using DeepDrftContent.Services.Constants;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Middleware;
using Microsoft.AspNetCore.Mvc;
@@ -10,15 +9,23 @@ namespace DeepDrftContent.Controllers;
[Route("api/[controller]")]
public class TrackController : ControllerBase
{
private readonly DeepDrftContent.Services.FileDatabase.Services.FileDatabase _fileDatabase;
private readonly DeepDrftContent.Services.TrackService _trackService;
private readonly WavOffsetService _wavOffsetService;
private readonly ILogger<TrackController> _logger;
// FileDatabase is injected directly for PutTrack because that endpoint receives a pre-processed
// AudioBinaryDto over the wire, not a WAV file path. TrackService.AddTrackFromWavAsync is
// file-path-oriented and not applicable here. If a file-upload flow is added in future,
// route it through TrackService instead.
private readonly DeepDrftContent.Services.FileDatabase.Services.FileDatabase _fileDatabase;
public TrackController(
DeepDrftContent.Services.TrackService trackService,
DeepDrftContent.Services.FileDatabase.Services.FileDatabase fileDatabase,
WavOffsetService wavOffsetService,
ILogger<TrackController> logger)
{
_trackService = trackService;
_fileDatabase = fileDatabase;
_wavOffsetService = wavOffsetService;
_logger = logger;
@@ -31,7 +38,7 @@ public class TrackController : ControllerBase
try
{
var file = await _fileDatabase.LoadResourceAsync<AudioBinary>(VaultConstants.Tracks, trackId);
var file = await _trackService.GetAudioBinaryAsync(trackId);
if (file == null)
{
_logger.LogWarning("Track not found: {TrackId}", trackId);
@@ -72,7 +79,10 @@ public class TrackController : ControllerBase
{
_logger.LogInformation("PutTrack called with trackId: {TrackId}", trackId);
var audioBinary = AudioBinary.From(track);
var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, trackId, audioBinary);
// Direct FileDatabase write: this endpoint receives an already-processed AudioBinaryDto,
// not a WAV file, so TrackService.AddTrackFromWavAsync does not apply. See constructor comment.
var success = await _fileDatabase.RegisterResourceAsync(
DeepDrftContent.Services.Constants.VaultConstants.Tracks, trackId, audioBinary);
return success ? Ok() : BadRequest("Failed to store audio track");
}
}
+4
View File
@@ -1,7 +1,9 @@
using DeepDrftContent.Services;
using DeepDrftContent.Services.Audio;
using DeepDrftContent.Services.Constants;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Services.Processors;
using DeepDrftContent.Models;
namespace DeepDrftContent
@@ -12,6 +14,8 @@ namespace DeepDrftContent
{
// Audio services
builder.Services.AddSingleton<WavOffsetService>();
builder.Services.AddSingleton<AudioProcessor>();
builder.Services.AddSingleton<TrackService>();
// File Database
builder.Configuration.AddJsonFile("environment/filedatabase.json", optional: false, reloadOnChange: true);
@@ -4,8 +4,6 @@ namespace DeepDrftWeb.Client.Common;
public class DarkModeSettings()
{
// public EventCallback<bool> IsDarkModeChanged { get; set; }
[PersistentState]
public bool IsDarkMode
{
@@ -14,7 +12,6 @@ public class DarkModeSettings()
{
if (value == field) return;
field = value;
// IsDarkModeChanged.InvokeAsync(value);
}
} = false;
}
@@ -225,6 +225,9 @@ public class AudioInteropService : IAsyncDisposable
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.disposePlayer", playerId);
}
// TODO: The typeof(T) switch below requires updating whenever a new result type is added.
// Consider introducing a shared marker interface (e.g. IAudioResult with a static factory
// method) so InvokeJsAsync can construct the failure result generically without a type switch.
private async Task<T> InvokeJsAsync<T>(string identifier, params object[] args)
{
try
@@ -2,6 +2,7 @@ using DeepDrftModels.Entities;
using DeepDrftWeb.Client.Clients;
using Microsoft.AspNetCore.Components;
using NetBlocks.Models;
using System.Buffers;
namespace DeepDrftWeb.Client.Services;
@@ -135,29 +136,25 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
private async Task StreamAudio(TrackMediaResponse audio)
{
const int bufferSize = 32 * 1024;
var rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
const int bufferSize = 32 * 1024;
long totalBytesRead = 0;
int currentBytes;
do
{
var buffer = new byte[bufferSize];
currentBytes = await audio.Stream.ReadAsync(buffer, 0, buffer.Length);
currentBytes = await audio.Stream.ReadAsync(rentedBuffer, 0, bufferSize);
if (currentBytes > 0)
{
totalBytesRead += currentBytes;
if (currentBytes < bufferSize)
{
var trimmedBuffer = new byte[currentBytes];
Array.Copy(buffer, trimmedBuffer, currentBytes);
buffer = trimmedBuffer;
}
// Slice to actual bytes read before sending to interop
var chunk = rentedBuffer[..currentBytes];
var appendResult = await _audioInterop.AppendAudioBlockAsync(PlayerId, buffer);
var appendResult = await _audioInterop.AppendAudioBlockAsync(PlayerId, chunk);
if (!appendResult.Success)
{
throw new Exception($"Failed to append audio block: {appendResult.Error}");
@@ -191,6 +188,10 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
await NotifyStateChanged();
throw;
}
finally
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
public async Task TogglePlayPause()
@@ -53,8 +53,6 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
// Resume AudioContext immediately on track selection (user interaction) to avoid clicks later
await _audioInterop.EnsureAudioContextReady(PlayerId);
// NotifyStateChanged();
await NotifyTrackSelected();
await LoadTrackStreaming(track);
+15
View File
@@ -0,0 +1,15 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using NetBlocks.Models;
namespace DeepDrftWeb.Services;
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);
Task<ResultContainer<TrackEntity>> Create(TrackEntity newTrack);
Task<ResultContainer<TrackEntity>> Update(TrackEntity track);
Task<Result> Delete(long id);
}
@@ -26,11 +26,18 @@ public class TrackRepository
public async Task<PagedResult<TrackEntity>> GetPage(PagingParameters<TrackEntity> pageParameters)
{
// Two separate queries with no transaction: count and page can be momentarily inconsistent
// under concurrent writes. Acceptable — SQLite concurrency is low and the UI is read-only.
// If filtering is added, the count query must be updated to apply the same filter.
var count = await _db.Tracks.CountAsync();
var page = await _db.Tracks
.OrderBy(pageParameters.OrderBy ?? (t => t.Id))
.Skip((pageParameters.Page - 1) * pageParameters.PageSize)
var orderBy = pageParameters.OrderBy ?? (t => t.Id);
var ordered = pageParameters.IsDescending
? _db.Tracks.OrderByDescending(orderBy)
: _db.Tracks.OrderBy(orderBy);
var page = await ordered
.Skip(pageParameters.Skip)
.Take(pageParameters.PageSize)
.ToListAsync();
+3 -5
View File
@@ -6,10 +6,8 @@ using NetBlocks.Models;
namespace DeepDrftWeb.Services;
public class TrackService
public class TrackService : ITrackService
{
private readonly string _sortLastAscending = Enumerable.Repeat(char.MaxValue, 64).Aggregate(string.Empty, (a, b) => a + b);
private readonly string _sortLastDescending = Enumerable.Repeat(char.MinValue.ToString(), 64).Aggregate(string.Empty, (a, b) => a + b);
private readonly TrackRepository _repository;
public TrackService(TrackRepository repository)
@@ -65,13 +63,13 @@ public class TrackService
parameters.OrderBy = entity => entity.Artist;
break;
case "Album":
parameters.OrderBy = entity => entity.Album ?? _sortLastAscending;
parameters.OrderBy = entity => entity.Album ?? "";
break;
case "ReleaseDate":
parameters.OrderBy = entity => entity.ReleaseDate ?? DateOnly.MaxValue;
break;
case "Genre":
parameters.OrderBy = entity => entity.Genre ?? _sortLastAscending;
parameters.OrderBy = entity => entity.Genre ?? "";
break;
}
+2 -2
View File
@@ -10,9 +10,9 @@ namespace DeepDrftWeb.Controllers;
[Route("api/[controller]")]
public class TrackController : ControllerBase
{
private readonly TrackService _trackService;
private readonly ITrackService _trackService;
public TrackController(TrackService trackService)
public TrackController(ITrackService trackService)
{
_trackService = trackService;
}
+1 -1
View File
@@ -22,7 +22,7 @@ public static class Startup
// Add Track services
builder.Services
.AddScoped<TrackRepository>()
.AddScoped<TrackService>();
.AddScoped<ITrackService, TrackService>();
}
public static string GetKestrelUrl(this WebApplicationBuilder builder)