using DeepDrftContent.Constants;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftModels.Enums;
using Microsoft.Extensions.Logging;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
namespace DeepDrftContent.Processors.Opus;
///
/// The server-side format resolution + sidecar lookup seam (Phase 18.2). Given a track's
/// EntryKey and a requested , returns the correct audio artifact and the
/// content-type that matches it; given an EntryKey, returns the Opus seek/setup sidecar bytes.
/// Downstream waves call this — 18.3 wires it behind the ?format= stream param and serves the
/// sidecar over HTTP; this wave delivers only the seam, not the HTTP surface.
///
/// Additive and non-breaking (C2): the lossless branch reads the source exactly as the existing stream
/// path does (via ), and an Opus request for a track
/// with no Opus artifact falls back to lossless rather than failing. Mirrors the
/// derived-artifact lookup precedent: read from the dedicated vault,
/// swallow misses to null (FileDatabase convention), let the caller decide.
///
///
public sealed class TrackFormatResolver
{
private readonly FileDb _fileDatabase;
private readonly TrackContentService _trackContentService;
private readonly ILogger _logger;
public TrackFormatResolver(
FileDb fileDatabase,
TrackContentService trackContentService,
ILogger logger)
{
_fileDatabase = fileDatabase;
_trackContentService = trackContentService;
_logger = logger;
}
///
/// Resolves + to the audio artifact to
/// serve plus its content-type. resolves the source artifact in the
/// tracks vault with its real MIME (WAV/MP3/FLAC). resolves the
/// derived Opus artifact (audio/ogg) when present, and falls back to lossless
/// when it is not (C2). Returns null only when even the lossless source is missing — i.e. the track has
/// no audio at all (an unknown key or a genuinely empty vault), the one case the caller treats as 404.
///
public async Task ResolveAsync(string entryKey, AudioFormat requestedFormat)
{
if (requestedFormat == AudioFormat.Opus)
{
var opus = await _fileDatabase.LoadResourceAsync(
VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey));
if (opus is not null)
return new ResolvedAudio(opus, MimeTypeExtensions.GetMimeType(opus.Extension), AudioFormat.Opus);
// C2 fallback: no Opus artifact yet (legacy row, not backfilled, or transcode failed). Degrade
// to lossless rather than 404 — Opus is strictly additive; its absence never means "no audio".
_logger.LogInformation(
"Opus requested for {EntryKey} but no Opus artifact exists; falling back to lossless.", entryKey);
}
return await ResolveLosslessAsync(entryKey);
}
///
/// Resolves the lossless source artifact and its real MIME — the existing read path, unchanged. Shared
/// by the explicit-lossless branch and the Opus fallback so both produce identical bytes + content-type.
///
private async Task ResolveLosslessAsync(string entryKey)
{
var source = await _trackContentService.GetAudioBinaryAsync(entryKey);
if (source is null)
return null;
return new ResolvedAudio(source, MimeTypeExtensions.GetMimeType(source.Extension), AudioFormat.Lossless);
}
///
/// Returns the Opus setup-header + seek-index sidecar bytes for , or null
/// when no sidecar is stored (no Opus artifact yet, or an older derive predating the sidecar). 18.3
/// serves these on their own path; 18.4 fetches them once on track load and parses them into the
/// client's OpusSeekData. The bytes are the raw blob
/// ([uint32 setupHeaderLength][setup-header][seek-index]) exactly as 18.1 stored them.
///
public async Task GetOpusSidecarAsync(string entryKey)
{
var sidecar = await _fileDatabase.LoadResourceAsync(
VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey));
return sidecar?.Buffer;
}
///
/// Reports whether already has a complete Opus derive — both the audio bytes
/// AND the seek/setup sidecar present in the track-opus vault. The Backfill-Opus pass (18.5) uses
/// this to enqueue only tracks that are missing or half-derived (audio without sidecar = unseekable, so
/// treated as incomplete and re-derived). Both halves are required because the transcode stores them in
/// sequence and a sidecar-write failure leaves a track the delivery layer must not treat as Opus-ready.
///
public async Task HasOpusAsync(string entryKey)
{
var audio = await _fileDatabase.LoadResourceAsync(
VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey));
if (audio is null)
return false;
var sidecar = await _fileDatabase.LoadResourceAsync(
VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey));
return sidecar is not null;
}
}