feat(release): front int PK with app-minted GUID EntryKey on the public addressing surface (P11 W5, 11.H)

This commit is contained in:
daniel-c-harvey
2026-06-16 17:11:55 -04:00
parent fe28573b68
commit f07d29cdcf
37 changed files with 627 additions and 160 deletions
+7 -7
View File
@@ -220,19 +220,19 @@ Paged release list, optionally filtered to one medium. Public browse data, same
- `sortDescending` (bool, optional, default false): sort direction.
- Returns 200 with `PagedResult<ReleaseDto>` on success. Returns 400 if `medium` is unrecognized. Returns 500 on query error.
### GET api/release/{id:long} (unauthenticated)
### GET api/release/{entryKey} (unauthenticated)
Single release with both metadata navs (nulls for non-matching media). Public, same auth posture as `GET api/release`.
Single release with both metadata navs (nulls for non-matching media). Public, same auth posture as `GET api/release`. Addresses releases by their opaque public `EntryKey` (GUID string), never the int PK (Phase 11 §3e).
- **Route parameter `id`** (long): the SQL release ID.
- **Response**: `ReleaseDto` with `Id`, `Title`, `Artist`, `Genre`, `ReleaseDate`, `Medium`, `ImagePath`, and media-specific metadata satellites (`MixMetadata` for Cut/Mix, `SessionMetadata` for Session; others null).
- **Route parameter `entryKey`** (string): the release's `EntryKey` (the public handle).
- **Response**: `ReleaseDto` with `Id`, `EntryKey`, `Title`, `Artist`, `Genre`, `ReleaseDate`, `Medium`, `ImagePath`, and media-specific metadata satellites (`MixMetadata` for Cut/Mix, `SessionMetadata` for Session; others null).
- Returns 200 on success. Returns 404 if not found. Returns 500 on query error.
### GET api/release/{id:long}/mix/waveform (unauthenticated)
### GET api/release/{entryKey}/mix/waveform (unauthenticated)
Serves the high-res waveform datum for a Mix release as base64-encoded bytes. Mirrors `GET api/track/{id}/waveform` but reads from the `mix-waveforms` vault.
Serves the high-res waveform datum for a Mix release as base64-encoded bytes. Mirrors `GET api/track/{id}/waveform` but reads from the `mix-waveforms` vault. Public read — addresses by the release `EntryKey` (§3e).
- **Route parameter `id`** (long): the SQL release ID.
- **Route parameter `entryKey`** (string): the release's `EntryKey`.
- **Response**: `WaveformProfileDto` with `BucketCount` and `Data` (base64).
- Returns 200 on success. Returns 404 if the release is not a Mix, carries no waveform key, or no datum is stored. Returns 500 on query/vault error.
+18 -15
View File
@@ -66,18 +66,20 @@ public class ReleaseController : ControllerBase
return Ok(result.Value);
}
// GET api/release/{id}/mix/waveform (unauthenticated)
// GET api/release/{entryKey}/mix/waveform (unauthenticated)
// Serves the high-res waveform datum for a Mix release as base64. Mirrors GET api/track/{id}/waveform
// but reads from the mix-waveforms vault. 404 when the release is not a Mix, carries no waveform key,
// or no datum is stored. Declared before the shorter "{id:long}" route for clarity.
[HttpGet("{id:long}/mix/waveform")]
public async Task<ActionResult> GetMixWaveform(long id, CancellationToken ct = default)
// or no datum is stored. Public read — addresses by the opaque EntryKey, not the int PK (§3e). The
// {entryKey} string segment cannot collide with the ApiKey-gated POST {id:long}/mix/waveform (different
// verb + constraint). Declared before the shorter "{entryKey}" route for clarity.
[HttpGet("{entryKey}/mix/waveform")]
public async Task<ActionResult> GetMixWaveform(string entryKey, CancellationToken ct = default)
{
var lookup = await _releaseService.GetByIdAsync(id, ct);
var lookup = await _releaseService.GetByEntryKeyAsync(entryKey, ct);
if (!lookup.Success)
{
var error = lookup.Messages.FirstOrDefault()?.Message ?? "Unknown error";
_logger.LogError("GetMixWaveform lookup failed for {ReleaseId}: {Error}", id, error);
_logger.LogError("GetMixWaveform lookup failed for {EntryKey}: {Error}", entryKey, error);
return StatusCode(500, "Failed to load release");
}
@@ -85,14 +87,14 @@ public class ReleaseController : ControllerBase
var waveformEntryKey = release?.MixMetadata?.WaveformEntryKey;
if (release is null || release.Medium != ReleaseMedium.Mix || string.IsNullOrEmpty(waveformEntryKey))
{
_logger.LogInformation("No mix waveform datum for release: {ReleaseId}", id);
_logger.LogInformation("No mix waveform datum for release: {EntryKey}", entryKey);
return NotFound();
}
var bytes = await _waveformProfileService.GetProfileAsync(waveformEntryKey, VaultConstants.MixWaveforms);
if (bytes is null)
{
_logger.LogInformation("Mix waveform key set but no datum stored for release: {ReleaseId}", id);
_logger.LogInformation("Mix waveform key set but no datum stored for release: {EntryKey}", entryKey);
return NotFound();
}
@@ -159,17 +161,18 @@ public class ReleaseController : ControllerBase
return StatusCode(500, error);
}
// GET api/release/{id} (unauthenticated)
// Single release with both metadata navs (nulls for non-matching media). Declared after the longer
// "{id:long}/mix/waveform" routes so the segmented routes resolve first.
[HttpGet("{id:long}")]
public async Task<ActionResult> GetReleaseById(long id, CancellationToken ct = default)
// GET api/release/{entryKey} (unauthenticated)
// Single release with both metadata navs (nulls for non-matching media). Public read — addresses by
// the opaque EntryKey, not the int PK (§3e). Declared after the longer "{entryKey}/mix/waveform"
// route so the segmented route resolves first.
[HttpGet("{entryKey}")]
public async Task<ActionResult> GetReleaseByEntryKey(string entryKey, CancellationToken ct = default)
{
var result = await _releaseService.GetByIdAsync(id, ct);
var result = await _releaseService.GetByEntryKeyAsync(entryKey, ct);
if (!result.Success)
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
_logger.LogError("GetReleaseById failed for {ReleaseId}: {Error}", id, error);
_logger.LogError("GetReleaseByEntryKey failed for {EntryKey}: {Error}", entryKey, error);
return StatusCode(500, "Failed to load release");
}