Files
deepdrft/DeepDrftAPI/CLAUDE.md
T
daniel-c-harvey ca44979b08
Deploy DeepDrftAPI / Build, Publish & Bundle (push) Successful in 2m25s
Deploy DeepDrftManager / Build & Publish (push) Successful in 1m28s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 4m2s
Package install tarball / package (push) Successful in 6s
Deploy DeepDrftAPI / Deploy (push) Successful in 1m33s
Deploy DeepDrftManager / Deploy (push) Successful in 1m59s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m22s
docs: record Opus/derived read-path streaming and index-only opus-status
2026-06-26 15:32:18 -04:00

41 KiB

CLAUDE.md - DeepDrftAPI

Guidance for working in the DeepDrftAPI project (the dual-database authority and AuthBlocks API host).

See the root CLAUDE.md for full architecture overview. This file covers what is specific to this project.

One-line purpose

Dual-database authority for tracks (SQL metadata + FileDatabase binary), releases (SQL metadata with media-specific satellites), and images (FileDatabase binary); AuthBlocks API host (JWT auth, role/admin seed). Track endpoints expose CRUD with upload+persist, delete+cleanup, paged listing with filters, metadata operations, waveform profiles (512-bucket player-bar seeker + per-track high-res visualizer datum), and release associations. Release endpoints provide paged listing with medium filter, single-release read, and media-specific operations (session hero-image upload; mix waveform is a caller-less legacy delegate — the track-cardinal GET api/track/{entryKey}/waveform/high-res is the live fetch path). Image endpoints provide authenticated upload and unauthenticated streaming. ApiKey middleware for authenticated endpoints, JWT + AuthBlocks for auth. CORS, forwarded headers. FileDatabase implementation lives in DeepDrftContent; SQL services in DeepDrftData.

What lives here now (only)

  • Program.cs, Startup.cs: HTTP host config, DI wiring, middleware setup, port binding. AuthBlocks startup: AddAuthBlocks, UseAuthBlocksStartupAsync, MapAuthBlocks, authentication/authorization middleware.
  • Services/UnifiedTrackService.cs: Host-internal orchestrator. Coordinates streaming vault write + SQL persist for upload (UploadAsync), and SQL delete + vault remove for delete (DeleteAsync). The upload/replace hot path streams audio into the vault via the ProcessedAudio plan (Wave 1 OOM fix) and then computes both waveform datums in a single bounded streaming pass via TryStoreWaveformDatumsAsync (Wave 2 OOM fix) — neither path buffers the whole audio file in a managed byte[].
  • Services/UnifiedReleaseService.cs: Host-internal orchestrator. Coordinates release mutations (mix waveform compute + store, session hero-image upload + link).
  • Controllers/TrackController.cs: Track endpoints (see below).
  • Controllers/ReleaseController.cs: Release endpoints (see below).
  • Middleware/ApiKeyAuthenticationMiddleware.cs, Middleware/ApiKeyAuthorizeAttribute.cs: ApiKey validation logic (for track endpoints only).
  • Models/: Settings POCOs only (ApiKeySettings, CorsSettings, FileDatabaseSettings, UploadSettings, UploadStagingDirectory). No domain code.
  • environment/filedatabase.json: FileDatabase vault path config (loaded via CredentialTools, not in repo).
  • environment/apikey.json: API key for track endpoints (loaded via CredentialTools, not in repo, must be created locally or at deployment).
  • environment/connections.json: SQL and Auth connection strings (loaded via CredentialTools, not in repo, format: { "ConnectionStrings": { "DefaultConnection": "...", "Auth": "..." } }).
  • environment/authblocks.json: AuthBlocks configuration (JWT secret, email sender creds, admin seed creds) (loaded via CredentialTools, not in repo).

What does NOT live here anymore

  • FileDatabase/, Processors/, media models (AudioBinary, ImageBinary, etc.) — all in DeepDrftContent (class library).
  • EF Core context and repository — in DeepDrftData.
  • Hosts only own HTTP surface and wiring. New domain code goes in *.Services (shared libraries) or host-internal Services/ folders (e.g., UnifiedTrackService here for dual-database orchestration).

The endpoint surface

GET api/track/{trackId} (unauthenticated)

