30999726e5
Retire the Tracks list view; promote the Releases view to /releases with medium tabs (ALL/CUTS/SESSIONS/MIXES). Migrate bulk profile/high-res backfill and per-track waveform columns into the releases grids. Point catalogue cards at the three mediums. Remove dead BrowseMode/ViewModel.
272 lines
11 KiB
Plaintext
272 lines
11 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 — DeepDrft CMS</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 tab per ReleaseMedium, ALL left-most. The medium tabs are
|
|
enum-driven — a fourth medium adds a tab automatically; only a label-lookup entry (MediumTabLabels)
|
|
and a content arm (MediumGrid) are needed, no markup fork. Selecting a tab swaps the grid below in
|
|
place. *@
|
|
@* 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 />
|
|
</MudTabPanel>
|
|
@foreach (var medium in Enum.GetValues<ReleaseMedium>())
|
|
{
|
|
<MudTabPanel Text="@MediumTabLabels[medium]">
|
|
@MediumGrid(medium)
|
|
</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; a future medium adds
|
|
// one entry here and surfaces a tab automatically. The ALL tab is rendered separately (it is not a
|
|
// medium).
|
|
private static readonly IReadOnlyDictionary<ReleaseMedium, string> MediumTabLabels =
|
|
new Dictionary<ReleaseMedium, string>
|
|
{
|
|
[ReleaseMedium.Cut] = "CUTS",
|
|
[ReleaseMedium.Session] = "SESSIONS",
|
|
[ReleaseMedium.Mix] = "MIXES",
|
|
};
|
|
|
|
// Medium → embedded grid. Each medium's grid is its own component (Cut has no per-row action; Session
|
|
// carries hero upload; Mix carries waveform generation), so the content dispatch is a per-medium
|
|
// mapping by nature — but it is a single switch returning a fragment, not a markup fork. The browsers
|
|
// render Embedded so their standalone page chrome (container, title, back button) is suppressed here.
|
|
private RenderFragment MediumGrid(ReleaseMedium medium) => medium switch
|
|
{
|
|
ReleaseMedium.Cut => @<CmsCutBrowser />,
|
|
ReleaseMedium.Session => @<CmsSessionBrowser Embedded="true" />,
|
|
ReleaseMedium.Mix => @<CmsMixBrowser Embedded="true" />,
|
|
_ => @<MudText Typo="Typo.body1" Class="mt-4">No grid for this medium.</MudText>
|
|
};
|
|
|
|
// 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();
|
|
}
|
|
|
|
/// <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();
|
|
|
|
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();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|