@page "/tracks" @using System.Net @using DeepDrftManager.Services @using DeepDrftModels.DTOs @attribute [Authorize] @inject ICmsTrackService CmsTrackService @inject IDialogService DialogService @inject ISnackbar Snackbar @inject ILogger Logger Tracks — DeepDrft CMS Tracks 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. var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel; var sortDescending = state.SortDirection == SortDirection.Descending; var result = await CmsTrackService.GetPagedAsync(pageNumber, state.PageSize, sortColumn, sortDescending, cancellationToken); if (!result.Success || result.Value is null) { var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error); return new TableData { Items = Array.Empty(), TotalItems = 0 }; } var page = result.Value; return new TableData { Items = page.Items, TotalItems = page.TotalCount }; } private async Task ConfirmAndDelete(TrackDto track) { var confirmed = await DialogService.ShowMessageBox( title: "Delete track", markupMessage: new MarkupString($"Delete {WebUtility.HtmlEncode(track.TrackName)} by {WebUtility.HtmlEncode(track.Artist)}? This removes both the metadata row and the underlying audio entry."), yesText: "Delete", cancelText: "Cancel"); if (confirmed != true) return; try { var result = await CmsTrackService.DeleteTrackAsync(track.Id); if (result.Success) { Snackbar.Add($"Deleted '{track.TrackName}'.", Severity.Success); if (_table is not null) await _table.ReloadServerData(); } else { var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Delete failed: {error}", Severity.Error); } } catch (Exception ex) { Logger.LogError(ex, "Delete failed for track {TrackId}", track.Id); 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(); } } }