130 lines
7.0 KiB
C#
130 lines
7.0 KiB
C#
using DeepDrftContent.Constants;
|
|
using DeepDrftContent.FileDatabase.Models;
|
|
using DeepDrftModels.Enums;
|
|
using Microsoft.Extensions.Logging;
|
|
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
|
|
|
|
namespace DeepDrftContent.Processors.Opus;
|
|
|
|
/// <summary>
|
|
/// The server-side format resolution + sidecar lookup seam (Phase 18.2). Given a track's
|
|
/// <c>EntryKey</c> and a requested <see cref="AudioFormat"/>, returns the correct audio artifact and the
|
|
/// content-type that matches it; given an <c>EntryKey</c>, returns the Opus seek/setup sidecar bytes.
|
|
/// Downstream waves call this — 18.3 wires it behind the <c>?format=</c> stream param and serves the
|
|
/// sidecar over HTTP; this wave delivers only the seam, not the HTTP surface.
|
|
/// <para>
|
|
/// Additive and non-breaking (C2): the lossless branch streams the source exactly as the existing read
|
|
/// path does (via <see cref="TrackContentService.OpenAudioMediaStreamAsync"/>, a non-buffering disk
|
|
/// stream), and an Opus request for a track with no Opus artifact falls back to lossless rather than
|
|
/// failing. Mirrors the <see cref="WaveformProfileService"/> derived-artifact lookup precedent: read from
|
|
/// the dedicated vault, swallow misses to null (FileDatabase convention), let the caller decide.
|
|
/// </para>
|
|
/// <para>
|
|
/// Read-path streaming: artifacts are resolved as open, seekable, disk-backed <see cref="ResolvedAudio"/>
|
|
/// handles — never whole-file <c>byte[]</c> loads — so the delivery layer streams them straight to the
|
|
/// response (Range/206 honoured by the seekable <c>FileStream</c>) without buffering a ~220 MB Opus file
|
|
/// or a ~970 MB lossless source per request.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class TrackFormatResolver
|
|
{
|
|
private readonly FileDb _fileDatabase;
|
|
private readonly TrackContentService _trackContentService;
|
|
private readonly ILogger<TrackFormatResolver> _logger;
|
|
|
|
public TrackFormatResolver(
|
|
FileDb fileDatabase,
|
|
TrackContentService trackContentService,
|
|
ILogger<TrackFormatResolver> logger)
|
|
{
|
|
_fileDatabase = fileDatabase;
|
|
_trackContentService = trackContentService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves <paramref name="entryKey"/> + <paramref name="requestedFormat"/> to the audio artifact to
|
|
/// serve plus its content-type. <see cref="AudioFormat.Lossless"/> resolves the source artifact in the
|
|
/// <c>tracks</c> vault with its real MIME (WAV/MP3/FLAC). <see cref="AudioFormat.Opus"/> resolves the
|
|
/// derived Opus artifact (<c>audio/ogg</c>) when present, and <strong>falls back to lossless</strong>
|
|
/// 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.
|
|
/// </summary>
|
|
public async Task<ResolvedAudio?> ResolveAsync(string entryKey, AudioFormat requestedFormat)
|
|
{
|
|
if (requestedFormat == AudioFormat.Opus)
|
|
{
|
|
var opusVault = _fileDatabase.GetVault(VaultConstants.TrackOpus);
|
|
if (opusVault is not null)
|
|
{
|
|
// Disk-backed, seekable stream over the Opus artifact — no whole-file buffer. The caller
|
|
// owns the stream (hands it to File(...) on success, disposes on a pre-handoff throw).
|
|
var opus = await opusVault.GetEntryStreamAsync(OpusTranscodeService.OpusAudioKey(entryKey));
|
|
if (opus is not null)
|
|
return new ResolvedAudio(
|
|
opus.Stream, 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the lossless source artifact and its real MIME as a non-buffering disk stream — the existing
|
|
/// read path. Shared by the explicit-lossless branch and the Opus fallback so both produce identical
|
|
/// bytes + content-type. The returned stream is seekable, so the delivery layer's Range→206 still works.
|
|
/// </summary>
|
|
private async Task<ResolvedAudio?> ResolveLosslessAsync(string entryKey)
|
|
{
|
|
var source = await _trackContentService.OpenAudioMediaStreamAsync(entryKey);
|
|
if (source is null)
|
|
return null;
|
|
|
|
return new ResolvedAudio(
|
|
source.Stream, MimeTypeExtensions.GetMimeType(source.Extension), AudioFormat.Lossless);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the Opus setup-header + seek-index sidecar bytes for <paramref name="entryKey"/>, 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 <c>OpusSeekData</c>. The bytes are the raw <see cref="OpusSidecar"/> blob
|
|
/// (<c>[uint32 setupHeaderLength][setup-header][seek-index]</c>) exactly as 18.1 stored them.
|
|
/// </summary>
|
|
public async Task<byte[]?> GetOpusSidecarAsync(string entryKey)
|
|
{
|
|
var sidecar = await _fileDatabase.LoadResourceAsync<MediaBinary>(
|
|
VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey));
|
|
return sidecar?.Buffer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reports whether <paramref name="entryKey"/> already has a complete Opus derive — both the audio bytes
|
|
/// AND the seek/setup sidecar present in the <c>track-opus</c> 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.
|
|
/// </summary>
|
|
public async Task<bool> HasOpusAsync(string entryKey)
|
|
{
|
|
// Index-only existence — never read a file body. The opus-status admin endpoint calls this in a loop
|
|
// over the entire catalogue, so a body load here would stream the whole library's audio sequentially.
|
|
// HasIndexEntry is a pure in-memory index lookup (no disk read, no allocation per track).
|
|
var opusVault = _fileDatabase.GetVault(VaultConstants.TrackOpus);
|
|
if (opusVault is null)
|
|
return false;
|
|
|
|
if (!await opusVault.HasIndexEntry(OpusTranscodeService.OpusAudioKey(entryKey)))
|
|
return false;
|
|
|
|
// Both halves required: audio without the seek/setup sidecar is unseekable, so a half-derived track
|
|
// counts as not-having-Opus (the same completeness rule the Backfill-Opus pass enqueues against).
|
|
return await opusVault.HasIndexEntry(OpusTranscodeService.OpusSidecarKey(entryKey));
|
|
}
|
|
}
|