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
@@ -4,7 +4,7 @@ using DeepDrftPublic.Client.Services;
namespace DeepDrftPublic.Client.ViewModels;
/// <summary>
/// State for the Cut album-detail page (/cuts/{id}). Unlike <see cref="ReleaseDetailViewModel"/>
/// State for the Cut album-detail page (/cuts/{entryKey}). Unlike <see cref="ReleaseDetailViewModel"/>
/// (which resolves a single playable track for Session/Mix), a Cut is multi-track: it loads the
/// release and the full ordered track list for that release. The list is fetched through the
/// existing releaseId-filtered track page sorted by TrackNumber — the explicit 1-based ordinal
@@ -40,7 +40,7 @@ public class CutDetailViewModel
IsLoading = false;
}
public async Task Load(long releaseId)
public async Task Load(string entryKey)
{
IsLoading = true;
NotFound = false;
@@ -49,7 +49,7 @@ public class CutDetailViewModel
try
{
var releaseResult = await _releaseData.GetById(releaseId);
var releaseResult = await _releaseData.GetByEntryKey(entryKey);
if (releaseResult is not { Success: true, Value: { } release })
{
NotFound = true;
@@ -59,9 +59,11 @@ public class CutDetailViewModel
Release = release;
// The album's tracks via the releaseId-filtered page — an exact join, not a title string
// (which collides across same-titled releases and breaks on rename). Sorted by TrackNumber
// so rows render in saved order. A Cut with no streamable tracks simply leaves the list
// empty (the page renders the header with no rows).
// (which collides across same-titled releases and breaks on rename). The public page
// addresses the release by EntryKey; the track→release join stays on the internal int FK
// (Phase 11 §3e), so use the resolved release.Id here. Sorted by TrackNumber so rows render
// in saved order. A Cut with no streamable tracks simply leaves the list empty (the page
// renders the header with no rows).
var trackResult = await _trackData.GetPage(
pageNumber: 1,
pageSize: AlbumPageSize,
@@ -35,7 +35,7 @@ public class ReleaseDetailViewModel
IsLoading = false;
}
public async Task Load(long releaseId)
public async Task Load(string entryKey)
{
IsLoading = true;
NotFound = false;
@@ -44,7 +44,7 @@ public class ReleaseDetailViewModel
try
{
var releaseResult = await _releaseData.GetById(releaseId);
var releaseResult = await _releaseData.GetByEntryKey(entryKey);
if (releaseResult is not { Success: true, Value: { } release })
{
NotFound = true;
@@ -54,9 +54,11 @@ public class ReleaseDetailViewModel
Release = release;
// Resolve the playable track via the releaseId-filtered track page — an exact join, not a
// title string (which collides across same-titled releases and breaks on rename). Session/Mix
// releases carry a single track; take the first. A release with no streamable track simply
// leaves Track null (the detail page hides the play affordance).
// title string (which collides across same-titled releases and breaks on rename). The public
// page addresses the release by EntryKey; the track→release join stays on the internal int
// FK (Phase 11 §3e leaves internal joins on the int PK), so use the resolved release.Id here.
// Session/Mix releases carry a single track; take the first. A release with no streamable
// track simply leaves Track null (the detail page hides the play affordance).
var trackResult = await _trackData.GetPage(
pageNumber: 1, pageSize: 1, releaseId: release.Id);
if (trackResult is { Success: true, Value: { Items: { } items } })