Phase 9 Wave 2: api/release endpoint family — medium-aware reads + metadata writes

Adds ReleaseRepository/ReleaseManager (IReleaseService) for paged medium-filtered
release reads and Session/Mix satellite writes, UnifiedReleaseService orchestrating
vault+SQL, and ReleaseController (5 endpoints). Refactors WaveformProfileService for
configurable bucketCount/vaultName (backward-compatible) and adds the mix-waveforms vault.
Promotes brittle error-string literals to named constants (MixHasNoTrackMessage,
MixTrackNoAudioMessage) on UnifiedReleaseService.
This commit is contained in:
daniel-c-harvey
2026-06-12 22:13:31 -04:00
parent 93dcc59814
commit ca44fc8794
9 changed files with 718 additions and 16 deletions
@@ -20,4 +20,10 @@ public static class VaultConstants
/// from <c>TrackEntity.ImagePath</c>.
/// </summary>
public const string Images = "images";
/// <summary>
/// Vault name for Mix high-resolution waveform datums, keyed by the mix track's EntryKey.
/// Distinct from WaveformProfiles (player-bar low-res); same pipeline at higher resolution.
/// </summary>
public const string MixWaveforms = "mix-waveforms";
}
@@ -39,12 +39,22 @@ public class WaveformProfileService
/// <summary>
/// Computes the loudness profile from <paramref name="wavBytes"/> and stores it under
/// <paramref name="entryKey"/>. Returns false (and logs) on any failure — a missing profile
/// is handled gracefully downstream, so callers on the upload path log-and-continue rather
/// than failing the upload. Does not throw for expected failure modes.
/// <paramref name="entryKey"/> in <paramref name="vaultName"/> (defaults to
/// <see cref="VaultConstants.WaveformProfiles"/> when null). Bucket resolution defaults to
/// <see cref="WaveformProfileOptions.BucketCount"/> (512) when <paramref name="bucketCount"/> is null;
/// pass a higher value (e.g., 2048) for the Mix high-res datum. Returns false (and logs) on any
/// failure — a missing profile is handled gracefully downstream, so callers on the upload path
/// log-and-continue rather than failing the upload. Does not throw for expected failure modes.
/// </summary>
public async Task<bool> ComputeAndStoreAsync(ReadOnlyMemory<byte> wavBytes, string entryKey)
public async Task<bool> ComputeAndStoreAsync(
ReadOnlyMemory<byte> wavBytes,
string entryKey,
int? bucketCount = null,
string? vaultName = null)
{
var effectiveBucketCount = bucketCount ?? _options.BucketCount;
var effectiveVaultName = vaultName ?? VaultConstants.WaveformProfiles;
try
{
var pcm = _audioProcessor.TryExtractPcm(wavBytes.Span);
@@ -62,15 +72,14 @@ public class WaveformProfileService
value.Channels,
value.SampleRate,
value.BitsPerSample,
_options.BucketCount);
effectiveBucketCount);
var quantized = Quantize(profile);
await EnsureVaultAsync();
await EnsureVaultAsync(effectiveVaultName);
var binary = new MediaBinary(new MediaBinaryParams(quantized, quantized.Length, ProfileExtension));
var stored = await _fileDatabase.RegisterResourceAsync(
VaultConstants.WaveformProfiles, entryKey, binary);
var stored = await _fileDatabase.RegisterResourceAsync(effectiveVaultName, entryKey, binary);
if (!stored)
{
@@ -88,14 +97,15 @@ public class WaveformProfileService
}
/// <summary>
/// Returns the stored quantized profile bytes for a track, or null if no profile is stored
/// (existing tracks predate profiling, and computation may have failed). Each byte is a
/// peak-normalized loudness value in [0, 255].
/// Returns the stored quantized profile bytes for a track from <paramref name="vaultName"/>
/// (defaults to <see cref="VaultConstants.WaveformProfiles"/> when null), or null if no profile
/// is stored (existing tracks predate profiling, and computation may have failed). Each byte is
/// a peak-normalized loudness value in [0, 255].
/// </summary>
public async Task<byte[]?> GetProfileAsync(string entryKey)
public async Task<byte[]?> GetProfileAsync(string entryKey, string? vaultName = null)
{
var binary = await _fileDatabase.LoadResourceAsync<MediaBinary>(
VaultConstants.WaveformProfiles, entryKey);
vaultName ?? VaultConstants.WaveformProfiles, entryKey);
return binary?.Buffer;
}
@@ -113,11 +123,11 @@ public class WaveformProfileService
return bytes;
}
private async Task EnsureVaultAsync()
private async Task EnsureVaultAsync(string vaultName)
{
if (!_fileDatabase.HasVault(VaultConstants.WaveformProfiles))
if (!_fileDatabase.HasVault(vaultName))
{
await _fileDatabase.CreateVaultAsync(VaultConstants.WaveformProfiles, MediaVaultType.Media);
await _fileDatabase.CreateVaultAsync(vaultName, MediaVaultType.Media);
}
}
}