docs: update CLAUDE.md files to reflect Range header seek, remove WavOffsetService references

This commit is contained in:
daniel-c-harvey
2026-06-09 07:41:38 -04:00
parent 0bd7e6904d
commit 79a015f60a
5 changed files with 14 additions and 32 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ DeepDrftHome is a **net10.0** solution consisting of ten projects implementing a
- **DeepDrftShared.Client**: Razor Class Library. Shared Blazor components consumed by both `DeepDrftPublic` and `DeepDrftManager` for consistency across public and admin surfaces. - **DeepDrftShared.Client**: Razor Class Library. Shared Blazor components consumed by both `DeepDrftPublic` and `DeepDrftManager` for consistency across public and admin surfaces.
- **DeepDrftData**: Class library. EF Core domain logic: `DeepDrftContext`, `TrackConfiguration`, `Migrations`, `TrackRepository`, `TrackService`, `TrackManager`. Consumed by `DeepDrftAPI` and tests. - **DeepDrftData**: Class library. EF Core domain logic: `DeepDrftContext`, `TrackConfiguration`, `Migrations`, `TrackRepository`, `TrackService`, `TrackManager`. Consumed by `DeepDrftAPI` and tests.
- **DeepDrftAPI**: ASP.NET Core host. Dual-database authority (SQL metadata + FileDatabase binary). AuthBlocks API host (owns registration, migration/seed, JWT endpoints). Seven track endpoints: `GET api/track/{id}` unauthenticated streaming; `PUT api/track/{id}` vault write (ApiKey); `POST api/track/upload` upload + SQL persist (ApiKey); `DELETE api/track/{id:long}` SQL delete + vault remove (ApiKey); `GET api/track/page` paged metadata list (unauthenticated); `GET api/track/meta/{id:long}` single metadata (ApiKey); `PUT api/track/meta/{id:long}` metadata update (ApiKey). - **DeepDrftAPI**: ASP.NET Core host. Dual-database authority (SQL metadata + FileDatabase binary). AuthBlocks API host (owns registration, migration/seed, JWT endpoints). Seven track endpoints: `GET api/track/{id}` unauthenticated streaming; `PUT api/track/{id}` vault write (ApiKey); `POST api/track/upload` upload + SQL persist (ApiKey); `DELETE api/track/{id:long}` SQL delete + vault remove (ApiKey); `GET api/track/page` paged metadata list (unauthenticated); `GET api/track/meta/{id:long}` single metadata (ApiKey); `PUT api/track/meta/{id:long}` metadata update (ApiKey).
- **DeepDrftContent**: Class library. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), `WavOffsetService`, `AudioProcessor`, content-side `TrackService`. Consumed by hosts and tests. - **DeepDrftContent**: Class library. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), `AudioProcessor`, content-side `TrackService`. Consumed by hosts and tests.
- **DeepDrftModels**: Shared contracts. `TrackEntity`, `TrackDto`, `PagingParameters<T>`, `PagedResult<T>`. Every project references this. - **DeepDrftModels**: Shared contracts. `TrackEntity`, `TrackDto`, `PagingParameters<T>`, `PagedResult<T>`. Every project references this.
- **DeepDrftTests**: NUnit test suite. Comprehensive FileDatabase tests (vault creation, media storage, indexing, factory patterns, utilities). Integration-focused with temp-directory test isolation. - **DeepDrftTests**: NUnit test suite. Comprehensive FileDatabase tests (vault creation, media storage, indexing, factory patterns, utilities). Integration-focused with temp-directory test isolation.
@@ -70,7 +70,7 @@ The player is not fetch-then-play:
2. `StreamingAudioPlayerService` reads in adaptive 1664 KB chunks, pushes each via `AudioInteropService.processStreamingChunk`. 2. `StreamingAudioPlayerService` reads in adaptive 1664 KB chunks, pushes each via `AudioInteropService.processStreamingChunk`.
3. TypeScript `StreamDecoder` parses WAV header, decodes chunks to `AudioBuffer`s. `PlaybackScheduler` schedules them on a Web Audio graph. 3. TypeScript `StreamDecoder` parses WAV header, decodes chunks to `AudioBuffer`s. `PlaybackScheduler` schedules them on a Web Audio graph.
4. Playback starts as soon as a min buffer is queued; UI duration from parsed header (not waiting for full file). 4. Playback starts as soon as a min buffer is queued; UI duration from parsed header (not waiting for full file).
5. **Seek beyond buffer**: if seek target is past what's decoded, client issues `GET api/track/{id}?offset={byteOffset}`. Server's `WavOffsetService` block-aligns offset, synthesises a fresh 44-byte WAV header, streams `[new header][data from offset]`. Player tears down and re-initialises decoder for the new stream. 5. **Seek beyond buffer**: if seek target is past what's decoded, client issues `GET api/track/{id}` with `Range: bytes={byteOffset}-`. Server streams raw bytes from that file-absolute offset with a `206 Partial Content` response. Player retains the parsed WAV header and feeds the raw PCM continuation into the existing decode pipeline.
Keep this seam clean — it is the most architecturally load-bearing part of the playback path. Keep this seam clean — it is the most architecturally load-bearing part of the playback path.
+8 -9
View File
@@ -22,21 +22,20 @@ Dual-database authority for tracks (SQL metadata + FileDatabase binary) and imag
## What does NOT live here anymore ## What does NOT live here anymore
- `FileDatabase/`, `Processors/`, media models (`AudioBinary`, `ImageBinary`, etc.), `WavOffsetService` — all in `DeepDrftContent` (class library). - `FileDatabase/`, `Processors/`, media models (`AudioBinary`, `ImageBinary`, etc.) — all in `DeepDrftContent` (class library).
- EF Core context and repository — in `DeepDrftData`. - 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). - **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 (seven endpoints) ## The endpoint surface (seven endpoints)
### GET api/track/{trackId}?offset=0 (unauthenticated) ### GET api/track/{trackId} (unauthenticated)
Returns the WAV bytes from the `tracks` vault with optional offset support. Returns the WAV bytes from the `tracks` vault with HTTP Range support.
- **Route parameter `trackId`** (string): the entry id inside the `tracks` vault (i.e. `TrackEntity.EntryKey`). - **Route parameter `trackId`** (string): the entry id inside the `tracks` vault (i.e. `TrackEntity.EntryKey`).
- **Query parameter `offset`** (optional, default 0): byte position to start streaming from. - **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.
- If `offset == 0`: streams the entire file directly from disk without buffering (so 100 MB WAVs do not force 100 MB LOH allocations per request). - Streams the file directly from disk with `enableRangeProcessing: true`, supporting both full-file and partial-range requests without synthesizing WAV headers or buffering.
- If `offset > 0`: `WavOffsetService.CreateOffsetStream` block-aligns the offset and synthesises a fresh 44-byte WAV header so the response is a valid standalone WAV starting from that byte position. This is load-bearing for seek-beyond-buffer — the player asks for a new stream at the offset it wants to seek to, gets back a valid WAV that starts there, and tears down/re-initialises the decoder. - Returns 200 for full-file requests, 206 for Range requests, 404 if track not found, 500 if vault operations fail (with error swallowing — the vault returns `null`).
- Returns 404 if track not found. Returns 500 if vault operations fail (with error swallowing — the vault returns `null`).
### PUT api/track/{trackId} ([ApiKeyAuthorize]) ### PUT api/track/{trackId} ([ApiKeyAuthorize])
@@ -161,7 +160,7 @@ Configured in `Startup.ConfigureDomainServices()`, applied to all endpoints via
3. Register `FileDatabase` as singleton. 3. Register `FileDatabase` as singleton.
4. Ensure the `tracks` vault exists (type `MediaVaultType.Audio`, created on first boot if missing). 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`. 5. Ensure the `images` vault exists (type `MediaVaultType.Image`, created on first boot if missing) via `InitializeImageVault`.
6. Register singletons: `WavOffsetService`, `AudioProcessor`, `ImageProcessor`, `TrackService` (the `DeepDrftContent` version for vault operations). 6. Register singletons: `AudioProcessor`, `ImageProcessor`, `TrackService` (the `DeepDrftContent` version for vault operations).
**In `Program.cs`** (SQL + AuthBlocks + wiring): **In `Program.cs`** (SQL + AuthBlocks + wiring):
@@ -252,7 +251,7 @@ dotnet build DeepDrftAPI
curl -H "ApiKey: your-secret-key" -X GET https://localhost:5002/api/track/page \ curl -H "ApiKey: your-secret-key" -X GET https://localhost:5002/api/track/page \
-H "Accept: application/json" -H "Accept: application/json"
curl https://localhost:5002/api/track/test-entry-key?offset=0 curl https://localhost:5002/api/track/test-entry-key
# Test auth endpoints (AuthBlocks API) # Test auth endpoints (AuthBlocks API)
curl -X POST https://localhost:5002/api/auth/login \ curl -X POST https://localhost:5002/api/auth/login \
+1 -17
View File
@@ -6,7 +6,7 @@ See the root `CLAUDE.md` for full architecture overview. This file covers what i
## One-line purpose ## One-line purpose
Binary-content domain logic. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), WAV stream-with-offset, audio processing, and the content-side track service. Consumed by `DeepDrftContent` (the host) and `DeepDrftCli` (the admin CLI). Binary-content domain logic. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), audio processing, and the content-side track service. Consumed by `DeepDrftContent` (the host) and `DeepDrftCli` (the admin CLI).
## Layout ## Layout
@@ -17,8 +17,6 @@ DeepDrftContent.Services/
│ ├── Models/ # Data models, DTOs, enums │ ├── Models/ # Data models, DTOs, enums
│ ├── Services/ # FileDatabase, MediaVault, IndexSystem, IndexWatcher │ ├── Services/ # FileDatabase, MediaVault, IndexSystem, IndexWatcher
│ └── Utils/ # StructuralMap, StructuralSet, FileUtils │ └── Utils/ # StructuralMap, StructuralSet, FileUtils
├── Audio/
│ └── WavOffsetService.cs # Byte offset → valid WAV stream
├── Processors/ ├── Processors/
│ └── AudioProcessor.cs # WAV file parsing, metadata extraction │ └── AudioProcessor.cs # WAV file parsing, metadata extraction
├── Constants/ ├── Constants/
@@ -76,19 +74,6 @@ public async Task<bool> RegisterResourceAsync(string vaultId, string entryId, Fi
**Callers must check return values.** Do not change this without a deliberate design pass — it's embedded in all FileDatabase tests and client code. **Callers must check return values.** Do not change this without a deliberate design pass — it's embedded in all FileDatabase tests and client code.
## WAV offset service
`WavOffsetService.CreateOffsetStream(buffer, byteOffset)`:
1. Parses the WAV header from the buffer.
2. Block-aligns the byte offset to the nearest block boundary (required for clean audio — misalignment causes clicks).
3. Synthesises a new 44-byte WAV header sized for the remaining data (from offset to EOF).
4. Returns a `MemoryStream` containing `[new header][data from offset]`.
Used by the content API to serve seek-beyond-buffer requests. The player asks for a new stream at the byte offset it wants to seek to; the server returns a valid WAV that starts there.
**Block alignment is critical.** Do not bypass it. The WAV fmt chunk tells you the block size; use it.
## Audio processor ## Audio processor
`AudioProcessor.ProcessWavFileAsync(filePath)`: `AudioProcessor.ProcessWavFileAsync(filePath)`:
@@ -141,7 +126,6 @@ Safety call to ensure the `tracks` vault exists (creates if missing). Called on
In `DeepDrftContent/Startup.ConfigureDomainServices()` and `DeepDrftCli/Program.cs`: In `DeepDrftContent/Startup.ConfigureDomainServices()` and `DeepDrftCli/Program.cs`:
```csharp ```csharp
services.AddSingleton<WavOffsetService>();
services.AddSingleton<FileDatabase>(/* from FileDatabase.FromAsync */); services.AddSingleton<FileDatabase>(/* from FileDatabase.FromAsync */);
services.AddScoped<AudioProcessor>(); services.AddScoped<AudioProcessor>();
services.AddScoped<TrackService>(); // DeepDrftContent.Services.TrackService services.AddScoped<TrackService>(); // DeepDrftContent.Services.TrackService
+3 -3
View File
@@ -33,7 +33,7 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
- Dark-mode services: `DarkModeServiceBase` (cookie name constant), `DarkModeCookieService` (JS cookie read/write). - Dark-mode services: `DarkModeServiceBase` (cookie name constant), `DarkModeCookieService` (JS cookie read/write).
- `Clients/`: HTTP API clients (both target DeepDrftAPI). - `Clients/`: HTTP API clients (both target DeepDrftAPI).
- `TrackClient`: SQL metadata API. Uses named `IHttpClientFactory` client `"DeepDrft.API"`. Sends `page` param (not `pageNumber`). Deserializes response as bare `PagedResult<TrackDto>` (not wrapped in ApiResultDto envelope). - `TrackClient`: SQL metadata API. Uses named `IHttpClientFactory` client `"DeepDrft.API"`. Sends `page` param (not `pageNumber`). Deserializes response as bare `PagedResult<TrackDto>` (not wrapped in ApiResultDto envelope).
- `TrackMediaClient`: Content API. Uses named `IHttpClientFactory` client `"DeepDrft.Content"`. Methods like `GetAudioStreamAsync(trackId, offset)``Stream`. - `TrackMediaClient`: Content API. Uses named `IHttpClientFactory` client `"DeepDrft.Content"`. Methods like `GetAudioStreamAsync(trackId, byteOffset?)``Stream` with optional Range header support for seek-beyond-buffer.
- `ViewModels/`: Component state. - `ViewModels/`: Component state.
- `TracksViewModel`: Scoped. Holds current page, page size, sort column, descending flag. `SetPage(pageNumber)` calls `TrackClient.GetPageAsync` and updates. Registered in `Startup.ConfigureDomainServices`. - `TracksViewModel`: Scoped. Holds current page, page size, sort column, descending flag. `SetPage(pageNumber)` calls `TrackClient.GetPageAsync` and updates. Registered in `Startup.ConfigureDomainServices`.
- `TrackDetailViewModel`: Scoped. Holds loaded track, loading flag, not-found flag. `Load(entryKey)` fetches via `ITrackDataService` and resets all flags per call (prevents cross-navigation bleed). Registered in `Startup.ConfigureDomainServices`. - `TrackDetailViewModel`: Scoped. Holds loaded track, loading flag, not-found flag. `Load(entryKey)` fetches via `ITrackDataService` and resets all flags per call (prevents cross-navigation bleed). Registered in `Startup.ConfigureDomainServices`.
@@ -61,12 +61,12 @@ Both are configured with JSON serializer settings (case-insensitive property mat
### Implementation ### Implementation
- `AudioPlayerService` (abstract base): Lifecycle. Stores current track, playback state, volume. `SelectTrack` throws `NotSupportedException` (buffered path is dead); derived classes override `SelectTrackStreaming`. - `AudioPlayerService` (abstract base): Lifecycle. Stores current track, playback state, volume. `SelectTrack` throws `NotSupportedException` (buffered path is dead); derived classes override `SelectTrackStreaming`.
- `StreamingAudioPlayerService` (production): Constructor takes `TrackMediaClient`, `AudioInteropService`, logger. `SelectTrackStreaming`: - `StreamingAudioPlayerService` (production): Constructor takes `TrackMediaClient`, `AudioInteropService`, logger. `SelectTrackStreaming`:
1. Calls `TrackMediaClient.GetAudioStreamAsync(trackId, offset: 0)`. 1. Calls `TrackMediaClient.GetAudioStreamAsync(trackId)`.
2. `StreamingAudioPlayerService.StreamAudioAsync` reads chunks (1664 KB adaptive), pushes each via `AudioInteropService.ProcessStreamingChunkAsync` (JS interop call). 2. `StreamingAudioPlayerService.StreamAudioAsync` reads chunks (1664 KB adaptive), pushes each via `AudioInteropService.ProcessStreamingChunkAsync` (JS interop call).
3. TypeScript `StreamDecoder` parses WAV header (first chunk), decodes subsequent chunks to `AudioBuffer`s. 3. TypeScript `StreamDecoder` parses WAV header (first chunk), decodes subsequent chunks to `AudioBuffer`s.
4. `PlaybackScheduler` schedules buffers on Web Audio `AudioContext`. 4. `PlaybackScheduler` schedules buffers on Web Audio `AudioContext`.
5. Playback starts as soon as a configurable min buffer count is queued. 5. Playback starts as soon as a configurable min buffer count is queued.
6. **Seek beyond buffer**: if seek target is past the decoded range, `Seek(position)` calls `TrackMediaClient.GetAudioStreamAsync(trackId, offset: byteOffset)`. Server's `WavOffsetService` synthesises a new 44-byte WAV header and streams from the offset. Player tears down and re-initialises decoder for the new stream. 6. **Seek beyond buffer**: if seek target is past the decoded range, `Seek(position)` calls `TrackMediaClient.GetAudioStreamAsync(trackId, byteOffset)` with a file-absolute byte offset. Client sends `Range: bytes={offset}-`; server responds 206 with raw PCM; decoder retains the parsed WAV header and feeds the continuation directly into the decode pipeline.
### Interop bridge ### Interop bridge
- `AudioInteropService.CreatePlayerAsync` polls `DeepDrftAudio.isReady()` before proceeding; `index.ts` sets `ready = true` after attaching the API to `window`. This guards against slow WASM boot / cache misses. - `AudioInteropService.CreatePlayerAsync` polls `DeepDrftAudio.isReady()` before proceeding; `index.ts` sets `ready = true` after attaching the API to `window`. This guards against slow WASM boot / cache misses.
-1
View File
@@ -233,7 +233,6 @@ Be honest about coverage gaps:
- `TrackClient` / `TrackMediaClient` (HTTP clients). - `TrackClient` / `TrackMediaClient` (HTTP clients).
- The audio player services (streaming, seek, interop). - The audio player services (streaming, seek, interop).
- Dark-mode round-trip (cookie → settings → persistent state). - Dark-mode round-trip (cookie → settings → persistent state).
- `WavOffsetService` (byte offset → new WAV stream).
- `AudioProcessor` (WAV parsing, metadata extraction). - `AudioProcessor` (WAV parsing, metadata extraction).
Any planned work in those areas should consider whether tests need to land alongside. **Testing the FileDatabase thoroughly does not mean testing everything** — it means testing the part that is most likely to break. Any planned work in those areas should consider whether tests need to land alongside. **Testing the FileDatabase thoroughly does not mean testing everything** — it means testing the part that is most likely to break.