diff --git a/DeepDrftManager/Components/Pages/Index.razor b/DeepDrftManager/Components/Pages/Index.razor index 9dcc89a..3b3a06d 100644 --- a/DeepDrftManager/Components/Pages/Index.razor +++ b/DeepDrftManager/Components/Pages/Index.razor @@ -15,11 +15,5 @@ Href="/tracks"> Tracks - - Waveform Pre-Processing - diff --git a/DeepDrftManager/Components/Pages/Tracks/TrackList.razor b/DeepDrftManager/Components/Pages/Tracks/TrackList.razor index a23f809..fdee66d 100644 --- a/DeepDrftManager/Components/Pages/Tracks/TrackList.razor +++ b/DeepDrftManager/Components/Pages/Tracks/TrackList.razor @@ -1,6 +1,7 @@ @page "/tracks" @using System.Net @using DeepDrftManager.Services +@using DeepDrftModels.DTOs @attribute [Authorize] @inject ICmsTrackService CmsTrackService @inject IDialogService DialogService @@ -10,80 +11,182 @@ Tracks — DeepDrft CMS - - Tracks - - - Waveform Pre-Processing - - - Add Track - - - + Tracks - - - No tracks found. - - - Loading tracks… - - - Track Name - Artist - Album - Genre - Release Date - Entry Key - Actions - - - @context.TrackName - @context.Artist - @(context.Album ?? "—") - @(context.Genre ?? "—") - @(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—") - @context.EntryKey - - - - - - - - - - - - - + + + + + Add Track + + + + + + No tracks found. + + + Loading tracks… + + + Track Name + Artist + Album + Genre + Release Date + Entry Key + Actions + + + @context.TrackName + @context.Artist + @(context.Album ?? "—") + @(context.Genre ?? "—") + @(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—") + @context.EntryKey + + + + + + + + + + + + + + + + + + + Waveform Pre-Processing + + Generate loudness profiles for tracks that predate the waveform seeker. + + + + @if (_bulkRunning) + { + + Generating @_bulkDone / @_bulkTotal… + } + else + { + Generate All Missing (@_missingCount) + } + + + + + + No tracks found. + + + Loading waveform status… + + + Track Name + Entry Key + Profile + Actions + + + @context.TrackName + + @context.EntryKey + + + @if (context.HasProfile) + { + Stored + } + else + { + Missing + } + + + @if (!context.HasProfile) + { + + @if (IsGenerating(context.EntryKey)) + { + + Generating… + } + else + { + Generate + } + + } + + + + + @code { + // Track list fields private MudTable? _table; + // Waveform fields + private List _waveformRows = new(); + private readonly HashSet _generating = new(); + private bool _waveformLoading = true; + private bool _bulkRunning; + private int _bulkTotal; + private int _bulkDone; + + private int _missingCount => _waveformRows.Count(r => !r.HasProfile); + + protected override async Task OnInitializedAsync() + { + await LoadWaveformStatus(); + } + + // ── Track list methods ────────────────────────────────────────────────── + private async Task> LoadServerData(TableState state, CancellationToken cancellationToken) { var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based. @@ -137,4 +240,114 @@ Snackbar.Add("Delete failed — please try again.", Severity.Error); } } + + // ── Waveform pre-processing methods ──────────────────────────────────── + + private async Task LoadWaveformStatus() + { + _waveformLoading = true; + var result = await CmsTrackService.GetWaveformStatusAsync(); + _waveformLoading = false; + + if (!result.Success || result.Value is null) + { + var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + Snackbar.Add($"Failed to load waveform status: {error}", Severity.Error); + _waveformRows = new List(); + return; + } + + _waveformRows = result.Value.OrderBy(r => r.HasProfile).ThenBy(r => r.TrackName).ToList(); + } + + private bool IsGenerating(string entryKey) => _generating.Contains(entryKey); + + private async Task GenerateOne(WaveformStatusDto row) + { + if (!await GenerateForRow(row)) + { + return; + } + + Snackbar.Add($"Generated profile for '{row.TrackName}'.", Severity.Success); + } + + private async Task GenerateAllMissing() + { + var missing = _waveformRows.Where(r => !r.HasProfile).ToList(); + if (missing.Count == 0) + { + return; + } + + _bulkRunning = true; + _bulkTotal = missing.Count; + _bulkDone = 0; + var failures = 0; + + // Sequential by design: one request at a time so a large backfill does not flood the API + // with concurrent WAV decodes. + foreach (var row in missing) + { + if (!await GenerateForRow(row)) + { + failures++; + } + _bulkDone++; + StateHasChanged(); + } + + _bulkRunning = false; + + var succeeded = missing.Count - failures; + if (failures == 0) + { + Snackbar.Add($"Generated {succeeded} profile(s).", Severity.Success); + } + else + { + Snackbar.Add($"Generated {succeeded} profile(s); {failures} failed.", Severity.Warning); + } + } + + /// + /// Runs generation for a single row, flipping its status on success. Returns false on failure + /// (a snackbar is raised here for the per-row path; the bulk path aggregates a summary). Marks + /// the row busy for the duration so its button shows a spinner and stays disabled. + /// + private async Task GenerateForRow(WaveformStatusDto row) + { + _generating.Add(row.EntryKey); + StateHasChanged(); + try + { + var result = await CmsTrackService.GenerateWaveformProfileAsync(row.EntryKey); + if (result.Success) + { + row.HasProfile = true; + return true; + } + + var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + if (!_bulkRunning) + { + Snackbar.Add($"Generate failed for '{row.TrackName}': {error}", Severity.Error); + } + return false; + } + catch (Exception ex) + { + Logger.LogError(ex, "Waveform generation failed for {EntryKey}", row.EntryKey); + if (!_bulkRunning) + { + Snackbar.Add($"Generate failed for '{row.TrackName}' — please try again.", Severity.Error); + } + return false; + } + finally + { + _generating.Remove(row.EntryKey); + StateHasChanged(); + } + } } diff --git a/DeepDrftManager/Components/Pages/Tracks/TrackPreProcessing.razor b/DeepDrftManager/Components/Pages/Tracks/TrackPreProcessing.razor deleted file mode 100644 index 6cbe582..0000000 --- a/DeepDrftManager/Components/Pages/Tracks/TrackPreProcessing.razor +++ /dev/null @@ -1,91 +0,0 @@ -@page "/tracks/preprocessing" -@attribute [Authorize] - -Waveform Pre-Processing — DeepDrft CMS - - - - - Waveform Pre-Processing - - Generate loudness profiles for tracks that predate the waveform seeker. - - - - @if (_bulkRunning) - { - - Generating @_bulkDone / @_bulkTotal… - } - else - { - Generate All Missing (@_missingCount) - } - - - - - - No tracks found. - - - Loading waveform status… - - - Track Name - Entry Key - Profile - Actions - - - @context.TrackName - - @context.EntryKey - - - @if (context.HasProfile) - { - Stored - } - else - { - Missing - } - - - @if (!context.HasProfile) - { - - @if (IsGenerating(context.EntryKey)) - { - - Generating… - } - else - { - Generate - } - - } - - - - diff --git a/DeepDrftManager/Components/Pages/Tracks/TrackPreProcessing.razor.cs b/DeepDrftManager/Components/Pages/Tracks/TrackPreProcessing.razor.cs deleted file mode 100644 index 41da1c8..0000000 --- a/DeepDrftManager/Components/Pages/Tracks/TrackPreProcessing.razor.cs +++ /dev/null @@ -1,135 +0,0 @@ -using DeepDrftManager.Services; -using DeepDrftModels.DTOs; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace DeepDrftManager.Components.Pages.Tracks; - -public partial class TrackPreProcessing : ComponentBase -{ - private List _rows = new(); - private readonly HashSet _generating = new(); - private bool _loading = true; - private bool _bulkRunning; - private int _bulkTotal; - private int _bulkDone; - - private int _missingCount => _rows.Count(r => !r.HasProfile); - - [Inject] private ICmsTrackService CmsTrackService { get; set; } = default!; - [Inject] private ISnackbar Snackbar { get; set; } = default!; - [Inject] private ILogger Logger { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - await LoadStatus(); - } - - private async Task LoadStatus() - { - _loading = true; - var result = await CmsTrackService.GetWaveformStatusAsync(); - _loading = false; - - if (!result.Success || result.Value is null) - { - var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - Snackbar.Add($"Failed to load waveform status: {error}", Severity.Error); - _rows = new List(); - return; - } - - _rows = result.Value.OrderBy(r => r.HasProfile).ThenBy(r => r.TrackName).ToList(); - } - - private bool IsGenerating(string entryKey) => _generating.Contains(entryKey); - - private async Task GenerateOne(WaveformStatusDto row) - { - if (!await GenerateForRow(row)) - { - return; - } - - Snackbar.Add($"Generated profile for '{row.TrackName}'.", Severity.Success); - } - - private async Task GenerateAllMissing() - { - var missing = _rows.Where(r => !r.HasProfile).ToList(); - if (missing.Count == 0) - { - return; - } - - _bulkRunning = true; - _bulkTotal = missing.Count; - _bulkDone = 0; - var failures = 0; - - // Sequential by design: one request at a time so a large backfill does not flood the API - // with concurrent WAV decodes. - foreach (var row in missing) - { - if (!await GenerateForRow(row)) - { - failures++; - } - _bulkDone++; - StateHasChanged(); - } - - _bulkRunning = false; - - var succeeded = missing.Count - failures; - if (failures == 0) - { - Snackbar.Add($"Generated {succeeded} profile(s).", Severity.Success); - } - else - { - Snackbar.Add($"Generated {succeeded} profile(s); {failures} failed.", Severity.Warning); - } - } - - /// - /// Runs generation for a single row, flipping its status on success. Returns false on failure - /// (a snackbar is raised here for the per-row path; the bulk path aggregates a summary). Marks - /// the row busy for the duration so its button shows a spinner and stays disabled. - /// - private async Task GenerateForRow(WaveformStatusDto row) - { - _generating.Add(row.EntryKey); - StateHasChanged(); - try - { - var result = await CmsTrackService.GenerateWaveformProfileAsync(row.EntryKey); - if (result.Success) - { - row.HasProfile = true; - return true; - } - - var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - if (!_bulkRunning) - { - Snackbar.Add($"Generate failed for '{row.TrackName}': {error}", Severity.Error); - } - return false; - } - catch (Exception ex) - { - Logger.LogError(ex, "Waveform generation failed for {EntryKey}", row.EntryKey); - if (!_bulkRunning) - { - Snackbar.Add($"Generate failed for '{row.TrackName}' — please try again.", Severity.Error); - } - return false; - } - finally - { - _generating.Remove(row.EntryKey); - StateHasChanged(); - } - } -}