Streams the track's audio bytes from disk with HTTP Range support. An optional ?format=opus query parameter selects between the derived Opus artifact and the lossless source.

  • Route parameter trackId (string): the entry id inside the tracks vault (i.e. TrackEntity.EntryKey).
  • Query parameter format (optional): opus requests the derived Ogg Opus artifact from the track-opus vault when present, falling back to lossless when it is not (C2 — never 404 if any audio exists); omitted or any other value delivers the lossless source in its stored format (WAV/MP3/FLAC). Both arms stream from a seekable disk FileStream via File(stream, contentType, enableRangeProcessing: true) — no whole-file byte[]; Content-Type reflects what was actually served (e.g., audio/ogg on an Opus hit, the source's real MIME on a lossless request or C2 fallback).
  • Range header (optional): HTTP Range header for byte-range requests (e.g., Range: bytes=1000-). Server responds with 206 Partial Content and streams from the requested offset. Honoured on both arms (seekable FileStream in both cases).
  • Returns 200 for full-file requests, 206 for Range requests, 404 if no audio artifact exists for the track, 500 if vault operations fail.

GET api/track/albums (unauthenticated)

Returns a list of all releases with per-release track counts. Public browse data, same auth posture as GET api/track/page.

  • Response: List<ReleaseDto> where each release carries its title, artist, genre, release date, medium, and track count.
  • Returns 200 with the release list on success. Returns 500 on query error.

GET api/track/genres (unauthenticated)

Returns distinct non-null genres with per-genre track counts. Public browse data, same auth posture as GET api/track/page.

  • Response: A collection of genre strings with track counts.
  • Returns 200 on success. Returns 500 on query error.

GET api/track/random (unauthenticated)

Picks one track at random from the full library and returns its metadata. Public, same auth posture as GET api/track/page.

  • Response: A single TrackDto selected uniformly at random.
  • Returns 200 on success. Returns 404 if the library is empty (a valid state). Returns 500 on query error.

GET api/track/{trackId}/waveform (unauthenticated)

Returns the stored waveform loudness profile for a track as base64-encoded bytes. Public listener data, same auth posture as GET api/track/{trackId}.

  • Route parameter trackId (string): the entry id (TrackEntity.EntryKey).
  • Response: WaveformProfileDto with BucketCount (number of loudness buckets) and Data (base64-encoded byte array).
  • Returns 200 on success. Returns 404 if no profile is stored (existing tracks may predate profiling, or computation failed at upload — the frontend falls back to a flat seekbar). Returns 500 on vault error.

POST api/track/{trackId}/waveform ([ApiKeyAuthorize])

Admin backfill: computes and stores a waveform profile for an existing track from its vault audio.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter trackId (string): the entry id (TrackEntity.EntryKey).
  • Streams the vault audio in bounded ≤80 KB chunks (no whole-file load) via WaveformProfileService.ComputeAndStoreProfileStreamingAsync. Tri-state result: null = no vault audio → 404; false = audio present but not WAV-decodable or vault write failed → 500; true = stored → 200.
  • Returns 200 on success. Returns 404 if no audio is stored under that key. Returns 500 if WAV decoding or vault write fails.

GET api/track/{trackId}/waveform/high-res (unauthenticated)

Track-cardinal high-res datum fetch. Returns the per-track duration-derived high-res waveform datum (~333 samples/sec) from the track-waveforms vault. This is the live read path for the WaveformVisualizer bridge — the release-level mix waveform endpoint is a caller-less legacy delegate.

  • Route parameter trackId (string): the entry id (TrackEntity.EntryKey).
  • Response: WaveformProfileDto with BucketCount and Data (base64).
  • Returns 200 on success. Returns 404 if no high-res datum is stored (graceful — not-yet-backfilled tracks fall back to no visualizer data). Returns 500 on vault error.

POST api/track/{trackId}/waveform/high-res ([ApiKeyAuthorize])

Server-side trigger: compute and store the per-track high-res datum for any track from its vault audio, keyed by EntryKey in the track-waveforms vault. Drives the CMS per-row "Generate high-res" action and the CMS batch backfill action. Generalised off Mix-only in Phase 12.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter trackId (string): the entry id (TrackEntity.EntryKey).
  • Reads the duration from vault index metadata (no audio body load), then streams the vault audio in bounded chunks via WaveformProfileService.ComputeAndStoreHighResStreamingAsync. Tri-state result: null = no vault audio → 404; false = audio present but not WAV-decodable or vault write failed → 500; true = stored → 200.
  • Returns 200 on success. Returns 404 if no audio stored under that key. Returns 500 on compute/storage failure.

GET api/track/meta/by-key/{entryKey} (unauthenticated)

Single track metadata by vault entry key (EntryKey). Unauthenticated, reachable through the public proxy.

  • Route parameter entryKey (string): the TrackEntity.EntryKey.
  • Response: TrackDto for the matching track.
  • Returns 200 on success. Returns 404 if not found. Returns 500 on query error.

GET api/track/waveform-status ([ApiKeyAuthorize])

Admin backfill view: returns every track with flags indicating whether each waveform type is stored. Used by the CMS track list to flag tracks needing waveform computation.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Response: List<WaveformStatusDto> with TrackId, EntryKey, TrackName, HasProfile (bool — 512-bucket player-bar seeker profile in waveform-profiles vault), and HasHighRes (bool — duration-derived high-res visualizer datum in track-waveforms vault).
  • Returns 200 on success. Returns 500 on query error.

GET api/track/opus-status ([ApiKeyAuthorize])

Admin Post-Processing view: returns every track with a flag indicating whether it has a complete Opus artifact (both audio AND seek/setup sidecar present in the track-opus vault). Used by the CMS to show the Backfill-Opus badge and to poll per-track Post-Processing status after an upload. Mirrors the shape and auth posture of GET api/track/waveform-status.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Response: List<OpusStatusDto> with TrackId, EntryKey, TrackName, and HasOpus (bool — true only when both the Opus audio entry and the seek/setup sidecar entry are present in track-opus; a half-derived track counts as incomplete).
  • HasOpus is resolved via TrackFormatResolver.HasOpusAsync — an index-only existence check (MediaVault.HasIndexEntry for both entries; no file-body load). The endpoint loops over the whole catalogue, so a body load per track would stream the full library sequentially; the index lookup costs zero disk reads per track.
  • Returns 200 on success. Returns 500 on query error.

POST api/track/duration/backfill ([ApiKeyAuthorize])

Admin backfill: for every track whose DurationSeconds SQL column is still null, reads the AudioBinary.Duration from the vault and writes it to SQL. Idempotent — a re-run only touches still-null rows; rows that already have a value are skipped.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • No request body.
  • Calls UnifiedTrackService.BackfillDurationsAsync. Lives on TrackController in the literal-route block (before {trackId} routes, so the segment is never treated as a trackId).
  • Response: { updated: int, skipped: int } — counts of rows written vs. already-populated rows bypassed.
  • Returns 200 on success. Returns 500 if the backfill operation fails.

GET api/track/release/exists ([ApiKeyAuthorize])

Upload-form pre-flight: checks whether a release with the given (title, artist) already exists in the catalogue. Returns the matching ReleaseDto (so the caller can name it in a block message) or 404 when none exists. Uses the same GetReleaseByTitleAndArtist read the upload CREATE-path duplicate guard uses, so the pre-flight and the server backstop agree on the match by construction (exact ordinal comparison, soft-deleted rows excluded).

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Query parameters:
    • title (string, required): the release title to check.
    • artist (string, required): the artist name to check.
  • Declared as a literal 2-segment route ("release/exists") before the parameterized {trackId} route and distinct from "release/{id:long}" (different segment shape) — no routing ambiguity.
  • Returns 200 with ReleaseDto JSON if a match exists. Returns 400 if either query parameter is missing or whitespace. Returns 404 if no match. Returns 500 on query error.

DELETE api/track/release/{id:long} ([ApiKeyAuthorize])

Soft-delete a release row. Used by the albums browser to remove an orphaned release (one with no live tracks).

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL release ID.
  • Calls ITrackService.DeleteRelease.
  • Returns 200 on success. Returns 500 on deletion error.

PUT api/track/{trackId} ([ApiKeyAuthorize])

Authenticated endpoint. Writes pre-processed audio bytes to the tracks vault.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter trackId (string): the entry id to store under.
  • Body: AudioBinaryDto (base64 buffer + size + mime + duration + bitrate). This endpoint receives an already-processed audio DTO, not a raw WAV file.
  • Validates MIME type (rejects unsupported types with .bin sentinel). Delegates to FileDatabase.RegisterResourceAsync.
  • Rarely used in production (the CLI calls FileDatabase.RegisterResourceAsync directly). Exists for potential future web-side intake paths.
  • Returns 200 on success, 401 if ApiKey invalid, 400 if MIME invalid.

POST api/track/upload ([ApiKeyAuthorize])

Authenticated endpoint. Accepts a raw audio file upload (.wav, .mp3, .flac) + metadata as multipart/form-data, processes the file, stores it in the vault, and persists metadata to SQL. Returns the fully persisted TrackDto with Id populated.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Form fields:
    • audioFile (IFormFile, required): the audio bytes. File name must end in .wav, .mp3, or .flac.
    • trackName (string, required)
    • artist (string, required)
    • album (string, optional)
    • genre (string, optional)
    • releaseDate (string, optional, format YYYY-MM-DD)
    • createdByUserId (long, required): audit trail — who uploaded this track.
    • releaseType (string, optional): enum ReleaseType (e.g., Single, Album, EP). Defaults to Single if null or unrecognized.
    • medium (string, optional): enum ReleaseMedium (e.g., Cut, Mix, Session). Defaults to Cut if null or unrecognized.
    • trackNumber (int?, optional): track position within the release (1-based). Defaults to 1 if ≤ 0 or null.
    • releaseId (long?, optional): the SQL release ID to attach this track to. Omit (null) on the first row of a submit — this is the CREATE path, which mints a new release and blocks a pre-existing (title, artist) with 409. Set to the release id returned by row 1 for rows 2..N of a within-batch multi-track Cut — this is the ATTACH path, which skips the (title, artist) pre-existing check and attaches directly to the already-created release after validating the id matches the natural key. The upload form is create-only; appending to a pre-existing release must go through the edit tools.
  • The upload stream is copied to a staging file under the upload staging directory (resolved from Upload:StagingPath, defaulting to a staging subdirectory under the FileDatabase vault path — on the data disk, never Path.GetTempPath()) with the appropriate extension (.wav, .mp3, or .flac). The audio processor reads from disk and requires the correct extension for format detection. The staging file is always deleted in a finally block — success or failure. The framework's own multipart file-section buffer is relocated off the system temp mount too: Startup.ConfigureDomainServices sets the ASPNETCORE_TEMP env var to the same staging directory, so neither on-disk copy of a large body lands on /tmp (a small RAM-backed tmpfs on the Linux host).
  • [RequestSizeLimit(~1.86 GB / 2_000_000_000)] + [RequestFormLimits(MultipartBodyLengthLimit = 2_000_000_000)] lift the per-request ceiling above the framework default (~28 MB) so production-sized files are accepted. The body is streamed to the staging file, not buffered in memory.
  • UnifiedTrackService.UploadAsync orchestrates: release resolution (CREATE or ATTACH, see above) → TrackContentService.AddTrackAsync (format-agnostic streaming vault write via router — audio is streamed to the vault via the ProcessedAudio plan, no whole-file buffer — Wave 1 OOM fix) → TrackManager (SQL persist with createdByUserId) → TryStoreWaveformDatumsAsync (best-effort: reads duration from vault index metadata, then computes both waveform datums from a single bounded streaming pass over the stored audio — Wave 2 OOM fix). Release resolution runs the cardinality guard on both paths and, on the CREATE path, calls ITrackService.FindOrCreateRelease (returns (ReleaseDto Release, bool WasCreated)); if WasCreated is false, a concurrent upload won the race and the request is rejected as a duplicate rather than silently attaching.
  • Returns 200 with the persisted TrackDto JSON (Id populated) on success. Returns 400 for missing/invalid form fields or unsupported audio format. Returns 409 for two distinct domain conditions: a pre-existing (title, artist) duplicate on the CREATE path (DUPLICATE_RELEASE: marker → 409 Conflict), or a track-number conflict within the release (CARDINALITY_VIOLATION: marker → 409 Conflict). Returns 500 if processing fails.

DELETE api/track/{id:long} ([ApiKeyAuthorize])

Authenticated endpoint. Removes a track: SQL row first, then vault entry. UnifiedTrackService owns the ordering.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL track ID (not EntryKey).
  • Calls UnifiedTrackService.DeleteAsync, which: looks up SQL row → deletes SQL row → deletes vault entry via EntryKey.
  • Returns 200 on success, 404 if track not found, 500 if deletion fails.

POST api/track/{id:long}/replace-audio ([ApiKeyAuthorize])

Authenticated endpoint. Accepts a raw audio file upload (.wav, .mp3, .flac) as multipart/form-data and replaces the existing track's vault bytes in place, preserving the track id, EntryKey, SQL row (metadata/release/position), and release membership. Both waveform datums (512-bucket seeker profile + high-res visualizer datum) are regenerated after the swap; waveform regen failure is logged and swallowed — it does not fail the replace.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL track ID.
  • Form field audioFile (IFormFile, required): the replacement audio bytes. File name must end in .wav, .mp3, or .flac.
  • [RequestSizeLimit(~1.86 GB / 2_000_000_000)] + [RequestFormLimits(MultipartBodyLengthLimit = 2_000_000_000)] mirror the upload ceiling. The body is streamed to a staging file under the upload staging directory (the same off-/tmp data-disk location as the upload path; correct extension preserved for the audio processor), always deleted in a finally block.
  • Calls UnifiedTrackService.ReplaceAudioAsync, which: looks up SQL row by id → calls TrackContentService.ReplaceTrackAudioAsync(entryKey, tempFilePath) (streams new audio into the vault under the existing EntryKey via the ProcessedAudio plan, no whole-file buffer — Wave 1 OOM fix; removes the stale backing file only on a cross-format swap, after the new write succeeds) → regenerates both waveform datums from a single bounded streaming pass over the freshly stored audio via TryStoreWaveformDatumsAsync (best-effort; a datum failure is logged and swallowed — Wave 2 OOM fix) → writes the new audio's duration to DurationSeconds via ITrackService.SetDuration (unconditional overwrite; a failure is surfaced, not swallowed, to prevent derived aggregates like MixRuntimeSeconds from silently going stale).
  • Returns 200 on success. Returns 400 if the file is missing or the format is unsupported. Returns 404 if the track id is not found. Returns 500 if vault processing fails.

GET api/track/page (unauthenticated)

Paged metadata list from SQL with optional filtering. Public browser data, same auth posture as GET api/track/{id}.

  • Query parameters:
    • page (int, optional, default 1): 1-based page number.
    • pageSize (int, optional, default 20): tracks per page.
    • sortColumn (string, optional): sort field. Supported: "TrackName", "Artist", "Album", "Genre", "ReleaseDate", "TrackNumber". Defaults to Id.
    • sortDescending (bool, optional, default false): sort direction.
    • q (string, optional): search text filter (matches track name / artist).
    • album (string, optional): album title filter.
    • genre (string, optional): genre filter.
    • releaseId (long?, optional): release ID filter (authoritative join; preferred over album title).
  • Calls ITrackService.GetPaged with optional TrackFilter (null if all filter params are empty).
  • Returns 200 with PagedResult<TrackDto> JSON (Items, TotalCount, PageNumber, PageSize). Returns 500 on query error.

GET api/track/meta/{id:long} ([ApiKeyAuthorize])

Authenticated endpoint. Single track metadata from SQL by ID.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL track ID.
  • Calls ITrackService.GetById, which returns the track as TrackDto or null.
  • Returns 200 with TrackDto JSON on success. Returns 404 if not found. Returns 500 on query error.

PUT api/track/meta/{id:long} ([ApiKeyAuthorize])

Authenticated endpoint. Updates track metadata in SQL. EntryKey (the vault link) is immutable.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL track ID.
  • Body: UpdateTrackMetadataRequest with fields:
    • TrackName (string, required)
    • Artist (string, required)
    • Album (string?, optional)
    • Genre (string?, optional)
    • ReleaseDate (DateOnly?, optional)
    • ImagePath (string?, tri-state: null = no change, "" = clear, value = set)
    • ReleaseType (ReleaseType?, optional): updates the linked release if present; null = no change.
    • Medium (ReleaseMedium?, optional): updates the linked release if present; null = no change. When Medium is set to non-Cut, also resets ReleaseType to Single (the DB default) to avoid stale studio-format values.
    • TrackNumber (int?, optional): track position within the release; validated > 0 when provided.
  • Looks up SQL row by ID, updates the provided fields, and persists via ITrackService.Update. Track-cardinal fields (TrackName, TrackNumber) update the track row; release-cardinal fields (Artist, Album, Genre, ReleaseDate, ImagePath, ReleaseType, Medium) update the linked release (if present; loose tracks ignore these).
  • Returns 200 on success. Returns 400 if TrackNumber ≤ 0 (when provided). Returns 404 if track not found. Returns 500 on update error.

The image endpoints (two endpoints)

POST api/image/upload ([ApiKeyAuthorize])

Authenticated endpoint. Accepts an image file upload, stores it in the images vault, and returns the entry key.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Form field image (IFormFile, required): the image bytes (PNG, JPEG, or other format supported by ImageProcessor). Maximum file size 50 MB.
  • Calls FileDatabase.RegisterResourceAsync("images", entryKey, imageBinary) where imageBinary is produced by ImageProcessor (computes aspect ratio from headers, defaults 1.0 for unsupported formats).
  • Returns 200 with JSON { entryKey } on success. Returns 400 for missing file. Returns 500 if processing or vault operations fail.

GET api/image/{entryKey} (unauthenticated)

Returns image bytes from the images vault.

  • Route parameter entryKey (string): the entry id inside the images vault.
  • Streams the image file directly from disk without buffering.
  • Returns 404 if image not found. Returns 500 if vault operations fail (with error swallowing — the vault returns null).

The release endpoints

GET api/release (unauthenticated)

Paged release list, optionally filtered to one medium. Public browse data, same auth posture as GET api/track/page.

  • Query parameters:
    • medium (string, optional): enum ReleaseMedium (e.g., Cut, Mix, Session). If provided, only releases of that medium are returned; the matching medium's metadata satellite is populated, others are null.
    • page (int, optional, default 1): 1-based page number.
    • pageSize (int, optional, default 20): releases per page.
    • sortColumn (string, optional): sort field (typically "Title").
    • 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/{entryKey} (unauthenticated)

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 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/{entryKey}/mix/waveform (unauthenticated — caller-less legacy delegate)

Legacy endpoint: formerly served the high-res waveform datum for a Mix release from the mix-waveforms vault. No longer called by the client — the live fetch path is now the track-cardinal GET api/track/{trackId}/waveform/high-res (Phase 12). The endpoint is retained in the API but has no active callers. UnifiedReleaseService.TriggerMixWaveformAsync now delegates to WaveformProfileService.ComputeAndStoreHighResAsync (the same shared seam used by the upload path and the generalized CMS generate action).

  • 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.

POST api/release/{id:long}/mix/waveform ([ApiKeyAuthorize])

Server-side trigger: fetch the Mix's track audio from the vault, compute the duration-derived high-res datum, store it in the track-waveforms vault under the track's EntryKey, and link it via MixMetadata.WaveformEntryKey. Delegates to WaveformProfileService.ComputeAndStoreHighResStreamingAsync (streams the vault audio in bounded chunks, no whole-file buffer) — the same shared seam used by the upload path and the generalized CMS generate action. No request body.

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL release ID.
  • Calls UnifiedReleaseService.TriggerMixWaveformAsync.
  • Returns 200 on success. Returns 404 if the release is missing, is not a Mix, has no track, or the track audio is not stored. Returns 500 on compute/storage failure.

POST api/release/{id:long}/session/hero-image ([ApiKeyAuthorize])

Stores a hero image in the images vault and links it via SessionMetadata.HeroImageEntryKey. The release must be a Session medium (enforced in the service).

  • Header ApiKey: required. Validated by ApiKeyAuthenticationMiddleware.
  • Route parameter id (long): the SQL release ID.
  • Form field image (IFormFile, required): the image bytes (PNG, JPEG, or other format supported by ImageProcessor). Maximum file size 50 MB.
  • Validates MIME type (rejects unsupported types with .bin sentinel). Calls UnifiedReleaseService.SetHeroImageAsync.
  • Returns 200 on success. Returns 400 for missing file or unsupported MIME type. Returns 404 if release not found. Returns 500 on processing or vault failure.

The stats endpoints

GET api/stats/home (unauthenticated)

Aggregate figures behind the public home hero stat row (NowPlayingStats). A single read returns everything the three cards need so the client makes one round-trip. Public, same auth posture as GET api/track/page.

  • Response: HomeStatsDto with:
    • CutTrackCount (int): total non-deleted tracks on Cut-medium releases.
    • CutReleaseTypeCounts (List<CutReleaseTypeCount>): per-ReleaseType Cut release counts; zero-count types are absent (zero-suppressed server-side).
    • MixReleaseCount (int): total non-deleted Mix-medium releases.
    • MixRuntimeSeconds (double): sum of DurationSeconds across all non-deleted tracks on Mix releases (null durations count as 0).
    • TotalPlays (long): site-wide total plays — sum of every play_counter row's bucket columns (PartialCount + SampledCount + CompleteCount), all-time (Phase 16). Zero until the play-telemetry migration is applied.
    • UniqueListeners (int): site-wide distinct anonymous listeners — distinct non-null anon_id across all play events, all-time (Phase 16). Zero until the migration is applied.
  • StatsController injects both ITrackService (track-domain aggregation — Cuts/Mixes cards) and IEventService (event-domain aggregation — Plays card). Neither domain reaches into the other's tables; the controller is the thin composition seam. Track-domain aggregation comes from TrackRepository.GetHomeStatsAsync via ITrackService.GetHomeStats; play/listener figures come from IEventService.GetTotalPlayCount and IEventService.GetDistinctListenerCount (Phase 16 wave 16.5). Play/listener reads are best-effort: a telemetry failure or not-yet-applied migration leaves those fields at 0 rather than failing the whole endpoint with 500.
  • Returns 200 on success. Returns 500 if the track-domain aggregation fails.

The event endpoints (Phase 16 anonymous telemetry)

Both endpoints are unauthenticated and rate-limited by the "events" fixed-window policy (30 requests / 60 s per IP, keyed on Connection.RemoteIpAddress after UseForwardedHeaders() resolves XFF). Returns 202 Accepted — fire-and-forget contract; the sendBeacon client ignores the response. Controller: EventController.

POST api/event/play (unauthenticated, rate-limited)

Records an anonymous play event. Client sends the track EntryKey, a completion bucket, and an optional anonId (wave 16.3); server-side release resolution joins track→release at write time (D4). The anonId is length-clamped server-side: whitespace-only / empty / null collapses to null (valid anonId-less event); a token longer than 64 chars returns 400 rather than being truncated (truncation would collide distinct listeners).

  • Body (PlayEventDto): { "trackEntryKey": "...", "bucket": "partial"|"sampled"|"complete", "anonId": "..." } (anonId optional — omitted when null).
  • Validates: non-empty trackEntryKey; bucket must be a defined PlayBucket enum value.
  • Delegates to IEventService.RecordPlay, which appends to play_event and bumps play_counter.
  • Returns 202 on success. Returns 400 for missing/invalid fields. Returns 429 when the rate limit is exceeded. Returns 500 on a write failure (logged; beacon ignores it).

POST api/event/share (unauthenticated, rate-limited)

Records an anonymous share event (a clipboard write from SharePopover).

  • Body (ShareEventDto): { "targetKey": "...", "targetType": "track"|"release", "channel": "link"|"embed", "anonId": "..." } (anonId optional — omitted when null; same length-clamp as the play endpoint).
  • Validates: non-empty targetKey; defined ShareTargetType and ShareChannel enum values; anonId ≤ 64 chars (reject-not-truncate).
  • Delegates to IEventService.RecordShare, which appends to share_event.
  • Returns 202 on success. Returns 400 for missing/invalid fields. Returns 429 on rate limit. Returns 500 on write failure.

ApiKey middleware behaviour

ApiKeyAuthenticationMiddleware runs on every request but only enforces on endpoints with [ApiKeyAuthorize] metadata.

  • Reads header ApiKey.
  • Compares against ApiKeySettings.ApiKey from environment/apikey.json.
  • Returns 401 with body "API Key was not provided" or "Unauthorized client" if validation fails.
  • Endpoints without [ApiKeyAuthorize] skip the check entirely (e.g., GET api/track/{id} is unauthenticated).

CORS configuration

CorsSettings.AllowedOrigins is required — the app throws on startup if missing. Policy is named ContentApiPolicy:

  • AllowCredentials()
  • AllowAnyMethod()
  • AllowAnyHeader()

Configured in Startup.ConfigureDomainServices(), applied to all endpoints via UseCors().

Forwarded headers

Enabled only in Production mode (via if (app.Environment.IsProduction())). This differs from DeepDrftWeb, which enables them always. Be aware when debugging proxy issues.

UseForwardedHeaders() processes X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host so the app knows its real client IP and scheme when sitting behind nginx.

Startup wiring (Startup.ConfigureDomainServices + Program.cs)

In Startup.ConfigureDomainServices (FileDatabase + binary services):

  1. Load environment/filedatabase.json via CredentialTools.ResolvePathOrThrow("filedatabase", ...) and bind FileDatabaseSettings.
  2. Await FileDatabase.FromAsync(VaultPath) to load or create the database.
  3. Register FileDatabase as singleton.
  4. Ensure the tracks vault exists (type MediaVaultType.Audio, created on first boot if missing).
  5. Ensure the images vault exists (type MediaVaultType.Image, created on first boot if missing) via InitializeImageVault. 5a. Ensure the track-waveforms vault exists (type MediaVaultType.Media, created on first boot if missing) — holds per-track high-res visualizer datum keyed by TrackEntity.EntryKey.
  6. Register singletons: AudioProcessor, ImageProcessor, TrackService (the DeepDrftContent version for vault operations), WaveformProfileService. 6a. Upload staging directory — resolve and create the on-disk staging directory (read Upload:StagingPath; if empty, default to a staging subdirectory under the FileDatabase vault path via Startup.ResolveStagingPath). Set the ASPNETCORE_TEMP env var to this directory before any request is served, relocating the framework's multipart file-section buffer (Layer 1) off the system temp mount. Register UploadStagingDirectory as a singleton so both UploadTrack and ReplaceAudio in TrackController stage to the same data-disk location (Layer 2) and never write to /tmp (a small RAM-backed tmpfs on the Linux host).

In Program.cs (SQL + AuthBlocks + wiring):

  1. Load environment/connections.json via CredentialTools.ResolvePathOrThrow("connections", ...) — contains both DefaultConnection (SQL metadata) and Auth (AuthBlocks Identity database).
  2. AuthBlocks startup: Call builder.Services.AddAuthBlocks(options => { ... }) with Auth connection string and settings from AuthBlocks:* config keys. Load environment/authblocks.json for JWT secret, email sender creds, and admin seed creds. This registers JWT bearer auth scheme and EF Identity.
  3. Register DbContext<DeepDrftContext> (scoped) with DefaultConnection from config.
  4. Register scoped: TrackRepository, TrackManager, ITrackService (factory resolves to TrackManager), UnifiedTrackService.
  5. After app.Build(): Call await app.Services.UseAuthBlocksStartupAsync() to apply migrations and seed roles + admin user to the Auth database.
  6. Configure forwarded headers (production-only) for reverse proxy support.
  7. Load environment/apikey.json and register API key middleware.
  8. Configure CORS policy (ContentApiPolicy): AllowAnyMethod, AllowAnyHeader, AllowCredentials, specific origins from config (includes DeepDrftManager origin for cross-origin auth calls).
  9. Map AuthBlocks endpoints: Call app.MapAuthBlocks() to mount /api/auth/* and /api/users/* endpoints. Ensure app.UseAuthentication() and app.UseAuthorization() are in the middleware pipeline (required for JWT bearer auth).
  10. Verify ApiKey middleware ordering: it must not interfere with JWT middleware. ApiKey gates only [ApiKeyAuthorize]-decorated track endpoints; JWT gates AuthBlocks endpoints.

The singleton FileDatabase is thread-safe for reads. Writes are atomic at the vault level (index updates are serialised). The IndexWatcher reloads the vault index if an external process (e.g., CLI) writes to it, so a long-running web host stays consistent. SQL services are scoped (DbContext not thread-safe).

OpenAPI

Mapped in Development only. Swagger UI at /swagger for testing endpoints locally.

Configuration files

  • appsettings.json: Logging, hosting, CORS, AuthBlocks, and non-secret upload config. Does not contain secrets.
    • Logging: standard ASP.NET structure.
    • Upload:StagingPath: non-secret string. Empty default → a staging subdirectory under the FileDatabase vault path (on the data disk). Override to an absolute path when the vault default is not suitable. Consumed by Startup.ResolveStagingPath.
    • CorsSettings.AllowedOrigins: array of origin URLs allowed to call the API (required; throws on startup if missing).
    • AuthBlocks:Jwt:Issuer, AuthBlocks:Jwt:Audience: JWT validation settings (loaded from environment/authblocks.json).
  • environment/filedatabase.json (required, loaded via CredentialTools, not in repo):
    {
      "FileDatabaseSettings": {
        "VaultPath": "../Database/Vaults"
      }
    }
    
  • environment/apikey.json (required at runtime, loaded via CredentialTools, not in repo):
    {
      "ApiKeySettings": {
        "ApiKey": "your-secret-key"
      }
    }
    
  • environment/connections.json (required, loaded via CredentialTools, not in repo):
    {
      "ConnectionStrings": {
        "DefaultConnection": "Host=localhost;Database=deepdrft;Username=postgres;Password=...",
        "Auth": "Host=localhost;Database=deepdrft_auth;Username=postgres;Password=..."
      }
    }
    
  • environment/authblocks.json (required, loaded via CredentialTools, not in repo):
    {
      "AuthBlocks": {
        "Jwt": {
          "Secret": "your-signing-secret-min-32-chars",
          "Issuer": "https://deepdrft.api",
          "Audience": "https://deepdrft.com"
        },
        "Email": {
          "Host": "smtp.provider.com",
          "Token": "your-smtp-password"
        },
        "Admin": {
          "UserName": "admin",
          "Email": "admin@deepdrft.com",
          "Password": "initial-admin-password"
        },
        "SupportEmail": "support@deepdrft.com"
      }
    }
    

Development commands

# Run the API host (default https://localhost:5002)
dotnet run --project DeepDrftAPI

# Watch during development
dotnet watch run --project DeepDrftAPI

# Build
dotnet build DeepDrftAPI

# Test track endpoints (requires API key in environment/apikey.json)
curl -H "ApiKey: your-secret-key" -X GET https://localhost:5002/api/track/page \
  -H "Accept: application/json"

curl https://localhost:5002/api/track/test-entry-key

# Test auth endpoints (AuthBlocks API)
curl -X POST https://localhost:5002/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@deepdrft.com","password":"initial-admin-password"}'

Important patterns

  • Result types: Controllers return ActionResult<T>. Service calls return Result or ResultContainer<T> from NetBlocks. The controller checks Success and returns 200/4xx/5xx accordingly.
  • Error swallowing: FileDatabase operations return null or false on failure. The controller surfaces this as 500. Never throw — check return values.
  • Async/await: All operations are async.
  • Vault operations: Always use the injected FileDatabase singleton. Never construct a new one — it has the IndexWatcher and is the source of truth.

The FileDatabase import

See DeepDrftContent/CLAUDE.md for the FileDatabase API and semantics. This host only provides the HTTP surface over it and the AuthBlocks authority.

When working with this project, focus on the HTTP surface (controllers, middleware, CORS, forwarded headers, AuthBlocks wiring) and the dual-database orchestration via UnifiedTrackService. New domain logic goes in DeepDrftContent (FileDatabase) or DeepDrftData (SQL). Keep this host focused on HTTP boundaries and wiring.