f00758dc47
Move the landed Phase 12 section from PLAN.md to COMPLETED.md; update DeepDrftAPI/Content/Public.Client CLAUDE.md for the WaveformVisualizer rename, per-track high-res datum + track-waveforms vault, track-cardinal fetch, popover controls, Ambient slot, and NowPlaying host.
166 lines
11 KiB
Markdown
166 lines
11 KiB
Markdown
# CLAUDE.md - DeepDrftContent.Services
|
|
|
|
Guidance for working in the DeepDrftContent.Services project (the binary-content domain logic).
|
|
|
|
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
|
|
|
|
## One-line purpose
|
|
|
|
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
|
|
|
|
```
|
|
DeepDrftContent.Services/
|
|
├── FileDatabase/ # The subsystem (port of TypeScript system)
|
|
│ ├── Abstractions/ # Interfaces
|
|
│ ├── Models/ # Data models, DTOs, enums
|
|
│ ├── Services/ # FileDatabase, MediaVault, IndexSystem, IndexWatcher
|
|
│ └── Utils/ # StructuralMap, StructuralSet, FileUtils
|
|
├── Processors/
|
|
│ └── AudioProcessor.cs # WAV file parsing, metadata extraction
|
|
├── Constants/
|
|
│ └── VaultConstants.cs # Vault name definitions
|
|
├── TrackService.cs # Content-side orchestrator
|
|
└── DeepDrftContent.Services.csproj
|
|
```
|
|
|
|
## FileDatabase model (high-level)
|
|
|
|
See `FileDatabase/README.md` for the long-form design discussion — it's a port of a TypeScript system and has deep rationale. This section covers the essentials for an agent walking in cold.
|
|
|
|
### Core structure
|
|
|
|
- **FileDatabase**: Root object. Created via `FileDatabase.FromAsync(rootPath)`. Holds a collection of `MediaVault` instances and an `IndexWatcher`. Implements `IDisposable`. Singleton in the host.
|
|
- **MediaVault**: A subdirectory under the FileDatabase root. Each vault has its own JSON `index` file listing entries and per-entry metadata. Typed via `MediaVaultType` enum (`Media | Image | Audio`).
|
|
- Concrete implementations: `ImageVault` (for images), `AudioVault` (for audio). Do not use `ImageDirectoryVault` (that's stale docs) — the type is `ImageVault`.
|
|
- **Entry filenames**: `{sanitized-key}{extension}`, where sanitisation is `Regex.Replace(entryKey, @"[^a-zA-Z0-9]", "-")`. So entry id `"my-song"` with extension `.wav` → filename `my-song.wav`.
|
|
|
|
### Binary hierarchy
|
|
|
|
```
|
|
FileBinary (base: byte buffer)
|
|
└── MediaBinary (+ Extension: string, MIME type inferred via MimeTypeExtensions)
|
|
├── AudioBinary (+ Duration: double, Bitrate: int)
|
|
└── ImageBinary (+ AspectRatio: double)
|
|
```
|
|
|
|
Each has a matching `*Dto` variant for base64 JSON transport (e.g., `AudioBinaryDto` with buffer encoded as base64).
|
|
|
|
### Index lifecycle
|
|
|
|
- **DirectoryIndex**: Root index file (at `{rootPath}/index`). Tracks which vaults exist.
|
|
- **VaultIndex**: Per-vault index (at `{vaultPath}/index`). Records `MediaVaultType` and lists all entries in that vault.
|
|
- Both are JSON files. Created/loaded via `IndexFactoryService`.
|
|
- When a file is written externally (e.g., the CLI calls `FileDatabase.RegisterResourceAsync` directly), the **IndexWatcher** detects the write to the vault's `index` file and triggers `MediaVault.ReloadIndexAsync`, so a long-running web host stays consistent without restart.
|
|
|
|
## Error-handling philosophy (load-bearing)
|
|
|
|
Public `Load*` / `Register*` operations **swallow exceptions and return `null` / `false`** to match the TypeScript original.
|
|
|
|
```csharp
|
|
public async Task<T?> LoadResourceAsync<T>(string vaultId, string entryId) where T : FileBinary
|
|
{
|
|
try { /* load and deserialize */ }
|
|
catch { return null; } // Swallow, return null
|
|
}
|
|
|
|
public async Task<bool> RegisterResourceAsync(string vaultId, string entryId, FileBinary media)
|
|
{
|
|
try { /* store and update index */ }
|
|
catch { return false; } // Swallow, return false
|
|
}
|
|
```
|
|
|
|
**Callers must check return values.** Do not change this without a deliberate design pass — it's embedded in all FileDatabase tests and client code.
|
|
|
|
## Audio processors
|
|
|
|
Multi-format support via router pattern. All processors live in `DeepDrftContent/Processors/`:
|
|
|
|
- `AudioProcessor.ProcessWavFileAsync(filePath)`: WAV-specific processor. Validates RIFF/WAVE structure and format code. Accepts standard PCM (audioFormat=1) and WAVE_FORMAT_EXTENSIBLE (audioFormat=0xFFFE) when the SubFormat GUID indicates PCM. Normalizes EXTENSIBLE-PCM uploads to standard 44-byte PCM WAV before storing. Parses fmt and data chunks; extracts duration and bitrate. Returns `AudioBinary` with metadata. On parse failure, logs warning and returns defaults (180s / 1411 kbps / 44.1 kHz / 16-bit stereo). Accepts standard PCM (audioFormat=1), WAVE_FORMAT_EXTENSIBLE with PCM SubFormat (0x0001), IEEE Float SubFormat (0x0003), and Padded 24-in-32 containers; normalizes Float and padded inputs to standard 24-bit PCM before storage.
|
|
- `Mp3AudioProcessor.ProcessMp3FileAsync(filePath)`: MP3 processor. Skips ID3v2 tag, finds first valid MPEG frame sync, decodes frame header (bitrate, sample rate, channels). Reads Xing/Info header for VBR total-frame count (accurate duration); VBRI header as fallback; CBR estimate from file size otherwise. Returns `AudioBinary` with original bytes and `.mp3` extension. On parse failure, falls back to defaults (180s / 320 kbps).
|
|
- `FlacAudioProcessor.ProcessFlacFileAsync(filePath)`: FLAC processor. Validates `fLaC` magic, reads STREAMINFO metadata block (20-bit sample rate, 3-bit channels, 5-bit bits-per-sample, 36-bit total samples — all bit-packed). Computes duration from `totalSamples / sampleRate`; average bitrate from file size. Returns `AudioBinary` with original bytes and `.flac` extension. On parse failure, falls back to defaults (180s / 1411 kbps).
|
|
- `AudioProcessorRouter.ProcessAudioFileAsync(filePath)`: Routes by extension — `.wav` → `AudioProcessor`, `.mp3` → `Mp3AudioProcessor`, `.flac` → `FlacAudioProcessor`. Throws `ArgumentException` for unsupported extensions.
|
|
- `WaveformProfileService.ComputeAndStoreHighResAsync(entryKey)`: The shared compute seam for the duration-derived high-res waveform datum (~333 samples/sec). Medium-neutral — computes for any track by `EntryKey`, stores in the `track-waveforms` vault. Called by the upload path (`UnifiedTrackService.UploadAsync` for every new track), the CMS per-row generate action, and the Mix release trigger (now a legacy delegate). Phase 12 generalization of the former Mix-only compute.
|
|
- `WaveformResolution`: Enum / constants controlling bucket density for the high-res compute. Renamed from `MixWaveformResolution` in Phase 12.
|
|
|
|
Vault stores original bytes with correct extension and MIME type (inferred from file extension or content-type header at upload time).
|
|
|
|
The primary entry point is `TrackContentService.AddTrackAsync(filePath, mimeType)` — format-agnostic. It selects the right processor via `AudioProcessorRouter`, processes the file, generates an entry GUID, stores in vault, returns unpersisted `TrackEntity`. Legacy `AddTrackFromWavAsync(filePath)` is now a shim over `AddTrackAsync` for backward compatibility.
|
|
|
|
## Image processor
|
|
|
|
`ImageProcessor.ProcessImageAsync(buffer, mimeType)`:
|
|
|
|
1. Accepts raw image bytes and MIME type (e.g., `image/png`, `image/jpeg`).
|
|
2. Parses PNG or JPEG headers to extract image dimensions.
|
|
3. Computes aspect ratio (width / height). Defaults to 1.0 if parsing fails or format is unsupported.
|
|
4. Returns `ImageBinary` with MIME type and aspect ratio metadata.
|
|
5. **No disk I/O**: operates on `byte[]` only — no file reading required.
|
|
|
|
## Content-side TrackService (orchestrator)
|
|
|
|
### AddTrackFromWavAsync(filePath)
|
|
|
|
1. Reads a WAV file from disk.
|
|
2. Calls `AudioProcessor.ProcessWavFileAsync` → `AudioBinary`.
|
|
3. Generates a GUID entry key (via `Guid.NewGuid().ToString()`).
|
|
4. Ensures the `tracks` vault exists (creates if missing).
|
|
5. Calls `FileDatabase.RegisterResourceAsync("tracks", entryKey, audioBinary)`.
|
|
6. Returns a populated `TrackEntity` (with `Id = 0` since it's not yet in SQL).
|
|
|
|
**Note**: The caller (CLI or web) is responsible for then saving this entity to SQL via `DeepDrftWeb.Services.TrackService.Create`. If the vault write succeeds and SQL write fails, audio is orphaned (no compensating rollback).
|
|
|
|
### GetAudioBinaryAsync(entryKey)
|
|
|
|
Reads audio from the `tracks` vault via `FileDatabase.LoadResourceAsync<AudioBinary>("tracks", entryKey)`. Returns `null` if not found or read fails.
|
|
|
|
### InitializeTracksVaultAsync()
|
|
|
|
Safety call to ensure the `tracks` vault exists (creates if missing). Called on host startup.
|
|
|
|
## Vault constants
|
|
|
|
`VaultConstants.Tracks = "tracks"`, `VaultConstants.Images = "images"`, and `VaultConstants.TrackWaveforms = "track-waveforms"` — the vault names in production use. `TrackWaveforms` holds the per-track high-res waveform datum keyed by `TrackEntity.EntryKey` (Phase 12; renamed from the former `mix-waveforms`, which was Mix-only). New vault names go here when adding new vault types.
|
|
|
|
## Service registration
|
|
|
|
In `DeepDrftContent/Startup.ConfigureDomainServices()` and `DeepDrftCli/Program.cs`:
|
|
|
|
```csharp
|
|
services.AddSingleton<FileDatabase>(/* from FileDatabase.FromAsync */);
|
|
services.AddScoped<AudioProcessor>();
|
|
services.AddScoped<TrackService>(); // DeepDrftContent.Services.TrackService
|
|
```
|
|
|
|
## Development commands
|
|
|
|
```bash
|
|
# Build
|
|
dotnet build DeepDrftContent.Services
|
|
|
|
# Run tests (FileDatabase tests cover vault/index/factory/utilities thoroughly)
|
|
dotnet test DeepDrftTests/
|
|
|
|
# Run CLI (which consumes this service)
|
|
dotnet run --project DeepDrftCli -- add myfile.wav "Track Name" "Artist"
|
|
```
|
|
|
|
## Important patterns
|
|
|
|
- **Async/await**: All FileDatabase operations are async. No sync methods.
|
|
- **Type safety**: Generic `LoadResourceAsync<T>` ensures callers know what they're loading.
|
|
- **Vault lifecycle**: Vaults are created on first boot, then reused. The `FileDatabase` singleton holds them in memory with live `IndexWatcher`es.
|
|
- **Entry sanitisation**: Keys are sanitised client-side (in the CLI and web host) *and* by `MediaVault` (defensive). Always sanitise before registering — it's the only way to ensure safe filenames.
|
|
- **Metadata hierarchy**: Use the appropriate media type (`AudioBinary`, `ImageBinary`) so downstream code can rely on the metadata. Don't store an audio file as a generic `MediaBinary` — use `AudioBinary` with duration/bitrate.
|
|
|
|
## What does NOT live here
|
|
|
|
- HTTP controllers or middleware — that's `DeepDrftContent`.
|
|
- SQL database code — that's `DeepDrftWeb.Services`.
|
|
- Blazor components or UI logic — that's `DeepDrftWeb.Client`.
|
|
- Configuration files (`appsettings.json`, `filedatabase.json`) — those are in the host project.
|
|
|
|
When working with this project, focus on the FileDatabase subsystem (the most complex piece of the codebase), audio processing, and the orchestration logic that bridges binary and SQL databases. The tests (in `DeepDrftTests/`) are the load-bearing documentation of FileDatabase behaviour — consult them when FileDatabase semantics are unclear.
|