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
@@ -2,6 +2,7 @@ using DeepDrftAPI.Services.Opus;
using DeepDrftContent;
using DeepDrftContent.Constants;
using DeepDrftContent.Processors;
using DeepDrftContent.Processors.Opus;
using DeepDrftData;
using DeepDrftModels.DTOs;
using DeepDrftModels.Enums;
@@ -41,6 +42,7 @@ public class UnifiedTrackService
private readonly FileDb _fileDatabase;
private readonly WaveformProfileService _waveformProfileService;
private readonly IOpusTranscodeQueue _opusTranscodeQueue;
private readonly TrackFormatResolver _formatResolver;
private readonly ILogger<UnifiedTrackService> _logger;
public UnifiedTrackService(
@@ -49,6 +51,7 @@ public class UnifiedTrackService
FileDb fileDatabase,
WaveformProfileService waveformProfileService,
IOpusTranscodeQueue opusTranscodeQueue,
TrackFormatResolver formatResolver,
ILogger<UnifiedTrackService> logger)
{
_contentTrackContentService = contentTrackContentService;
@@ -56,6 +59,7 @@ public class UnifiedTrackService
_fileDatabase = fileDatabase;
_waveformProfileService = waveformProfileService;
_opusTranscodeQueue = opusTranscodeQueue;
_formatResolver = formatResolver;
_logger = logger;
}
@@ -395,6 +399,69 @@ public class UnifiedTrackService
return ResultContainer<(int, int)>.CreatePassResult((updated, skipped));
}
/// <summary>
/// Backfill-Opus (18.5, OQ4): enqueue a background Opus derive for every non-deleted track that lacks a
/// complete Opus artifact (missing audio OR missing sidecar — a half-derived track is treated as missing
/// and re-derived). Mirrors the duration-backfill posture: enumerate SQL rows, check each against the
/// <c>track-opus</c> vault, schedule the misses. Enqueue-only and non-blocking — the actual transcodes run
/// on the shared background worker, serially (the same queue the upload/replace paths feed), so this
/// returns as soon as the misses are scheduled rather than waiting on CPU-heavy transcodes. Idempotent:
/// a re-run only enqueues tracks still missing Opus, and already-queued/in-flight derives simply overwrite
/// in place. Returns (enqueued, skipped) — skipped = tracks that already have a complete Opus artifact.
/// </summary>
public async Task<ResultContainer<(int Enqueued, int Skipped)>> BackfillOpusAsync(CancellationToken ct)
{
var all = await _sqlTrackService.GetAll();
if (!all.Success || all.Value is null)
{
var error = all.Messages.FirstOrDefault()?.Message ?? "Unknown error";
_logger.LogError("BackfillOpusAsync: failed to load tracks: {Error}", error);
return ResultContainer<(int, int)>.CreateFailResult($"Could not load tracks: {error}");
}
var enqueued = 0;
var skipped = 0;
foreach (var track in all.Value)
{
ct.ThrowIfCancellationRequested();
if (await _formatResolver.HasOpusAsync(track.EntryKey))
{
skipped++;
continue;
}
_opusTranscodeQueue.Enqueue(track.EntryKey);
enqueued++;
}
_logger.LogInformation("BackfillOpusAsync complete: {Enqueued} enqueued, {Skipped} already had Opus.",
enqueued, skipped);
return ResultContainer<(int, int)>.CreatePassResult((enqueued, skipped));
}
/// <summary>
/// Per-track Opus (re)derive trigger (18.5): schedule a background transcode for one track. Returns false
/// only when the track id is unknown; the enqueue itself is non-blocking and best-effort, like the bulk
/// backfill. Re-runnable — overwrites any prior artifact in place.
/// </summary>
public async Task<Result> EnqueueOpusAsync(string entryKey, CancellationToken ct)
{
var lookup = await _sqlTrackService.GetByEntryKey(entryKey);
if (!lookup.Success)
{
var error = lookup.Messages.FirstOrDefault()?.Message ?? "Unknown error";
_logger.LogError("EnqueueOpusAsync: lookup failed for {EntryKey}: {Error}", entryKey, error);
return Result.CreateFailResult("Failed to load track.");
}
if (lookup.Value is null)
return Result.CreateFailResult(TrackNotFoundMessage);
_opusTranscodeQueue.Enqueue(entryKey);
return Result.CreatePassResult();
}
/// <summary>
/// Delete a track's SQL row, then its vault entry. SQL is the source of truth: a SQL delete
/// failure fails the operation (and leaves the vault untouched), but a subsequent vault delete