Merge branch 'merge-preprocessing-tab' into dev
This commit is contained in:
@@ -15,11 +15,5 @@
|
|||||||
Href="/tracks">
|
Href="/tracks">
|
||||||
Tracks
|
Tracks
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
|
||||||
Href="/tracks/preprocessing">
|
|
||||||
Waveform Pre-Processing
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/tracks"
|
@page "/tracks"
|
||||||
@using System.Net
|
@using System.Net
|
||||||
@using DeepDrftManager.Services
|
@using DeepDrftManager.Services
|
||||||
|
@using DeepDrftModels.DTOs
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject ICmsTrackService CmsTrackService
|
@inject ICmsTrackService CmsTrackService
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
@@ -10,80 +11,182 @@
|
|||||||
<PageTitle>Tracks — DeepDrft CMS</PageTitle>
|
<PageTitle>Tracks — DeepDrft CMS</PageTitle>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
|
<MudText Typo="Typo.h3" Class="mb-4">Tracks</MudText>
|
||||||
<MudText Typo="Typo.h3">Tracks</MudText>
|
|
||||||
<MudStack Row="true" Spacing="2">
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
|
||||||
Href="/tracks/preprocessing">
|
|
||||||
Waveform Pre-Processing
|
|
||||||
</MudButton>
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Href="/tracks/new">
|
|
||||||
Add Track
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<MudTable T="TrackDto"
|
<MudTabs Elevation="0" Rounded="false" ApplyEffectsToContainer="true" PanelClass="pt-4">
|
||||||
@ref="_table"
|
<MudTabPanel Text="Tracks" Icon="@Icons.Material.Filled.LibraryMusic">
|
||||||
ServerData="LoadServerData"
|
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-2">
|
||||||
Hover="true"
|
<MudButton Variant="Variant.Filled"
|
||||||
Striped="true"
|
Color="Color.Primary"
|
||||||
Dense="true"
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
Bordered="false"
|
Href="/tracks/new">
|
||||||
FixedHeader="true"
|
Add Track
|
||||||
RowsPerPage="20"
|
</MudButton>
|
||||||
AllowUnsorted="false">
|
</MudStack>
|
||||||
<NoRecordsContent>
|
|
||||||
<MudText Typo="Typo.body1">No tracks found.</MudText>
|
<MudTable T="TrackDto"
|
||||||
</NoRecordsContent>
|
@ref="_table"
|
||||||
<LoadingContent>
|
ServerData="LoadServerData"
|
||||||
<MudText Typo="Typo.body1">Loading tracks…</MudText>
|
Hover="true"
|
||||||
</LoadingContent>
|
Striped="true"
|
||||||
<HeaderContent>
|
Dense="true"
|
||||||
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackDto" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
|
Bordered="false"
|
||||||
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackDto">Artist</MudTableSortLabel></MudTh>
|
FixedHeader="true"
|
||||||
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackDto">Album</MudTableSortLabel></MudTh>
|
RowsPerPage="20"
|
||||||
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh>
|
AllowUnsorted="false">
|
||||||
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh>
|
<NoRecordsContent>
|
||||||
<MudTh>Entry Key</MudTh>
|
<MudText Typo="Typo.body1">No tracks found.</MudText>
|
||||||
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
</NoRecordsContent>
|
||||||
</HeaderContent>
|
<LoadingContent>
|
||||||
<RowTemplate>
|
<MudText Typo="Typo.body1">Loading tracks…</MudText>
|
||||||
<MudTd DataLabel="Track Name">@context.TrackName</MudTd>
|
</LoadingContent>
|
||||||
<MudTd DataLabel="Artist">@context.Artist</MudTd>
|
<HeaderContent>
|
||||||
<MudTd DataLabel="Album">@(context.Album ?? "—")</MudTd>
|
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackDto" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
|
||||||
<MudTd DataLabel="Genre">@(context.Genre ?? "—")</MudTd>
|
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackDto">Artist</MudTableSortLabel></MudTh>
|
||||||
<MudTd DataLabel="Release Date">@(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—")</MudTd>
|
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackDto">Album</MudTableSortLabel></MudTh>
|
||||||
<MudTd DataLabel="Entry Key"><MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText></MudTd>
|
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh>
|
||||||
<MudTd DataLabel="Actions">
|
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh>
|
||||||
<MudTooltip Text="Edit">
|
<MudTh>Entry Key</MudTh>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
||||||
Size="Size.Small"
|
</HeaderContent>
|
||||||
Color="Color.Primary"
|
<RowTemplate>
|
||||||
Href="@($"/tracks/{context.Id}")" />
|
<MudTd DataLabel="Track Name">@context.TrackName</MudTd>
|
||||||
</MudTooltip>
|
<MudTd DataLabel="Artist">@context.Artist</MudTd>
|
||||||
<MudTooltip Text="Delete">
|
<MudTd DataLabel="Album">@(context.Album ?? "—")</MudTd>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
<MudTd DataLabel="Genre">@(context.Genre ?? "—")</MudTd>
|
||||||
Size="Size.Small"
|
<MudTd DataLabel="Release Date">@(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—")</MudTd>
|
||||||
Color="Color.Error"
|
<MudTd DataLabel="Entry Key"><MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText></MudTd>
|
||||||
OnClick="@(() => ConfirmAndDelete(context))" />
|
<MudTd DataLabel="Actions">
|
||||||
</MudTooltip>
|
<MudTooltip Text="Edit">
|
||||||
</MudTd>
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
</RowTemplate>
|
Size="Size.Small"
|
||||||
<PagerContent>
|
Color="Color.Primary"
|
||||||
<MudTablePager PageSizeOptions="new[] { 10, 20, 50 }" />
|
Href="@($"/tracks/{context.Id}")" />
|
||||||
</PagerContent>
|
</MudTooltip>
|
||||||
</MudTable>
|
<MudTooltip Text="Delete">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Error"
|
||||||
|
OnClick="@(() => ConfirmAndDelete(context))" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<PagerContent>
|
||||||
|
<MudTablePager PageSizeOptions="new[] { 10, 20, 50 }" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudTable>
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
<MudTabPanel Text="Waveform Pre-Processing" Icon="@Icons.Material.Filled.GraphicEq">
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
|
||||||
|
<MudStack Spacing="0">
|
||||||
|
<MudText Typo="Typo.h5">Waveform Pre-Processing</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
|
Generate loudness profiles for tracks that predate the waveform seeker.
|
||||||
|
</MudText>
|
||||||
|
</MudStack>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.AutoFixHigh"
|
||||||
|
Disabled="@(_bulkRunning || _missingCount == 0)"
|
||||||
|
OnClick="GenerateAllMissing">
|
||||||
|
@if (_bulkRunning)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||||
|
<span>Generating @_bulkDone / @_bulkTotal…</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Generate All Missing (@_missingCount)</span>
|
||||||
|
}
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudTable T="WaveformStatusDto"
|
||||||
|
Items="_waveformRows"
|
||||||
|
Loading="_waveformLoading"
|
||||||
|
Hover="true"
|
||||||
|
Striped="true"
|
||||||
|
Dense="true"
|
||||||
|
Bordered="false"
|
||||||
|
FixedHeader="true">
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.body1">No tracks found.</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<LoadingContent>
|
||||||
|
<MudText Typo="Typo.body1">Loading waveform status…</MudText>
|
||||||
|
</LoadingContent>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Track Name</MudTh>
|
||||||
|
<MudTh>Entry Key</MudTh>
|
||||||
|
<MudTh Style="width: 1%; white-space: nowrap;">Profile</MudTh>
|
||||||
|
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Track Name">@context.TrackName</MudTd>
|
||||||
|
<MudTd DataLabel="Entry Key">
|
||||||
|
<MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Profile">
|
||||||
|
@if (context.HasProfile)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Text"
|
||||||
|
Icon="@Icons.Material.Filled.CheckCircle">Stored</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Variant="Variant.Text"
|
||||||
|
Icon="@Icons.Material.Filled.Cancel">Missing</MudChip>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Actions">
|
||||||
|
@if (!context.HasProfile)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.GraphicEq"
|
||||||
|
Disabled="@(_bulkRunning || IsGenerating(context.EntryKey))"
|
||||||
|
OnClick="@(() => GenerateOne(context))">
|
||||||
|
@if (IsGenerating(context.EntryKey))
|
||||||
|
{
|
||||||
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||||
|
<span>Generating…</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Generate</span>
|
||||||
|
}
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
// Track list fields
|
||||||
private MudTable<TrackDto>? _table;
|
private MudTable<TrackDto>? _table;
|
||||||
|
|
||||||
|
// Waveform fields
|
||||||
|
private List<WaveformStatusDto> _waveformRows = new();
|
||||||
|
private readonly HashSet<string> _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<TableData<TrackDto>> LoadServerData(TableState state, CancellationToken cancellationToken)
|
private async Task<TableData<TrackDto>> LoadServerData(TableState state, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based.
|
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);
|
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<WaveformStatusDto>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
@page "/tracks/preprocessing"
|
|
||||||
@attribute [Authorize]
|
|
||||||
|
|
||||||
<PageTitle>Waveform Pre-Processing — DeepDrft CMS</PageTitle>
|
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
|
|
||||||
<MudStack Spacing="0">
|
|
||||||
<MudText Typo="Typo.h3">Waveform Pre-Processing</MudText>
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
|
||||||
Generate loudness profiles for tracks that predate the waveform seeker.
|
|
||||||
</MudText>
|
|
||||||
</MudStack>
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.AutoFixHigh"
|
|
||||||
Disabled="@(_bulkRunning || _missingCount == 0)"
|
|
||||||
OnClick="GenerateAllMissing">
|
|
||||||
@if (_bulkRunning)
|
|
||||||
{
|
|
||||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
|
||||||
<span>Generating @_bulkDone / @_bulkTotal…</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>Generate All Missing (@_missingCount)</span>
|
|
||||||
}
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<MudTable T="WaveformStatusDto"
|
|
||||||
Items="_rows"
|
|
||||||
Loading="_loading"
|
|
||||||
Hover="true"
|
|
||||||
Striped="true"
|
|
||||||
Dense="true"
|
|
||||||
Bordered="false"
|
|
||||||
FixedHeader="true">
|
|
||||||
<NoRecordsContent>
|
|
||||||
<MudText Typo="Typo.body1">No tracks found.</MudText>
|
|
||||||
</NoRecordsContent>
|
|
||||||
<LoadingContent>
|
|
||||||
<MudText Typo="Typo.body1">Loading waveform status…</MudText>
|
|
||||||
</LoadingContent>
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Track Name</MudTh>
|
|
||||||
<MudTh>Entry Key</MudTh>
|
|
||||||
<MudTh Style="width: 1%; white-space: nowrap;">Profile</MudTh>
|
|
||||||
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd DataLabel="Track Name">@context.TrackName</MudTd>
|
|
||||||
<MudTd DataLabel="Entry Key">
|
|
||||||
<MudText Typo="Typo.caption" Style="font-family: monospace;">@context.EntryKey</MudText>
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Profile">
|
|
||||||
@if (context.HasProfile)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Text"
|
|
||||||
Icon="@Icons.Material.Filled.CheckCircle">Stored</MudChip>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Variant="Variant.Text"
|
|
||||||
Icon="@Icons.Material.Filled.Cancel">Missing</MudChip>
|
|
||||||
}
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Actions">
|
|
||||||
@if (!context.HasProfile)
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Size="Size.Small"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
|
||||||
Disabled="@(_bulkRunning || IsGenerating(context.EntryKey))"
|
|
||||||
OnClick="@(() => GenerateOne(context))">
|
|
||||||
@if (IsGenerating(context.EntryKey))
|
|
||||||
{
|
|
||||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
|
||||||
<span>Generating…</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>Generate</span>
|
|
||||||
}
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
</MudTable>
|
|
||||||
</MudContainer>
|
|
||||||
@@ -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<WaveformStatusDto> _rows = new();
|
|
||||||
private readonly HashSet<string> _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<TrackPreProcessing> 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<WaveformStatusDto>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user