From 4c9bf0ca8d50a273d2e9f9bf75abcc15807f4d87 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Sun, 17 May 2026 16:10:56 -0400 Subject: [PATCH] Fix 9 design majors: ITrackService interface, IsDescending, ArrayPool base, CliUtils, sort sentinel cleanup, content controller via TrackService, Skip property --- DeepDrftCli/Program.cs | 2 +- DeepDrftCli/Services/CliService.cs | 22 +++--------- DeepDrftCli/Services/GuiService.cs | 19 +++------- DeepDrftCli/Utils/CliUtils.cs | 16 +++++++++ .../Controllers/TrackController.cs | 18 +++++++--- DeepDrftContent/Startup.cs | 4 +++ DeepDrftWeb.Client/Common/DarkModeSettings.cs | 3 -- .../Services/AudioInteropService.cs | 3 ++ .../Services/AudioPlayerService.cs | 35 ++++++++++--------- .../Services/StreamingAudioPlayerService.cs | 2 -- DeepDrftWeb.Services/ITrackService.cs | 15 ++++++++ .../Repositories/TrackRepository.cs | 15 +++++--- DeepDrftWeb.Services/TrackService.cs | 8 ++--- DeepDrftWeb/Controllers/TrackController.cs | 4 +-- DeepDrftWeb/Startup.cs | 2 +- 15 files changed, 97 insertions(+), 71 deletions(-) create mode 100644 DeepDrftCli/Utils/CliUtils.cs create mode 100644 DeepDrftWeb.Services/ITrackService.cs diff --git a/DeepDrftCli/Program.cs b/DeepDrftCli/Program.cs index 31634ae..117b010 100644 --- a/DeepDrftCli/Program.cs +++ b/DeepDrftCli/Program.cs @@ -47,7 +47,7 @@ builder.Services.AddSingleton(provider => // Add services builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/DeepDrftCli/Services/CliService.cs b/DeepDrftCli/Services/CliService.cs index 5c37f6b..ae26961 100644 --- a/DeepDrftCli/Services/CliService.cs +++ b/DeepDrftCli/Services/CliService.cs @@ -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 _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 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"); } - /// - /// Truncates a string to fit within specified length - /// - private string TruncateString(string input, int maxLength) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "..."; - } - /// /// Prompts user for track metadata interactively /// diff --git a/DeepDrftCli/Services/GuiService.cs b/DeepDrftCli/Services/GuiService.cs index 233d6ef..762a094 100644 --- a/DeepDrftCli/Services/GuiService.cs +++ b/DeepDrftCli/Services/GuiService.cs @@ -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 _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 logger, - DeepDrftWeb.Services.TrackService webTrackService, + DeepDrftWeb.Services.ITrackService webTrackService, DeepDrftContent.Services.TrackService contentTrackService) { _logger = logger; @@ -784,8 +785,8 @@ public class GuiService _tracks = result.Value.ToList(); // 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}" + var displayItems = _tracks.Select(t => + $"{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"); } - /// - /// Truncate string to fit display width - /// - private string TruncateString(string input, int maxLength) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "..."; - } } \ No newline at end of file diff --git a/DeepDrftCli/Utils/CliUtils.cs b/DeepDrftCli/Utils/CliUtils.cs new file mode 100644 index 0000000..d71d20f --- /dev/null +++ b/DeepDrftCli/Utils/CliUtils.cs @@ -0,0 +1,16 @@ +namespace DeepDrftCli.Utils; + +internal static class CliUtils +{ + /// + /// Truncates a string to fit within the specified column width, + /// appending "..." when the string is longer. + /// + 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) + "..."; + } +} diff --git a/DeepDrftContent/Controllers/TrackController.cs b/DeepDrftContent/Controllers/TrackController.cs index 7a2f838..d26fc00 100644 --- a/DeepDrftContent/Controllers/TrackController.cs +++ b/DeepDrftContent/Controllers/TrackController.cs @@ -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 _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 logger) { + _trackService = trackService; _fileDatabase = fileDatabase; _wavOffsetService = wavOffsetService; _logger = logger; @@ -31,7 +38,7 @@ public class TrackController : ControllerBase try { - var file = await _fileDatabase.LoadResourceAsync(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"); } } \ No newline at end of file diff --git a/DeepDrftContent/Startup.cs b/DeepDrftContent/Startup.cs index cb5677d..8c0b601 100644 --- a/DeepDrftContent/Startup.cs +++ b/DeepDrftContent/Startup.cs @@ -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(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // File Database builder.Configuration.AddJsonFile("environment/filedatabase.json", optional: false, reloadOnChange: true); diff --git a/DeepDrftWeb.Client/Common/DarkModeSettings.cs b/DeepDrftWeb.Client/Common/DarkModeSettings.cs index 4c0c0d1..8de7563 100644 --- a/DeepDrftWeb.Client/Common/DarkModeSettings.cs +++ b/DeepDrftWeb.Client/Common/DarkModeSettings.cs @@ -4,8 +4,6 @@ namespace DeepDrftWeb.Client.Common; public class DarkModeSettings() { - // public EventCallback IsDarkModeChanged { get; set; } - [PersistentState] public bool IsDarkMode { @@ -14,7 +12,6 @@ public class DarkModeSettings() { if (value == field) return; field = value; - // IsDarkModeChanged.InvokeAsync(value); } } = false; } \ No newline at end of file diff --git a/DeepDrftWeb.Client/Services/AudioInteropService.cs b/DeepDrftWeb.Client/Services/AudioInteropService.cs index 8b85204..9ccd872 100644 --- a/DeepDrftWeb.Client/Services/AudioInteropService.cs +++ b/DeepDrftWeb.Client/Services/AudioInteropService.cs @@ -225,6 +225,9 @@ public class AudioInteropService : IAsyncDisposable return await InvokeJsAsync("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 InvokeJsAsync(string identifier, params object[] args) { try diff --git a/DeepDrftWeb.Client/Services/AudioPlayerService.cs b/DeepDrftWeb.Client/Services/AudioPlayerService.cs index 8915d0e..fa8b741 100644 --- a/DeepDrftWeb.Client/Services/AudioPlayerService.cs +++ b/DeepDrftWeb.Client/Services/AudioPlayerService.cs @@ -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,34 +136,30 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable private async Task StreamAudio(TrackMediaResponse audio) { + const int bufferSize = 32 * 1024; + var rentedBuffer = ArrayPool.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; - } - - var appendResult = await _audioInterop.AppendAudioBlockAsync(PlayerId, buffer); + + // Slice to actual bytes read before sending to interop + var chunk = rentedBuffer[..currentBytes]; + + var appendResult = await _audioInterop.AppendAudioBlockAsync(PlayerId, chunk); if (!appendResult.Success) { throw new Exception($"Failed to append audio block: {appendResult.Error}"); } - + if (audio.ContentLength > 0) { LoadProgress = Math.Min(1.0, (double)totalBytesRead / audio.ContentLength); @@ -170,13 +167,13 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable } } } while (currentBytes > 0); - + var finalizeResult = await _audioInterop.FinalizeAudioBufferAsync(PlayerId); if (!finalizeResult.Success) { throw new Exception($"Failed to finalize audio buffer: {finalizeResult.Error}"); } - + Duration = finalizeResult.Duration; LoadProgress = 1.0; IsLoaded = true; @@ -191,6 +188,10 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable await NotifyStateChanged(); throw; } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } } public async Task TogglePlayPause() diff --git a/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs b/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs index 707854e..132a081 100644 --- a/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs +++ b/DeepDrftWeb.Client/Services/StreamingAudioPlayerService.cs @@ -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); diff --git a/DeepDrftWeb.Services/ITrackService.cs b/DeepDrftWeb.Services/ITrackService.cs new file mode 100644 index 0000000..93184a5 --- /dev/null +++ b/DeepDrftWeb.Services/ITrackService.cs @@ -0,0 +1,15 @@ +using DeepDrftModels.Entities; +using DeepDrftModels.Models; +using NetBlocks.Models; + +namespace DeepDrftWeb.Services; + +public interface ITrackService +{ + Task> GetById(long id); + Task>> GetAll(); + Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending); + Task> Create(TrackEntity newTrack); + Task> Update(TrackEntity track); + Task Delete(long id); +} diff --git a/DeepDrftWeb.Services/Repositories/TrackRepository.cs b/DeepDrftWeb.Services/Repositories/TrackRepository.cs index 44ac968..69972a5 100644 --- a/DeepDrftWeb.Services/Repositories/TrackRepository.cs +++ b/DeepDrftWeb.Services/Repositories/TrackRepository.cs @@ -26,11 +26,18 @@ public class TrackRepository public async Task> GetPage(PagingParameters 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(); diff --git a/DeepDrftWeb.Services/TrackService.cs b/DeepDrftWeb.Services/TrackService.cs index b2660d0..fe6015d 100644 --- a/DeepDrftWeb.Services/TrackService.cs +++ b/DeepDrftWeb.Services/TrackService.cs @@ -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; } diff --git a/DeepDrftWeb/Controllers/TrackController.cs b/DeepDrftWeb/Controllers/TrackController.cs index 30a305f..1bbe995 100644 --- a/DeepDrftWeb/Controllers/TrackController.cs +++ b/DeepDrftWeb/Controllers/TrackController.cs @@ -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; } diff --git a/DeepDrftWeb/Startup.cs b/DeepDrftWeb/Startup.cs index 20b8649..f762135 100644 --- a/DeepDrftWeb/Startup.cs +++ b/DeepDrftWeb/Startup.cs @@ -22,7 +22,7 @@ public static class Startup // Add Track services builder.Services .AddScoped() - .AddScoped(); + .AddScoped(); } public static string GetKestrelUrl(this WebApplicationBuilder builder)