docs: spec Phase 9 — Release Medium Types
Four-wave plan for ReleaseMedium discriminator (Cut/Session/Mix), medium-specific metadata tables, CMS Release Archive tab, and public ARCHIVE nav + CUTS/SESSIONS/MIXES browse + detail surfaces.
This commit is contained in:
@@ -147,6 +147,118 @@ A small set of items that are real but don't fit a phase yet. Surface them when
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Release Medium Types
|
||||
|
||||
Releases gain a top-level **medium** discriminator above the existing `ReleaseType`. Three media: **Studio CUTS** (`Cut` — the only medium that uses Single/EP/Album), **Live SESSIONS** (`Session` — a single live track with a distinct hero image), **DJ MIXES** (`Mix` — a single long track with a preprocessed high-resolution waveform datum). This touches the data model, the API, the CMS, and the public site.
|
||||
|
||||
The public home page **already** carries the three-medium framing as editorial cards (Studio / Live / DJ Mix — `COMPLETED.md §8.6`, landed 2026-06-12), but those cards have no destinations and nothing below the copy layer knows what a medium is. Phase 9 makes the medium real and gives those cards somewhere to point.
|
||||
|
||||
**Architectural spine — discriminator enum + optional metadata table.** `ReleaseMedium` is a plain enum column on `ReleaseEntity`. A medium that needs data beyond the base release (Session's hero image, Mix's waveform datum) gets its own 1:1 metadata table; a medium that needs nothing extra (`Cut`) *is* the base `ReleaseEntity`. This is Open/Closed at the schema level — a future medium (e.g. Video, `§3.1`) adds an enum value and *optionally* one metadata table, and changes **zero** existing tables. The alternatives (one wide nullable table; an EF type hierarchy) both collapse to the god-table the Phase 8 normalization moved away from — rejected. Full design, contracts, and the SOLID rationale: `product-notes/phase-9-release-medium-types.md`.
|
||||
|
||||
**Design discipline throughout: extension, not modification.** Where a per-medium mapping is unavoidable (card → browser, medium → API projection, medium → detail hero), keep it in **one table per concern** — never a scattered three-arm `switch`. Drive CMS cards and nav sub-items off `Enum.GetValues<ReleaseMedium>()` + a display-metadata lookup, so a new medium surfaces automatically.
|
||||
|
||||
**The `ReleaseType`-only-for-`Cut` invariant.** Single/EP/Album is meaningful only when `Medium == Cut`. Enforce as a **domain rule** (service layer ignores/resets `ReleaseType` for non-`Cut`; CMS hides the field unless `Cut`), **not** a DB constraint — EF cannot express a conditional constraint cleanly and a raw `CHECK` is migration-fragile. The column stays on `ReleaseEntity` with its default, simply unused for non-`Cut` media.
|
||||
|
||||
Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2–4 the lettered tracks are parallel.
|
||||
|
||||
**Dependency summary:** `1 → 2 → 3 → 4`. Wave 4 (public site) can begin once Wave 2's `api/release` family is stable; it does not wait on Wave 3 (CMS).
|
||||
|
||||
---
|
||||
|
||||
### 9.1 Wave 1 — Data model + migration `[prerequisite gate]`
|
||||
|
||||
- **What:** New `ReleaseMedium` enum (`Cut, Session, Mix`) in `DeepDrftModels/Enums/`. `ReleaseEntity` gains `ReleaseMedium Medium` (default `Cut`) plus 1:1 nav properties to two new metadata entities. New `SessionMetadata` (`HeroImagePath`) and `MixMetadata` (`WaveformEntryKey`) entities, each 1:1 with `ReleaseEntity`. EF configurations + migration.
|
||||
- **Why:** Every other wave reads this schema. The discriminator-plus-optional-table shape is the load-bearing decision of the phase; it must land first and land right.
|
||||
- **Shape:**
|
||||
- `ReleaseMedium` enum with `Cut = 0` (default — existing/migrated releases stay studio cuts with no discriminator data migration).
|
||||
- `Medium` column on `releases`; `ReleaseConfiguration` documents the `ReleaseType`-only-for-`Cut` invariant.
|
||||
- `session_metadata` and `mix_metadata` tables, each with a unique FK to `releases` (1:1). `MixMetadata.WaveformEntryKey` is a vault entry key (recommended — see open question), not an inline blob.
|
||||
- Migration is **additive only** — no data migration of existing rows beyond defaulting `Medium = Cut`. Lower risk than the Phase 8 normalization.
|
||||
- **Prerequisite:** Phase 8 §8.0 normalization (`ReleaseEntity` exists) — already landed.
|
||||
- **Acceptance criteria:**
|
||||
- `ReleaseMedium` enum exists; `ReleaseEntity.Medium` defaults to `Cut`.
|
||||
- `SessionMetadata` / `MixMetadata` entities + EF configs + migration applied; solution compiles and existing releases read back as `Cut`.
|
||||
- The invariant is documented in `ReleaseConfiguration` (no DB constraint).
|
||||
- **Open questions:**
|
||||
- **Waveform storage:** vault blob + `WaveformEntryKey` (recommended — keeps the high-res datum out of SQL, consistent with the dual-database split) vs. a JSON column on `MixMetadata`. Determines whether this wave touches the vault abstraction. Resolve before the migration is written.
|
||||
|
||||
---
|
||||
|
||||
### 9.2 Wave 2 — API: medium reads + metadata uploads
|
||||
|
||||
A new `api/release` controller — the medium unit is the *release*, not the track, so medium browse and metadata uploads are release-cardinal rather than bolted onto `api/track/page`.
|
||||
|
||||
- **9.2.A — Release read endpoints (data layer + controller).**
|
||||
- **What:** `GET api/release?medium={cut|session|mix}&page=&pageSize=&sort=` (unauth, paginated, medium filter additive — omitting returns all) and `GET api/release/{id}` (unauth, single release + its medium metadata). Both `Include` the matching metadata table via a per-medium projection map.
|
||||
- **Why:** The public CUTS/SESSIONS/MIXES surfaces and the CMS browsers all read releases by medium. One cohesive release-read family keeps `api/track/page` focused on Phase 8's track-list cases.
|
||||
- **Shape:** Repository/service join through the metadata tables only for the relevant medium; base release reads never touch them. Projection map is per-medium (extension-shaped), not an `if/else` chain in the controller. `ReleaseDto` gains `Medium` + optional nested `SessionMetadataDto?` / `MixMetadataDto?` (populated only for the matching medium — mirrors Phase 8's nested-`Release` choice, not denormalized flat fields).
|
||||
- **Acceptance criteria:** `GET api/release?medium=session` returns Session releases with hero-image metadata included and no `MixMetadata`; `medium=cut` returns Cuts with neither metadata block; pagination + sort parity with `api/track/page`.
|
||||
- **9.2.B — Metadata upload endpoints.**
|
||||
- **What:** `POST api/release/session/hero-image` (ApiKey — hero image → image vault → set `SessionMetadata.HeroImagePath`) and `POST api/release/mix/waveform` (ApiKey — preprocessed waveform datum → vault → set `MixMetadata.WaveformEntryKey`).
|
||||
- **Why:** The CMS authoring flows (Wave 3 B/C) need write paths for the medium-specific data. Splitting them from the track-upload endpoint keeps each endpoint single-responsibility.
|
||||
- **Shape:** Hero-image upload mirrors the existing cover-art `UploadImageAsync` → image-vault → link pattern, targeting `HeroImagePath`. Waveform upload writes the datum to the vault and records its entry key. Both find-or-create the metadata row for the release.
|
||||
- **Acceptance criteria:** Posting a hero image to a Session release sets `HeroImagePath` and the image is served back through the existing image proxy; posting a waveform datum to a Mix release sets `WaveformEntryKey` and the datum is retrievable.
|
||||
- **Prerequisite:** 9.1.
|
||||
- **Open questions:**
|
||||
- **New endpoints vs. `api/track/page` query-param extension.** Recommend the new `api/release` family (release-cardinal browse, medium metadata `Include`); `api/track/page` can gain a cheap `medium=` passthrough later if a track-level filter is ever needed.
|
||||
|
||||
---
|
||||
|
||||
### 9.3 Wave 3 — CMS: Release Archive tab, medium selector, medium browsers
|
||||
|
||||
- **9.3.A — Release Archive tab + medium selector.**
|
||||
- **What:** Rename `TrackList.razor`'s third tab **Genre → Release Archive**. Inside it, render a **medium card group** (one card per `ReleaseMedium`, styled like the existing `CmsGenreBrowser` cards) where each card *navigates* to a medium-specific browser. Add a `ReleaseMedium` selector to `TrackNew` / `TrackEdit` / `BatchUpload` / `BatchEdit` / `AlbumHeaderFields`; show `ReleaseType` only when `Medium == Cut`, hide it (and surface medium-specific fields) for Session/Mix.
|
||||
- **Why:** The CMS needs to author medium per release and browse the archive by medium. The card-group-of-media is the CMS analogue of the home page's three-medium block.
|
||||
- **Shape:** Cards driven by `Enum.GetValues<ReleaseMedium>()` + a display-metadata lookup (label/descriptor/swatch) — **no hardcoded card switch**. Cut card → `CmsAlbumBrowser` (reused, with a `MediumFilter`); Session card → `CmsSessionBrowser`; Mix card → `CmsMixBrowser`. Selector-driven conditional fields may be explicit `@if` per medium in the form (three genuinely different field sets — SOLID discipline matters at the data/service layer, not the form markup; do not over-abstract the UI).
|
||||
- **Acceptance criteria:** The third tab reads "Release Archive" and shows one card per medium; each card navigates to its browser; the upload/edit forms show `ReleaseType` only for `Cut`.
|
||||
- **9.3.B — `CmsSessionBrowser` + hero-image authoring.**
|
||||
- **What:** New `CmsSessionBrowser.razor` — a flat list of Session releases (`Medium == Session`) with cover + hero thumbnail, session name, artist; row Edit + hero-image management. Wire the Session upload/edit path to the hero-image upload endpoint (9.2.B).
|
||||
- **Why:** Sessions are single-track releases with a distinct hero image; the album parent/child expansion of `CmsAlbumBrowser` is the wrong shape for them.
|
||||
- **Shape:** Reuse `CmsTrackGrid` parameterized by `MediumFilter` where the layout fits; the hero thumbnail is an additive column / thin wrapper, not a forked table. Hero upload reuses the cover-art one-shot pattern against `HeroImagePath`.
|
||||
- **Acceptance criteria:** Session browser lists only Session releases; uploading a hero image persists it and renders the thumbnail.
|
||||
- **9.3.C — `CmsMixBrowser` + waveform pipeline.**
|
||||
- **What:** New `CmsMixBrowser.razor` — a flat list of Mix releases (`Medium == Mix`) with an in-grid waveform-generation **status** column (mirroring Phase 8's `HasWaveformProfile` idiom) and a per-row **Generate Waveform** action. Wire the Mix upload to trigger high-resolution waveform preprocessing and upload the datum (9.2.B).
|
||||
- **Why:** A Mix without a generated high-res waveform is incomplete; status-in-grid + generate-action is the Phase 8-established pattern for waveform readiness.
|
||||
- **Shape:** Model the high-res preprocessor on the **existing player-bar waveform preprocessor** — the *same pipeline parameterized by resolution*, not a copy (player-bar peek = low-res, Mix = high-res; honours *One source, multiple views*). Upload flow: `UploadTrackAsync` → preprocess → `POST api/release/mix/waveform`.
|
||||
- **Acceptance criteria:** Mix browser lists only Mix releases and shows per-row waveform status; uploading a Mix generates + stores a high-res waveform; the per-row Generate action recovers a missing waveform.
|
||||
- **Prerequisite:** 9.2.
|
||||
- **Open questions:**
|
||||
- **Genre browse fate.** Renaming the Genre tab to Release Archive takes its slot. Recommend keeping `CmsGenreBrowser` route-reachable but tab-less (it's built and works), not retiring genre browse wholesale. Confirm with Daniel.
|
||||
- **Waveform preprocessor reuse.** Is the existing preprocessor factored to accept a resolution parameter, or does 9.3.C include a refactor to share one pipeline across player-bar and Mix? Recommend one parameterized pipeline.
|
||||
- **Single-track invariant.** Is "single track per Session/Mix" a hard upload constraint (drop the multi-track master list for those media) or a convention? Recommend enforce — it simplifies both form and detail view.
|
||||
|
||||
---
|
||||
|
||||
### 9.4 Wave 4 — Public site: ARCHIVE nav, CUTS / SESSIONS / MIXES, waveform visualizer
|
||||
|
||||
- **9.4.A — ARCHIVE nav + popover.**
|
||||
- **What:** Replace the current RELEASES / SESSIONS / MIXES nav items (in `DeepDrftPublic.Client/Layout/Pages.cs`) with a single **ARCHIVE** item. Desktop: hover shows a MudBlazor popover with CUTS / SESSIONS / MIXES → `/cuts`, `/sessions`, `/mixes`. Mobile / direct nav: ARCHIVE → an overview page `/archive` (three medium cards, reusing the §8.6 card idiom). Fixes the current **dead** Sessions/Mixes links.
|
||||
- **Why:** The nav must route into the new medium surfaces; today's Sessions/Mixes links point nowhere.
|
||||
- **Shape:** `DeepDrftMenu.razor` renders `Pages.MenuPages` as a flat `<a>` list today with no dropdown mechanism. Recommend extending the nav model with an optional `Children` collection (generalizes to future dropdowns; mobile renders children as indented sub-links) over a bespoke hardcoded popover.
|
||||
- **Acceptance criteria:** ARCHIVE replaces the three flat items; desktop hover reveals the three sub-links; mobile routes to `/archive`; no dead links remain.
|
||||
- **9.4.B — CUTS (`/cuts`).**
|
||||
- **What:** New `/cuts` route reusing the existing `AlbumsView` layout, filtered to `Medium == Cut`. Studio Singles/EPs/Albums appear as they do on the current Releases page.
|
||||
- **Why:** Honour the existing studio-release browse under the new medium taxonomy. Lowest-effort of the three media.
|
||||
- **Shape:** Parameterize `AlbumsView`'s data load with a medium filter rather than forking a component. `/cuts` = `AlbumsView` with `Medium == Cut`.
|
||||
- **Acceptance criteria:** `/cuts` shows only `Cut` releases with the current AlbumsView ergonomics.
|
||||
- **Open question:** Does the current Releases/`AlbumsView` route redirect to `/cuts` or stay an all-media view? Small routing call.
|
||||
- **9.4.C — SESSIONS (`/sessions` + `/sessions/{id}`).**
|
||||
- **What:** Gallery of session cards (cover, session name, artist) at `/sessions`; detail at `/sessions/{id}` mirroring `TrackDetail` but with the **hero image dominant above the fold**, cover secondary.
|
||||
- **Why:** Sessions are an authored content kind the home page advertises; the hero image is their distinctive visual.
|
||||
- **Shape:** Gallery borrows `AlbumsView`'s card-gallery skeleton with a session card face. Detail composes a shared `ReleaseDetailScaffold` (extracted common metadata + play + player wiring) with a hero-image hero slot — see 9.4.D open question.
|
||||
- **Acceptance criteria:** `/sessions` lists Session releases; `/sessions/{id}` renders hero-dominant with the play affordance intact.
|
||||
- **9.4.D — MIXES (`/mixes` + `/mixes/{id}`) + `MixWaveformVisualizer`.**
|
||||
- **What:** Gallery at `/mixes`; detail at `/mixes/{id}` whose above-the-fold hero is a **`MixWaveformVisualizer`** component fed by the preprocessed waveform datum from `MixMetadata`. The visualizer is a **named, reusable** component.
|
||||
- **Why:** Mixes are long continuous sets; the waveform is their signature visual and the brief calls for a reusable visualizer.
|
||||
- **Shape:** `MixWaveformVisualizer` takes the waveform datum (via `WaveformEntryKey` → content endpoint) + optional playback-position binding; renders SVG/canvas peak-bars in the established `SpectrumVisualizer` / `LevelMeterFab` visual language (don't invent a new idiom). Detail composes the same `ReleaseDetailScaffold` with the visualizer as its hero slot.
|
||||
- **Acceptance criteria:** `/mixes` lists Mix releases; `/mixes/{id}` renders the waveform visualizer fed by real datum; the visualizer is a standalone reusable component.
|
||||
- **Open question:** Design the visualizer's seek-on-click position-binding seam now even if click-to-seek ships later? Recommend yes — design the seam, defer the feature (*Design for adaptability up front*).
|
||||
- **Prerequisite:** 9.2 (the `api/release` read family). Independent of Wave 3.
|
||||
- **Open questions:**
|
||||
- **Detail-page strategy.** Three separate detail pages vs. one branching `TrackDetail` vs. a shared `ReleaseDetailScaffold` + per-medium hero slot. Recommend the scaffold (DRY-by-composition, the Phase 8 `BatchUpload`/`BatchEdit` extraction move; honours *One source, multiple views*). Sets the shape of 9.4.C and 9.4.D.
|
||||
|
||||
---
|
||||
|
||||
## Working with this file
|
||||
|
||||
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.
|
||||
|
||||
Reference in New Issue
Block a user