Fix 9 design majors: ITrackService interface, IsDescending, ArrayPool base, CliUtils, sort sentinel cleanup, content controller via TrackService, Skip property
This commit is contained in:
@@ -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>();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/// <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) + "...";
|
||||
}
|
||||
}
|
||||
@@ -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) + "...";
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,34 +136,30 @@ 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;
|
||||
}
|
||||
|
||||
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<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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user