diff --git a/DeepDrftAPI/CLAUDE.md b/DeepDrftAPI/CLAUDE.md index a546e51..8fb1e9a 100644 --- a/DeepDrftAPI/CLAUDE.md +++ b/DeepDrftAPI/CLAUDE.md @@ -6,7 +6,7 @@ See the root `CLAUDE.md` for full architecture overview. This file covers what i ## One-line purpose -Dual-database authority for tracks (SQL metadata + FileDatabase binary), and AuthBlocks API host (JWT auth, role/admin seed). Seven track endpoints expose CRUD with upload+persist, delete+cleanup, paged listing, and metadata operations. ApiKey middleware for track endpoints, JWT + AuthBlocks endpoints for auth. CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent`; SQL services in `DeepDrftData`.** +Dual-database authority for tracks (SQL metadata + FileDatabase binary) and images (FileDatabase binary), and AuthBlocks API host (JWT auth, role/admin seed). Seven track endpoints expose CRUD with upload+persist, delete+cleanup, paged listing, and metadata operations. Two image endpoints provide authenticated upload and unauthenticated streaming. ApiKey middleware for track/image endpoints, JWT + AuthBlocks endpoints for auth. CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent`; SQL services in `DeepDrftData`.** ## What lives here now (only) @@ -104,10 +104,29 @@ Returns the WAV bytes from the `tracks` vault with optional offset support. - **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`. - **Route parameter `id`** (long): the SQL track ID. -- **Body**: `UpdateTrackMetadataRequest` with fields: `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`. -- Looks up SQL row by ID (returns `TrackDto`), updates the provided fields (nulls in the request clear optional fields), and persists the DTO via `ITrackService.Update`. +- **Body**: `UpdateTrackMetadataRequest` with fields: `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`, `ImagePath?` (tri-state: null = no change, "" = clear, value = set). +- Looks up SQL row by ID (returns `TrackDto`), updates the provided fields (nulls in the request for optional metadata clear those fields; `ImagePath` follows tri-state logic), and persists the DTO via `ITrackService.Update`. - Returns 200 with the updated `TrackDto` on success. 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`). + ## ApiKey middleware behaviour `ApiKeyAuthenticationMiddleware` runs on every request but only enforces on endpoints with `[ApiKeyAuthorize]` metadata. @@ -141,7 +160,8 @@ Configured in `Startup.ConfigureDomainServices()`, applied to all endpoints via 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. Register singletons: `WavOffsetService`, `AudioProcessor`, `TrackService` (the `DeepDrftContent` version for vault operations). +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). **In `Program.cs`** (SQL + AuthBlocks + wiring): diff --git a/DeepDrftContent/CLAUDE.md b/DeepDrftContent/CLAUDE.md index ad8264a..ade3d96 100644 --- a/DeepDrftContent/CLAUDE.md +++ b/DeepDrftContent/CLAUDE.md @@ -101,6 +101,16 @@ Used by the content API to serve seek-beyond-buffer requests. The player asks fo PCM-only today. Other formats (mp3, flac, aac, ogg, m4a) are listed in `MimeTypeExtensions` but not implemented. The processor validates RIFF/WAVE/PCM format — anything else is rejected. +## 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) @@ -124,7 +134,7 @@ Safety call to ensure the `tracks` vault exists (creates if missing). Called on ## Vault constants -`VaultConstants.Tracks = "tracks"` — the one vault name in production use. New vault names go here when adding new vault types (e.g., `VaultConstants.Images = "images"` if image uploads are added). +`VaultConstants.Tracks = "tracks"` and `VaultConstants.Images = "images"` — the vault names in production use. New vault names go here when adding new vault types. ## Service registration diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md index 96c8f9b..3bf4729 100644 --- a/DeepDrftPublic.Client/CLAUDE.md +++ b/DeepDrftPublic.Client/CLAUDE.md @@ -73,7 +73,7 @@ Both are configured with JSON serializer settings (case-insensitive property mat ### Component integration - `AudioPlayerProvider.razor` is the cascading host. It injects `IStreamingPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade with `IsFixed="true"`, and keeps it alive across navigation. -- `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render itself when the cascade updates. +- `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. Minimize-state mutations (`Expand`, `ToggleMinimized`, `Close`) all route through a private `SetMinimized(bool value)` mutator, which guards no-ops, fires the `OnMinimized` callback, and calls `StateHasChanged()`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render itself when the cascade updates. - `SpectrumVisualizer.razor` calls `AudioInteropService.GetSpectrumData()` on a timer, receives bar heights, renders via MudBlazor `MudChart` or custom canvas. - `TracksView.razor` injects `TracksViewModel` + cascaded `IStreamingPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrackStreaming(track)`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` and calls `StateHasChanged()` unconditionally on any state change, ensuring the gallery correctly reflects play/pause/track-change transitions. Active-track state is derived from `PlayerService.CurrentTrack` and `PlayerService.IsPlaying` (no local `_selectedTrack` field).