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:
@@ -0,0 +1,467 @@
|
||||
# Phase 9 — Release Medium Types
|
||||
|
||||
Status: spec / design. Author: product-designer. Date: 2026-06-12.
|
||||
**Plan only — no code edits made by this doc.**
|
||||
|
||||
Cross-references: `PLAN.md §9` (the concise phase entry, four waves), `COMPLETED.md §8.6`
|
||||
("Music through Every Medium" home-page section — the public-facing three-medium framing already
|
||||
landed and these browse surfaces are where those cards point), `product-notes/phase-8-cms-track-browser.md`
|
||||
(the `ReleaseEntity` normalization and the CMS browser components this phase extends), memory
|
||||
*One source, multiple views* and *Design for adaptability up front*.
|
||||
|
||||
---
|
||||
|
||||
## 0. Why this phase exists, and what already landed
|
||||
|
||||
Phase 8 normalized the schema into `ReleaseEntity` + slim `TrackEntity`, and built the CMS browse
|
||||
surface (`CmsTrackGrid`, `CmsAlbumBrowser`, `CmsGenreBrowser`) plus the public `AlbumsView` /
|
||||
`GenresView`. COMPLETED §8.6 then pivoted the public home page from a genre-taxonomy block to a
|
||||
**three-medium** editorial block: **Studio / Live / DJ Mix**. So the medium vocabulary is *already
|
||||
user-visible* — but it is purely presentational copy on the home page today. Nothing in the data
|
||||
model, the CMS, or the browse routes knows what a "medium" is.
|
||||
|
||||
Phase 9 makes the medium real: a top-level discriminator on every release, medium-specific metadata
|
||||
where a medium needs more than a Studio cut does, and the CMS + public surfaces to author and browse
|
||||
by medium. The home-page cards from §8.6 get real destinations.
|
||||
|
||||
**The three media (Daniel's framing):**
|
||||
|
||||
| Medium | Enum | What it is | Extra data beyond a Release |
|
||||
|---|---|---|---|
|
||||
| Studio CUTS | `Cut` | Studio recordings. **The only medium that uses `ReleaseType`** (Single/EP/Album). | none — a Cut *is* the base `ReleaseEntity` |
|
||||
| Live SESSIONS | `Session` | A single live track with a distinct **hero image** (separate from cover art). | `SessionMetadata` (hero image) |
|
||||
| DJ MIXES | `Mix` | A single long track with a preprocessed **high-resolution waveform** datum. | `MixMetadata` (waveform datum ref) |
|
||||
|
||||
---
|
||||
|
||||
## 1. The core design move — discriminator enum + optional metadata table
|
||||
|
||||
This is the SOLID spine of the phase, and the thing most worth getting right because "there may be
|
||||
additional release varieties in the future."
|
||||
|
||||
**The pattern:** `ReleaseMedium` is a top-level enum discriminator on `ReleaseEntity`. Each medium
|
||||
that needs data *beyond* the base release fields gets its **own 1:1 metadata table** keyed to the
|
||||
release. A medium that needs nothing extra (Studio `Cut`) gets no table — it *is* the base
|
||||
`ReleaseEntity`.
|
||||
|
||||
```
|
||||
ReleaseEntity
|
||||
├── Medium: ReleaseMedium (Cut | Session | Mix) ← the discriminator
|
||||
├── ReleaseType (valid only when Medium == Cut)
|
||||
├── ...base release fields...
|
||||
│
|
||||
├── SessionMetadata? 1:1, present iff Medium == Session { HeroImagePath }
|
||||
└── MixMetadata? 1:1, present iff Medium == Mix { WaveformEntryKey }
|
||||
```
|
||||
|
||||
### Why this over the alternatives
|
||||
|
||||
Three shapes were on the table:
|
||||
|
||||
**(A) Single wide table.** Add `HeroImagePath` and `WaveformEntryKey` (and every future medium's
|
||||
columns) directly onto `ReleaseEntity`, nullable. *Rejected.* Every new medium widens the central
|
||||
table with columns that are null for every other medium. The table becomes a union of all media,
|
||||
and the "valid only when Medium == X" invariant multiplies across columns with no structural support.
|
||||
This is the "god table" anti-pattern — exactly what Phase 8 normalized *away* from.
|
||||
|
||||
**(B) Table-per-Hierarchy (EF TPH discriminator with subclasses).** `SessionRelease : ReleaseEntity`,
|
||||
`MixRelease : ReleaseEntity`, EF maps them into one table with a discriminator column. *Rejected for
|
||||
now.* TPH still collapses to one wide table at the storage layer (same null-column sprawl as A), and
|
||||
TPT (table-per-type) forces a subclass even for media that add nothing, plus complicates the
|
||||
`Tracks` collection and every query that doesn't care about medium. The polymorphism buys little
|
||||
when most reads are "give me releases of medium X" — a discriminator column answers that without a
|
||||
type hierarchy.
|
||||
|
||||
**(C) Discriminator enum + optional sibling metadata tables (RECOMMENDED).** The medium is a plain
|
||||
enum column — cheap to filter, cheap to index, trivially extensible (add an enum value). Medium-
|
||||
specific data lives in its own table, joined only when that medium is queried. Adding a future
|
||||
medium is: add an enum value; *if* it needs extra data, add one metadata table. The base
|
||||
`ReleaseEntity` never changes shape. This is Open/Closed at the schema level — extension by addition,
|
||||
not modification.
|
||||
|
||||
**The cost of (C):** medium-specific reads need a join (or an `Include`) to pull the metadata. That
|
||||
is the right cost to pay — it is paid only on the medium-specific surfaces (Session detail, Mix
|
||||
detail), and the base release queries (the bulk of traffic) never touch the metadata tables.
|
||||
|
||||
### The `ReleaseType`-only-for-Cut invariant
|
||||
|
||||
`ReleaseType` (Single/EP/Album) is semantically meaningful **only when `Medium == Cut`**. For
|
||||
Session and Mix it is noise. The question is how to enforce it.
|
||||
|
||||
**Recommendation: domain rule, not DB constraint.** EF Core cannot express "this column is required
|
||||
iff a sibling column equals a value" as a clean check constraint, and a raw SQL `CHECK` would be
|
||||
opaque and migration-fragile. Instead:
|
||||
|
||||
- **Service layer** is the enforcement point. `ReleaseType` is *ignored* (or reset to its default)
|
||||
when `Medium != Cut` on write; readers of a non-Cut release should not surface it.
|
||||
- **CMS validation** hides the `ReleaseType` field entirely unless `Medium == Cut` (so an admin
|
||||
cannot set a contradictory value in the first place).
|
||||
- **Leave the column on `ReleaseEntity`** with its existing default. It is simply unused for non-Cut
|
||||
media. This keeps the schema flat for the base entity and avoids a nullable-everywhere refactor.
|
||||
|
||||
Document the invariant in `ReleaseConfiguration` and the service so future readers know `ReleaseType`
|
||||
on a Session/Mix is meaningless, not missing.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data contracts
|
||||
|
||||
### 2.1 New enum
|
||||
|
||||
```csharp
|
||||
// DeepDrftModels/Enums/ReleaseMedium.cs
|
||||
public enum ReleaseMedium
|
||||
{
|
||||
Cut, // Studio recording — uses ReleaseType (Single/EP/Album)
|
||||
Session, // Single live track + hero image
|
||||
Mix // Single long track + preprocessed waveform datum
|
||||
}
|
||||
```
|
||||
|
||||
`Cut` is value 0 so it is the default, matching the brief and keeping existing/migrated releases as
|
||||
studio cuts without a data migration of the discriminator itself.
|
||||
|
||||
### 2.2 `ReleaseEntity` gains
|
||||
|
||||
```csharp
|
||||
public ReleaseMedium Medium { get; set; } = ReleaseMedium.Cut;
|
||||
public SessionMetadata? SessionMetadata { get; set; } // nav, 1:1, present iff Medium == Session
|
||||
public MixMetadata? MixMetadata { get; set; } // nav, 1:1, present iff Medium == Mix
|
||||
```
|
||||
|
||||
### 2.3 New metadata entities
|
||||
|
||||
```csharp
|
||||
// SessionMetadata — 1:1 with ReleaseEntity, present only for Session releases
|
||||
public class SessionMetadata : BaseEntity, IEntity
|
||||
{
|
||||
public long ReleaseId { get; set; } // FK + 1:1 (unique)
|
||||
public ReleaseEntity Release { get; set; }
|
||||
public required string HeroImagePath { get; set; } // entry key in the image vault
|
||||
}
|
||||
|
||||
// MixMetadata — 1:1 with ReleaseEntity, present only for Mix releases
|
||||
public class MixMetadata : BaseEntity, IEntity
|
||||
{
|
||||
public long ReleaseId { get; set; } // FK + 1:1 (unique)
|
||||
public ReleaseEntity Release { get; set; }
|
||||
public required string WaveformEntryKey { get; set; } // entry key for the waveform datum in the vault
|
||||
}
|
||||
```
|
||||
|
||||
**Open question — waveform storage shape.** Two readings of "preprocessed waveform datum":
|
||||
|
||||
- **(i) Vault blob + entry-key reference (RECOMMENDED).** The high-resolution waveform is a binary
|
||||
datum stored in a FileDatabase vault (mirroring how audio binaries live in the `tracks` vault),
|
||||
and `MixMetadata.WaveformEntryKey` is the lookup key. Keeps the (potentially large) datum out of
|
||||
the SQL row, consistent with the dual-database split (SQL = metadata, vault = binary). This is the
|
||||
recommendation — the existing player-bar waveform preprocessing already produces a byte-level
|
||||
datum; the Mix variant is the same pipeline at higher resolution, stored durably in the vault
|
||||
rather than computed per-play.
|
||||
- **(ii) JSON column on `MixMetadata`.** Store the waveform points as a JSON array directly in SQL.
|
||||
Simpler (no vault round-trip) but bloats the SQL row and breaks the "binary lives in the vault"
|
||||
rule. Only defensible if the datum is small (a few hundred points). *Not recommended* for a
|
||||
*high-resolution* waveform.
|
||||
|
||||
Recommend (i). Flag for Daniel — it determines whether Wave 1 touches the vault abstraction or just
|
||||
adds a SQL column.
|
||||
|
||||
### 2.4 DTOs
|
||||
|
||||
- `ReleaseDto` gains `ReleaseMedium Medium`.
|
||||
- New `SessionMetadataDto { HeroImagePath }` and `MixMetadataDto { WaveformEntryKey }`.
|
||||
- `ReleaseDto` gains optional `SessionMetadata? SessionMetadata` / `MixMetadata? MixMetadata`
|
||||
(populated on reads of the relevant medium, null otherwise — mirroring the nested-`Release`
|
||||
pattern Phase 8 chose for `TrackDto`). Do **not** denormalize hero-image / waveform onto every
|
||||
`ReleaseDto`; they ride the nested metadata object, present only for the matching medium.
|
||||
|
||||
---
|
||||
|
||||
## 3. CMS surface
|
||||
|
||||
### 3.1 "Release Archive" tab (was "Genre")
|
||||
|
||||
The third tab in `TrackList.razor` is renamed **Genre → Release Archive**. Today that tab renders
|
||||
`CmsGenreBrowser` (a card grid + accordion). The Release Archive tab instead renders a **medium
|
||||
card group** — one card per `ReleaseMedium` value, styled like the existing `CmsGenreBrowser` cards
|
||||
(same `MudCard` + swatch idiom, see the verified markup in §6). The medium cards do *not* expand
|
||||
inline; each **navigates** into a medium-specific browser.
|
||||
|
||||
**SOLID note — drive the cards off the enum, not a hardcoded list.** Render one card per
|
||||
`Enum.GetValues<ReleaseMedium>()` (with a small display-metadata lookup for label / descriptor /
|
||||
swatch — a `static IReadOnlyDictionary<ReleaseMedium, MediumCardInfo>` or a `[Display]` attribute
|
||||
read via reflection). Adding a future medium then surfaces a new card automatically. **Do not write a
|
||||
three-arm `switch` in the markup** — that is the modification-not-extension trap the phase is meant
|
||||
to avoid. The *navigation target* per medium is the one place a mapping is unavoidable; keep that
|
||||
mapping in one table, not scattered.
|
||||
|
||||
| Medium card | Navigates to | Browser component |
|
||||
|---|---|---|
|
||||
| Studio (Cut) | existing album browse, filtered `Medium == Cut` | `CmsAlbumBrowser` (reused, with a `MediumFilter`) |
|
||||
| Live (Session) | new | `CmsSessionBrowser` (new) |
|
||||
| DJ Mix (Mix) | new | `CmsMixBrowser` (new) |
|
||||
|
||||
What the Genre browse becomes: the genre card grid does not disappear from the product — genre is
|
||||
still a meaningful axis — but it is no longer the *third top-level tab*. Decide with Daniel whether
|
||||
genre browsing (a) moves under the Studio/Cut browser as a secondary filter, (b) stays reachable via
|
||||
a route but loses its tab, or (c) is retired in the CMS. **Recommend (b)** — keep `CmsGenreBrowser`
|
||||
reachable (it's built and works) but give the top-level third tab to Release Archive. *Open question
|
||||
for Daniel — the brief says "rename," which implies the genre tab's slot is taken; confirm genre
|
||||
browse isn't being dropped wholesale.*
|
||||
|
||||
### 3.2 New browsers
|
||||
|
||||
- **`CmsSessionBrowser.razor`** — single-track sessions. Card/row per Session release showing cover
|
||||
+ hero thumbnail, session name, artist. Reuses `CmsTrackGrid`-style data access filtered to
|
||||
`Medium == Session`; each session is one track, so the album-parent/child expansion of
|
||||
`CmsAlbumBrowser` is unnecessary — a flat list is the right shape. Row Edit → `TrackEdit` (or a
|
||||
session-aware edit); row actions include hero-image management.
|
||||
- **`CmsMixBrowser.razor`** — single-track mixes. Flat list filtered to `Medium == Mix`. Each row
|
||||
shows waveform-generation status (mirroring the Phase 8 `HasWaveformProfile` in-grid status idiom —
|
||||
a Mix without a generated high-res waveform is incomplete). Per-row "Generate Waveform" action.
|
||||
|
||||
Both reuse `CmsTrackGrid` where the layout fits, parameterized by `MediumFilter` — same
|
||||
"one grid, parameterized" DRY move Phase 8 established for genre. Where a medium's list genuinely
|
||||
differs (hero thumb column for sessions, waveform-status column for mixes), those are additive
|
||||
columns on a shared grid or a thin medium-specific wrapper — **not** a forked table.
|
||||
|
||||
### 3.3 Medium selector on the upload/edit forms
|
||||
|
||||
`TrackNew.razor`, `TrackEdit.razor`, `BatchUpload.razor`, `BatchEdit.razor`, and the shared
|
||||
`AlbumHeaderFields.razor` gain a **`ReleaseMedium` selector** (`MudSelect<ReleaseMedium>`).
|
||||
|
||||
Conditional fields driven by the selector:
|
||||
|
||||
- `Medium == Cut` → show the existing `ReleaseType` (Single/EP/Album) field. Show album-header
|
||||
multi-track ergonomics as today.
|
||||
- `Medium == Session` → **hide** `ReleaseType`; show a **hero-image upload** field (in addition to
|
||||
cover art). Constrain to a single track.
|
||||
- `Medium == Mix` → **hide** `ReleaseType`; the upload triggers **waveform preprocessing** (§3.4).
|
||||
Constrain to a single track.
|
||||
|
||||
The conditional rendering should key off the enum, not a cascade of `@if (medium == X)` blocks where
|
||||
avoidable — but with only three media and genuinely different field sets, a small `@if` per medium
|
||||
in the form is acceptable and clearer than over-abstracting. The SOLID discipline matters most at the
|
||||
*data/service* layer; a form is allowed to be explicit. Flag the tension, don't over-engineer the UI.
|
||||
|
||||
### 3.4 Mix waveform pipeline (CMS-triggered)
|
||||
|
||||
When a Mix is uploaded, the CMS triggers the **high-resolution waveform preprocessor** and uploads
|
||||
the resulting datum to the vault. Model this on the **existing player-bar waveform preprocessing**
|
||||
(the pipeline that already produces a byte-level waveform datum), but:
|
||||
|
||||
- produce a **high-resolution** datum (more sample points than the player-bar peek),
|
||||
- store it durably in the vault (not compute-per-play),
|
||||
- record its `WaveformEntryKey` in `MixMetadata`.
|
||||
|
||||
The CMS upload flow becomes: upload audio (existing `UploadTrackAsync`) → trigger waveform
|
||||
preprocessing → `POST api/release/mix/waveform` with the datum → service writes datum to vault +
|
||||
sets `MixMetadata.WaveformEntryKey`. The per-row "Generate Waveform" action in `CmsMixBrowser` is the
|
||||
recovery path for a mix whose waveform failed or predates the feature.
|
||||
|
||||
**Reuse point.** The existing waveform preprocessor should be the *same code path* parameterized by
|
||||
resolution, not a copy. If the current preprocessor isn't factored to allow a resolution parameter,
|
||||
that refactor is part of Wave 3 track C — flag it. (This honours the *One source, multiple views*
|
||||
preference: the player-bar peek and the Mix high-res datum are two resolutions of one pipeline, not
|
||||
two pipelines.)
|
||||
|
||||
### 3.5 Session hero-image pipeline (CMS)
|
||||
|
||||
Session uploads provide a **hero-image** upload path (distinct from cover art). The hero image is
|
||||
stored in the **image vault** (same vault as cover art, different entry key) and recorded in
|
||||
`SessionMetadata.HeroImagePath`. Flow: `POST api/release/session/hero-image` (multipart) → image
|
||||
vault write → `SessionMetadata.HeroImagePath` set. Mirrors the existing cover-art
|
||||
`UploadImageAsync` + link-via-`UpdateAsync` pattern Phase 8 documented; the only difference is the
|
||||
target field.
|
||||
|
||||
---
|
||||
|
||||
## 4. API surface
|
||||
|
||||
The phase prefers **a new `release` controller** over bolting medium concerns onto the track
|
||||
endpoints, because the unit of medium is the *release*, not the track. Endpoints:
|
||||
|
||||
| Endpoint | Auth | Purpose |
|
||||
|---|---|---|
|
||||
| `GET api/release?medium={cut\|session\|mix}&page=&pageSize=&sort=` | unauth (public reads) | Paginated releases of a medium, with medium-specific metadata `Include`d. The medium filter is additive — omitting it returns all releases. |
|
||||
| `GET api/release/{id}` | unauth | Single release + its medium metadata (hero image for Session, waveform key for Mix). Feeds the public detail views. |
|
||||
| `POST api/release/session/hero-image` | ApiKey | Upload hero image → image vault → set `SessionMetadata.HeroImagePath`. |
|
||||
| `POST api/release/mix/waveform` | ApiKey | Upload preprocessed waveform datum → vault → set `MixMetadata.WaveformEntryKey`. |
|
||||
|
||||
**Decision — new endpoints vs. query-param extension of `api/track/page`.** The brief offers either.
|
||||
Recommend a **new `api/release` family** rather than overloading `api/track/page`:
|
||||
|
||||
- The browse axis is now the *release* (medium lives on the release, and Session/Mix are
|
||||
single-track releases where "list of tracks" is the wrong primary shape).
|
||||
- Medium-specific metadata (`Include` of `SessionMetadata` / `MixMetadata`) belongs on a release
|
||||
read, not a track-page read.
|
||||
- `api/track/page` stays focused on the track-list use cases Phase 8 built; it can *gain* a
|
||||
`medium=` passthrough filter cheaply if a track-level medium filter is ever needed, but the
|
||||
primary medium-browse path is release-cardinal.
|
||||
|
||||
This keeps each endpoint cohesive (SRP at the HTTP boundary) rather than growing `api/track/page`
|
||||
into the everything-endpoint.
|
||||
|
||||
**Extensibility note.** `GET api/release?medium=` should accept *any* `ReleaseMedium` value and
|
||||
`Include` the matching metadata via a small per-medium projection map — not a hardcoded
|
||||
`if session … else if mix …` chain in the controller. A future medium adds a projection entry, not a
|
||||
new endpoint branch. Same Open/Closed discipline as the CMS cards.
|
||||
|
||||
---
|
||||
|
||||
## 5. Public site surface
|
||||
|
||||
### 5.1 ARCHIVE nav with sub-items
|
||||
|
||||
Replace the current **RELEASES / SESSIONS / MIXES** nav links (defined in
|
||||
`DeepDrftPublic.Client/Layout/Pages.cs` — note: `Pages.cs` lives in `Layout/`, not `Pages/`) with a
|
||||
single **ARCHIVE** item.
|
||||
|
||||
- **Desktop:** hovering ARCHIVE shows a **MudBlazor popover** (`MudMenu` on hover, or a `MudPopover`
|
||||
triggered on mouse-enter) with three sub-items: **CUTS / SESSIONS / MIXES** → `/cuts`,
|
||||
`/sessions`, `/mixes`.
|
||||
- **Mobile / direct nav:** ARCHIVE links to an **overview page** (`/archive`) — the three media as
|
||||
large cards, the mobile-friendly equivalent of the desktop popover. (The home-page §8.6 three-card
|
||||
block is the design precedent for this overview; `/archive` can reuse that card idiom.)
|
||||
|
||||
This fixes the current **dead links** — today's "Sessions" and "Mixes" nav items point nowhere. They
|
||||
resolve to the new `/sessions` and `/mixes` routes.
|
||||
|
||||
**Nav data note.** `DeepDrftMenu.razor` renders `Pages.MenuPages` as a flat `<a>` list (verified).
|
||||
ARCHIVE-with-popover needs a nav item that can carry *children*. Either (a) extend the `MenuPages`
|
||||
model with an optional `Children` collection and special-case rendering of items that have children,
|
||||
or (b) hardcode ARCHIVE as a distinct popover component in `DeepDrftMenu` alongside the flat list.
|
||||
**Recommend (a)** — a `Children` collection on the nav model generalizes (a future "About" dropdown
|
||||
gets it free) and keeps the menu data-driven. Mobile renders children as indented sub-links inside
|
||||
the existing hamburger panel.
|
||||
|
||||
### 5.2 CUTS — `/cuts`
|
||||
|
||||
Reuses the existing **`AlbumsView`** layout, filtered to `Medium == Cut`. Studio Singles / EPs /
|
||||
Albums all appear here exactly as the current Releases page shows them. Lowest-effort of the three —
|
||||
it is the current `AlbumsView` with a medium filter on its data source.
|
||||
|
||||
**Recommendation:** parameterize `AlbumsView`'s data load with a medium filter rather than forking a
|
||||
new component. `/cuts` is `AlbumsView` with `Medium == Cut`; if a future "/all releases" view wants
|
||||
the unfiltered set, the same component serves it. (Whether the *old* `/releases`-style route
|
||||
redirects to `/cuts` or is retired is a small routing call — flag.)
|
||||
|
||||
### 5.3 SESSIONS — `/sessions` + `/sessions/{id}`
|
||||
|
||||
- **Gallery (`/sessions`):** card grid of session cards — cover image, session name, artist. New
|
||||
component, but borrow `AlbumsView`'s card-gallery skeleton (it's the same gallery shape with a
|
||||
different card face).
|
||||
- **Detail (`/sessions/{id}`):** mirrors `TrackDetail` but the **hero image is the dominant
|
||||
above-the-fold visual**, cover art secondary. New `SessionDetail` page (or `TrackDetail`
|
||||
parameterized with a "hero-dominant" layout variant — see below).
|
||||
|
||||
**Reuse decision — new pages vs. parameterized `TrackDetail`.** `TrackDetail` today is cover-led.
|
||||
Session detail is hero-led; Mix detail is waveform-led. Three readings:
|
||||
|
||||
- **(i) Three separate detail pages.** Clearest per-medium, most duplication of the shared scaffolding
|
||||
(play affordance, metadata block, player wiring).
|
||||
- **(ii) One `TrackDetail` with a layout-variant switch.** Least duplication, but a single component
|
||||
branching on medium for above-the-fold layout gets busy.
|
||||
- **(iii) Shared scaffolding component + per-medium "hero slot" (RECOMMENDED).** Extract the common
|
||||
detail scaffolding (metadata, play control, player wiring) into a `ReleaseDetailScaffold` that
|
||||
takes a `RenderFragment HeroContent`. `CutDetail`/`SessionDetail`/`MixDetail` each supply their
|
||||
hero (cover, hero image, waveform visualizer respectively) and compose the shared scaffold. This is
|
||||
the same DRY-by-composition move Phase 8 used for `BatchUpload`/`BatchEdit` sub-component
|
||||
extraction — and it honours *One source, multiple views*: one scaffold, three hero renderings.
|
||||
|
||||
Recommend (iii). It bounds the duplication while letting each medium own its distinctive
|
||||
above-the-fold without polluting the others.
|
||||
|
||||
### 5.4 MIXES — `/mixes` + `/mixes/{id}`
|
||||
|
||||
- **Gallery (`/mixes`):** card grid like sessions.
|
||||
- **Detail (`/mixes/{id}`):** the hero slot is a **`MixWaveformVisualizer`** component fed by the
|
||||
preprocessed waveform datum from `MixMetadata.WaveformEntryKey`. Designed as a **named, reusable
|
||||
component** (the brief is explicit) so it can be reused — e.g., a future inline waveform on the
|
||||
player bar, or a mix card preview.
|
||||
|
||||
**`MixWaveformVisualizer` design notes.**
|
||||
|
||||
- **Input:** the waveform datum (fetched via the `WaveformEntryKey` → vault read, served through a
|
||||
content endpoint like the existing audio/image proxies). Component takes the datum (or a URL to
|
||||
it) + optional playback-position binding.
|
||||
- **Rendering:** SVG or canvas peak-bars, consistent with the existing `SpectrumVisualizer` /
|
||||
`LevelMeterFab` visual language already in the player stack (don't invent a new visual idiom —
|
||||
borrow the established peak-bar look). The §8 player-bar waveform is the low-res cousin; this is its
|
||||
high-res, full-width sibling.
|
||||
- **Interactivity (optional, flag):** clicking the waveform could seek (the streaming player already
|
||||
supports seek-beyond-buffer). Worth designing the component's position-binding seam *now* even if
|
||||
seek-on-waveform-click is deferred — designing the seam costs little, backfilling it costs a
|
||||
rewrite. (Memory: *Design for adaptability up front* — defer the feature, design the seam.)
|
||||
|
||||
---
|
||||
|
||||
## 6. Verified facts (read against live source 2026-06-12)
|
||||
|
||||
- `ReleaseEntity` exists (`DeepDrftModels/Entities/ReleaseEntity.cs`), inherits `BaseEntity`, has
|
||||
`Title, Artist, Genre?, ReleaseDate?, ImagePath?, ReleaseType (default Single), CreatedByUserId?,
|
||||
Tracks`. **No `Medium` field yet.**
|
||||
- `ReleaseType` enum (`DeepDrftModels/Enums/ReleaseType.cs`): `Single, EP, Album`.
|
||||
- CMS browser components **already exist** (Phase 8 landed): `CmsTrackGrid.razor`,
|
||||
`CmsAlbumBrowser.razor`, `CmsGenreBrowser.razor` in `DeepDrftManager/Components/Pages/Tracks/`.
|
||||
- `CmsGenreBrowser` card idiom (verified): `MudGrid Spacing=3` → `MudItem xs=12 sm=6 md=4` →
|
||||
`MudCard` with an `@onclick` toggle, a swatch `<div>`, and `MudCardContent` (`Typo.h6` name +
|
||||
`Typo.body2` count). The Release Archive medium cards should match this idiom. `CmsTrackGrid`
|
||||
already takes `GenreFilter` / `ShowAddButton` parameters — the precedent for a `MediumFilter`.
|
||||
- Public nav: `DeepDrftMenu.razor` renders `Pages.MenuPages` as a **flat `<a>` list** (desktop) and
|
||||
inside a hamburger panel (mobile). `Pages.cs` lives in **`DeepDrftPublic.Client/Layout/`** (the
|
||||
root `CLAUDE.md` for the client confirms `Layout/Pages.cs`, "MenuPages for header, AllPages for
|
||||
exhaustive list"). The nav has **no popover/dropdown mechanism today** — ARCHIVE introduces the
|
||||
first one.
|
||||
- Public home page **already** carries the three-medium framing (Studio / Live / DJ Mix) as
|
||||
editorial cards — COMPLETED §8.6, landed 2026-06-12. Those cards currently have no destinations;
|
||||
Phase 9's `/cuts`, `/sessions`, `/mixes` (or `/archive`) are where they should point.
|
||||
- The dual-database split: SQL = metadata (EF), vault = binary (FileDatabase). Waveform datum
|
||||
(recommended) and hero/cover images live vault-side; medium discriminator + metadata-table rows
|
||||
live SQL-side.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open questions (need Daniel before build)
|
||||
|
||||
1. **Waveform storage shape (§2.3).** Vault blob + `WaveformEntryKey` (recommended) vs. JSON column
|
||||
on `MixMetadata`. Determines whether Wave 1 touches the vault abstraction. *Recommend vault blob.*
|
||||
2. **Genre browse fate (§3.1).** "Rename the Genre tab to Release Archive" takes the third tab's
|
||||
slot. Does genre browsing survive (route-reachable but tab-less — recommended), move under the
|
||||
Cut browser as a secondary filter, or retire in the CMS? *Recommend keep route-reachable.*
|
||||
3. **Waveform preprocessor reuse (§3.4).** Is the existing player-bar waveform preprocessor factored
|
||||
to accept a resolution parameter, or does Wave 3 track C include a refactor to share one pipeline
|
||||
across player-bar (low-res) and Mix (high-res)? *Recommend one parameterized pipeline.*
|
||||
4. **Detail-page strategy (§5.3).** Three separate detail pages vs. one branching `TrackDetail` vs.
|
||||
shared `ReleaseDetailScaffold` + per-medium hero slot (recommended). Sets the public-site Wave 4
|
||||
shape. *Recommend the scaffold.*
|
||||
5. **Old releases route (§5.2).** Does the current Releases/`AlbumsView` route redirect to `/cuts`,
|
||||
or stay as an all-media view? *Small routing call.*
|
||||
6. **Nav model children (§5.1).** Extend `MenuPages` with an optional `Children` collection
|
||||
(recommended, generalizes) vs. hardcode ARCHIVE as a bespoke popover component. *Recommend the
|
||||
model extension.*
|
||||
7. **`MixWaveformVisualizer` seek-on-click (§5.4).** Design the position-binding seam now even if
|
||||
click-to-seek ships later? *Recommend design the seam, defer the feature.*
|
||||
8. **Single-track invariant for Session/Mix (§3.3).** Sessions and Mixes are "single-track." Is that
|
||||
a hard constraint enforced at upload (one track per Session/Mix release), or a convention? If
|
||||
hard, the CMS upload form for those media should drop the multi-track master list entirely.
|
||||
*Recommend enforce — it simplifies both the form and the detail view.*
|
||||
|
||||
---
|
||||
|
||||
## 8. SOLID summary — why this is extension-shaped
|
||||
|
||||
The phase is designed so a **fourth medium** (say, "Video," already hinted in `PLAN.md §3.1`) costs:
|
||||
|
||||
1. one new `ReleaseMedium` enum value,
|
||||
2. *if* it needs extra data, one new metadata table + DTO,
|
||||
3. one display-metadata entry (so the CMS card + nav sub-item appear automatically),
|
||||
4. one projection entry in the `api/release?medium=` map,
|
||||
5. its own hero-slot renderer for the detail scaffold.
|
||||
|
||||
It costs **zero** changes to: the base `ReleaseEntity` shape, the other media's tables, the existing
|
||||
browse grids, or the existing detail scaffolding. That is the Open/Closed payoff of discriminator-
|
||||
enum + optional-metadata-table over a wide table or a type hierarchy. Where the design *does* admit a
|
||||
mapping (card → browser, medium → projection, medium → hero renderer), that mapping is kept in **one
|
||||
table per concern**, never duplicated as scattered `switch`/`if` chains. That single-table-of-mappings
|
||||
discipline is the difference between "extensible" and "extensible on paper."
|
||||
Reference in New Issue
Block a user