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; } }