Files
deepdrft/DeepDrftManager/Components/Pages/Tracks/Releases.razor
T
daniel-c-harvey 2c1571057a
Deploy DeepDrftAPI / Build, Publish & Bundle (push) Successful in 2m13s
Deploy DeepDrftManager / Build & Publish (push) Successful in 1m23s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 4m4s
Deploy DeepDrftAPI / Deploy (push) Successful in 1m33s
Deploy DeepDrftManager / Deploy (push) Successful in 1m28s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m28s
feature: Manager Menu Styles and Page Titles
2026-06-22 23:04:49 -04:00

295 lines
13 KiB
Plaintext

@page "/releases"
@page "/tracks"
@page "/tracks/albums"
@page "/tracks/archive"
@using DeepDrftManager.Services
@using DeepDrftModels.DTOs
@using DeepDrftModels.Enums
@inject ICmsTrackService CmsTrackService
@inject ISnackbar Snackbar
@inject ILogger<Releases> Logger
@inject NavigationManager NavigationManager
@attribute [Authorize]
<PageTitle>Releases — Deep DRFT Management</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
<MudText Typo="Typo.h3">Releases</MudText>
@* Catalogue-wide waveform backfill (migrated from the retired /tracks view). Both buttons act over
every track's waveform status — independent of any single grid — so the page owns the status map
directly: it computes the missing counts and re-fetches after a run. No grid reference involved. *@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.AutoFixHigh"
Disabled="@(_bulkRunning || _highResBulkRunning || MissingProfileCount == 0)"
OnClick="GenerateAllMissingAsync">
@if (_bulkRunning)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Generating @_bulkDone / @_bulkTotal…</span>
}
else
{
<span>Generate All Profiles (@MissingProfileCount)</span>
}
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Waves"
Disabled="@(_bulkRunning || _highResBulkRunning || MissingHighResCount == 0)"
OnClick="GenerateAllMissingHighResAsync">
@if (_highResBulkRunning)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Backfilling @_highResBulkDone / @_highResBulkTotal…</span>
}
else
{
<span>Backfill High-res (@MissingHighResCount)</span>
}
</MudButton>
</MudStack>
</MudStack>
@* Medium tab strip: an ALL tab plus one explicit MudTabPanel per ReleaseMedium, ALL left-most. Each
panel is hand-declared in markup (not enum-driven) so @ref captures of the per-tab grid components
are possible. Adding a future medium requires a hand-added MudTabPanel; its position in markup must
match ReleaseMedium enum order, since the ?medium= deep-link seed and ActiveMedium getter are
position-based (panel 0 = ALL, panels 1.. = enum values in order). *@
@* Medium-aware Add Track: the button reflects the active tab and pre-selects the upload form to that
tab's medium via a single query-param (?medium=…); the ALL tab defaults to Cut. The medium is a seed
only — the upload form's selector stays user-changeable after landing. *@
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
Href="@AddTrackHref(ActiveMedium)">
Add Track
</MudButton>
</MudStack>
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pt-4"
@bind-ActivePanelIndex="_activeTabIndex">
<MudTabPanel Text="ALL">
<CmsAllReleasesGrid @ref="_allGrid"
OnWaveformGenerated="RefreshWaveformStatusAsync" />
</MudTabPanel>
<MudTabPanel Text="@MediumTabLabels[ReleaseMedium.Cut]">
<CmsCutBrowser @ref="_cutBrowser"
OnWaveformGenerated="RefreshWaveformStatusAsync" />
</MudTabPanel>
<MudTabPanel Text="@MediumTabLabels[ReleaseMedium.Session]">
<CmsSessionBrowser @ref="_sessionBrowser"
Embedded="true"
OnWaveformGenerated="RefreshWaveformStatusAsync" />
</MudTabPanel>
<MudTabPanel Text="@MediumTabLabels[ReleaseMedium.Mix]">
<CmsMixBrowser @ref="_mixBrowser"
Embedded="true"
OnWaveformGenerated="RefreshWaveformStatusAsync" />
</MudTabPanel>
</MudTabs>
</MudContainer>
@code {
// Active tab. Panel 0 is ALL; panels 1.. map to Enum.GetValues<ReleaseMedium>() in order. Seeded
// from the ?medium= query param so the catalogue cards can deep-link straight to a medium's tab.
private int _activeTabIndex;
// Optional deep-link target from the catalogue cards (?medium=session selects the Sessions tab) and the
// seed for the Add Track button on the ALL tab. Read once on init; the user can switch tabs freely after.
[SupplyParameterFromQuery(Name = "medium")] public string? MediumParam { get; set; }
// The medium the Add Track button pre-selects for the active tab. ALL (panel 0) defaults to Cut; each
// medium tab maps to its enum value by position, so a fourth medium tab gets a correct Add Track for
// free — no markup fork.
private ReleaseMedium ActiveMedium =>
_activeTabIndex <= 0 ? ReleaseMedium.Cut : Enum.GetValues<ReleaseMedium>()[_activeTabIndex - 1];
// Single query-param convention: the upload page reads ?medium=… and seeds its selector (which stays
// user-changeable). Always explicit, including ALL→cut, so the link is unambiguous.
private static string AddTrackHref(ReleaseMedium medium) =>
$"/tracks/upload?medium={medium.ToString().ToLowerInvariant()}";
// Medium → tab label. The one place medium display text lives for the tab strip. The ALL tab is
// rendered separately (it is not a medium). Tabs are explicit markup so @ref captures work.
private static readonly IReadOnlyDictionary<ReleaseMedium, string> MediumTabLabels =
new Dictionary<ReleaseMedium, string>
{
[ReleaseMedium.Cut] = "CUTS",
[ReleaseMedium.Session] = "SESSIONS",
[ReleaseMedium.Mix] = "MIXES",
};
// @ref handles for the per-tab grids. Used to (a) invalidate their cached per-track waveform status
// after a page-level bulk run, and (b) to wire OnWaveformGenerated so per-row generates bubble up
// and refresh the page-level missing-count badges. Tabs are now explicit markup rather than the
// former enum-driven MediumGrid() switch so @ref captures are possible.
private CmsAllReleasesGrid? _allGrid;
private CmsCutBrowser? _cutBrowser;
private CmsSessionBrowser? _sessionBrowser;
private CmsMixBrowser? _mixBrowser;
// EntryKey → HasProfile / HasHighRes, loaded once on init so the bulk buttons can show accurate missing
// counts without depending on any rendered grid. Re-fetched after each bulk run so the counts settle.
private IReadOnlyList<WaveformStatusDto> _waveformStatus = Array.Empty<WaveformStatusDto>();
private int MissingProfileCount => _waveformStatus.Count(s => !s.HasProfile);
private int MissingHighResCount => _waveformStatus.Count(s => !s.HasHighRes);
// Local state for the parent-owned "Generate All Profiles" bulk run.
private bool _bulkRunning;
private int _bulkTotal;
private int _bulkDone;
// Local state for the "Backfill High-res" bulk run. Independent of the profile bulk above.
private bool _highResBulkRunning;
private int _highResBulkTotal;
private int _highResBulkDone;
protected override async Task OnInitializedAsync()
{
// Seed the active tab from ?medium= so a catalogue card deep-links straight to its medium. Panel 0
// is ALL; a recognised medium maps to its 1-based position. Unrecognised/absent falls through to ALL.
if (!string.IsNullOrWhiteSpace(MediumParam)
&& Enum.TryParse<ReleaseMedium>(MediumParam, ignoreCase: true, out var medium)
&& Enum.IsDefined(medium))
{
_activeTabIndex = Array.IndexOf(Enum.GetValues<ReleaseMedium>(), medium) + 1;
}
await RefreshWaveformStatusAsync();
}
private async Task RefreshWaveformStatusAsync()
{
var result = await CmsTrackService.GetWaveformStatusAsync();
_waveformStatus = result.Success && result.Value is not null
? result.Value
: Array.Empty<WaveformStatusDto>();
StateHasChanged();
}
// Invalidates the cached per-track waveform status on all embedded grids so the next row expand
// re-fetches fresh data. Called after each catalogue-wide bulk run so already-expanded rows
// reflect the new waveform state on the next expand interaction.
private async Task InvalidateAllGridsAsync()
{
var tasks = new[]
{
_allGrid?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
_cutBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
_sessionBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
_mixBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
};
await Task.WhenAll(tasks);
}
/// <summary>
/// Backfill every track missing a waveform profile, one request at a time so a large backfill does not
/// flood the API with concurrent WAV decodes. On completion, re-reads the status map so the missing
/// count settles.
/// </summary>
private async Task GenerateAllMissingAsync()
{
var missing = _waveformStatus.Where(s => !s.HasProfile).ToList();
if (missing.Count == 0)
{
return;
}
_bulkRunning = true;
_bulkTotal = missing.Count;
_bulkDone = 0;
var failures = 0;
foreach (var status in missing)
{
try
{
var result = await CmsTrackService.GenerateWaveformProfileAsync(status.EntryKey);
if (!result.Success)
{
failures++;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Waveform generation failed for {EntryKey}", status.EntryKey);
failures++;
}
_bulkDone++;
StateHasChanged();
}
_bulkRunning = false;
await RefreshWaveformStatusAsync();
await InvalidateAllGridsAsync();
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>
/// Backfill the per-track high-res visualizer datum for every track missing one, one request at a time
/// so a large backfill does not flood the API with concurrent WAV decodes. Re-runnable (a second run
/// re-reads status and only retries what is still missing). On completion, re-reads the status map.
/// </summary>
private async Task GenerateAllMissingHighResAsync()
{
var missing = _waveformStatus.Where(s => !s.HasHighRes).ToList();
if (missing.Count == 0)
{
return;
}
_highResBulkRunning = true;
_highResBulkTotal = missing.Count;
_highResBulkDone = 0;
var failures = 0;
foreach (var status in missing)
{
try
{
var result = await CmsTrackService.GenerateHighResWaveformAsync(status.EntryKey);
if (!result.Success)
{
failures++;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "High-res waveform generation failed for {EntryKey}", status.EntryKey);
failures++;
}
_highResBulkDone++;
StateHasChanged();
}
_highResBulkRunning = false;
await RefreshWaveformStatusAsync();
await InvalidateAllGridsAsync();
var succeeded = missing.Count - failures;
if (failures == 0)
{
Snackbar.Add($"Backfilled {succeeded} high-res datum(s).", Severity.Success);
}
else
{
Snackbar.Add($"Backfilled {succeeded} high-res datum(s); {failures} failed.", Severity.Warning);
}
}
}