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 streams the source exactly as the existing read /// path does (via , 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 derived-artifact lookup precedent: read from /// the dedicated vault, swallow misses to null (FileDatabase convention), let the caller decide. /// /// /// Read-path streaming: artifacts are resolved as open, seekable, disk-backed /// handles — never whole-file byte[] loads — so the delivery layer streams them straight to the /// response (Range/206 honoured by the seekable FileStream) without buffering a ~220 MB Opus file /// or a ~970 MB lossless source per request. /// /// 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 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); } /// /// 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. /// private async Task 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); } /// /// 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) { // 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)); } }