Wire Opus end-to-end playback + Backfill-Opus action (Phase 18.5)

Player picks Opus when the browser can decode it and a sidecar exists (else lossless), injecting the sidecar before stream init; seek reuses the same format. Adds the Backfill-Opus bulk API endpoint + CMS action.
This commit is contained in:
daniel-c-harvey
2026-06-23 12:39:13 -04:00
parent dce5530890
commit 2bde4908d7
12 changed files with 961 additions and 1 deletions
@@ -51,6 +51,26 @@
<span>Backfill High-res (@MissingHighResCount)</span>
}
</MudButton>
@* Backfill-Opus (Phase 18.5). Unlike the two waveform buttons, the Opus derive runs on a
server-side background worker: the API decides which tracks lack Opus and enqueues them, so
there is no client-side "missing N" count to gate on and no per-track progress to render — the
action schedules the work and reports the (enqueued / skipped) outcome. Re-runnable: a second
press only enqueues tracks still missing Opus. Disabled while a press is in flight. *@
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.GraphicEq"
Disabled="@(_bulkRunning || _highResBulkRunning || _opusBackfillRunning)"
OnClick="BackfillOpusAsync">
@if (_opusBackfillRunning)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Scheduling…</span>
}
else
{
<span>Backfill Opus</span>
}
</MudButton>
</MudStack>
</MudStack>
@@ -150,6 +170,11 @@
private int _highResBulkTotal;
private int _highResBulkDone;
// Local state for the "Backfill Opus" action. The Opus derive is server-side and background-queued, so
// there is no client-side per-track loop or progress total — this flag only guards the button while the
// single scheduling call is in flight.
private bool _opusBackfillRunning;
protected override async Task OnInitializedAsync()
{
// Seed the active tab from ?medium= so a catalogue card deep-links straight to its medium. Panel 0
@@ -291,4 +316,50 @@
Snackbar.Add($"Backfilled {succeeded} high-res datum(s); {failures} failed.", Severity.Warning);
}
}
/// <summary>
/// Kick off the catalogue-wide Backfill-Opus pass. The API enumerates the tracks lacking a complete Opus
/// artifact, enqueues a background derive for each, and returns the (enqueued, skipped) counts. This is a
/// single scheduling call — the transcodes run server-side afterward — so there is no per-track progress
/// to render here, just a busy flag and a result snackbar. Re-runnable: a second press only schedules
/// tracks still missing Opus.
/// </summary>
private async Task BackfillOpusAsync()
{
_opusBackfillRunning = true;
StateHasChanged();
try
{
var result = await CmsTrackService.BackfillOpusAsync();
if (!result.Success)
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Failed to start the Opus backfill.";
Snackbar.Add(error, Severity.Error);
return;
}
var (enqueued, skipped) = (result.Value.Enqueued, result.Value.Skipped);
if (enqueued == 0)
{
Snackbar.Add($"All {skipped} track(s) already have Opus — nothing to backfill.", Severity.Info);
}
else
{
Snackbar.Add(
$"Scheduled {enqueued} Opus transcode(s) in the background ({skipped} already had Opus). " +
"They will appear as each finishes.",
Severity.Success);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Opus backfill failed to start");
Snackbar.Add("Failed to start the Opus backfill.", Severity.Error);
}
finally
{
_opusBackfillRunning = false;
StateHasChanged();
}
}
}