DeepDrftAPI Rename
This commit is contained in:
@@ -0,0 +1,221 @@
|
|||||||
|
# CLAUDE.md - DeepDrftContent
|
||||||
|
|
||||||
|
Guidance for working in the DeepDrftContent project (the binary content API host).
|
||||||
|
|
||||||
|
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
|
||||||
|
|
||||||
|
## One-line purpose
|
||||||
|
|
||||||
|
The dual-database authority for tracks: SQL metadata and FileDatabase binary. Seven endpoints expose track CRUD with upload+persist, delete+cleanup, paged listing, and metadata operations. ApiKey middleware, CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent.Services`; SQL services in `DeepDrftData`.**
|
||||||
|
|
||||||
|
## What lives here now (only)
|
||||||
|
|
||||||
|
- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, middleware setup, port binding.
|
||||||
|
- `Services/UnifiedTrackService.cs`: Host-internal orchestrator. Coordinates vault write + SQL persist for upload (`UploadAsync`), and SQL delete + vault remove for delete (`DeleteAsync`).
|
||||||
|
- `Controllers/TrackController.cs`: Seven endpoints (see below).
|
||||||
|
- `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`: ApiKey validation logic.
|
||||||
|
- `Models/`: Settings POCOs only (`ApiKeySettings`, `CorsSettings`, `FileDatabaseSettings`). No domain code.
|
||||||
|
- `environment/filedatabase.json`: FileDatabase vault path config (loaded via CredentialTools, not in repo).
|
||||||
|
- `environment/apikey.json`: API key (loaded via CredentialTools, not in repo, must be created locally or at deployment).
|
||||||
|
- `environment/connections.json`: SQL connection string (loaded via CredentialTools, not in repo, format: `{ "ConnectionStrings": { "DefaultConnection": "..." } }`).
|
||||||
|
|
||||||
|
## What does NOT live here anymore
|
||||||
|
|
||||||
|
- `FileDatabase/`, `Processors/`, media models (`AudioBinary`, `ImageBinary`, etc.), `WavOffsetService` — all in `DeepDrftContent.Services`.
|
||||||
|
- 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 (seven endpoints)
|
||||||
|
|
||||||
|
### GET api/track/{trackId}?offset=0 (unauthenticated)
|
||||||
|
|
||||||
|
Returns the WAV bytes from the `tracks` vault with optional offset support.
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- 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).
|
||||||
|
- 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 404 if track not found. Returns 500 if vault operations fail (with error swallowing — the vault returns `null`).
|
||||||
|
|
||||||
|
### 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 WAV upload + metadata as `multipart/form-data`, processes the WAV, stores it in the vault, and persists metadata to SQL. Returns the fully persisted `TrackEntity` with `Id` populated.
|
||||||
|
|
||||||
|
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
||||||
|
- **Form fields**:
|
||||||
|
- `wav` (`IFormFile`, required): the WAV bytes. File name must end in `.wav`.
|
||||||
|
- `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.
|
||||||
|
- The upload stream is copied to a `.wav`-suffixed temp file under `Path.GetTempPath()` (the audio processor requires that extension and reads from disk). The temp file is always deleted in a `finally` block — success or failure.
|
||||||
|
- `[RequestSizeLimit(1 GB)]` + `[RequestFormLimits(MultipartBodyLengthLimit = 1 GB)]` lift the per-request ceiling above the framework default (~28 MB) so production-sized WAVs are accepted. The body is streamed to the temp file, not buffered in memory.
|
||||||
|
- Calls `UnifiedTrackService.UploadAsync`, which orchestrates: `TrackService.AddTrackFromWavAsync` (vault write) → `TrackManager` (SQL persist with `createdByUserId`).
|
||||||
|
- Returns 200 with the **persisted** `TrackEntity` JSON (Id populated) on success. Returns 400 for missing/invalid form fields. 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.
|
||||||
|
|
||||||
|
### GET api/track/page ([ApiKeyAuthorize])
|
||||||
|
|
||||||
|
**Authenticated endpoint.** Paged metadata list from SQL. Used by CMS track browser.
|
||||||
|
|
||||||
|
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
||||||
|
- **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"`. Defaults to `Id`.
|
||||||
|
- `sortDescending` (bool, optional, default false): sort direction.
|
||||||
|
- Calls `ITrackService.GetPaged` (via DI), which is actually `TrackManager` from `DeepDrftData`.
|
||||||
|
- Returns 200 with `PagedResult<TrackEntity>` 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 or null.
|
||||||
|
- Returns 200 with `TrackEntity` 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`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`.
|
||||||
|
- Looks up SQL row by ID, updates the provided fields (nulls in the request clear optional fields), and persists via `ITrackService.Update`.
|
||||||
|
- Returns 200 on success. Returns 404 if track not found. Returns 500 on update error.
|
||||||
|
|
||||||
|
## 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. Register singletons: `WavOffsetService`, `AudioProcessor`, `TrackService` (the `DeepDrftContent.Data` version for vault operations).
|
||||||
|
|
||||||
|
**In `Program.cs`** (SQL + wiring):
|
||||||
|
|
||||||
|
6. Load `environment/connections.json` via `CredentialTools.ResolvePathOrThrow("connections", ...)`.
|
||||||
|
7. Register `DbContext<DeepDrftContext>` (scoped) with connection string from config.
|
||||||
|
8. Register scoped: `TrackRepository`, `TrackManager`, `ITrackService` (factory resolves to `TrackManager`), `UnifiedTrackService`.
|
||||||
|
9. Configure forwarded headers (production-only) for reverse proxy support.
|
||||||
|
10. Load `environment/apikey.json` and register API key middleware.
|
||||||
|
11. Configure CORS policy (`ContentApiPolicy`): AllowAnyMethod, AllowAnyHeader, AllowCredentials, specific origins from config.
|
||||||
|
|
||||||
|
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, and CORS config. **Does not contain secrets.**
|
||||||
|
- `Logging`: standard ASP.NET structure.
|
||||||
|
- `CorsSettings.AllowedOrigins`: array of origin URLs allowed to call the API (required; throws on startup if missing).
|
||||||
|
- `environment/filedatabase.json` (required, loaded via CredentialTools, not in repo):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"FileDatabaseSettings": {
|
||||||
|
"VaultPath": "../Database/Vaults"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `environment/apikey.json` (required at runtime, loaded via CredentialTools, not in repo):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ApiKeySettings": {
|
||||||
|
"ApiKey": "your-secret-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `environment/connections.json` (required, loaded via CredentialTools, not in repo):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=localhost;Database=deepdrft;Username=postgres;Password=..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the content API (default https://localhost:5002)
|
||||||
|
dotnet run --project DeepDrftContent
|
||||||
|
|
||||||
|
# Watch during development
|
||||||
|
dotnet watch run --project DeepDrftContent
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dotnet build DeepDrftContent
|
||||||
|
|
||||||
|
# Test endpoints (requires API key in environment/apikey.json)
|
||||||
|
curl -H "ApiKey: your-secret-key" -X PUT https://localhost:5002/api/track/test-id \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"buffer":"base64-encoded-audio","size":1000,"mime":"audio/wav"}'
|
||||||
|
|
||||||
|
curl https://localhost:5002/api/track/test-id?offset=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.Services/CLAUDE.md` for the FileDatabase API and semantics. This host only provides the HTTP surface over it.
|
||||||
|
|
||||||
|
When working with this project, focus on the HTTP surface (controllers, middleware, CORS, forwarded headers) and the wiring that connects the host to the FileDatabase. New domain logic goes in `DeepDrftContent.Services`.
|
||||||
+19
-19
@@ -1,40 +1,40 @@
|
|||||||
using DeepDrftContent.Data.Audio;
|
using DeepDrftAPI.Middleware;
|
||||||
using DeepDrftContent.Data.Constants;
|
using DeepDrftAPI.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftAPI.Services;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.Audio;
|
||||||
using DeepDrftContent.Middleware;
|
using DeepDrftContent.Constants;
|
||||||
using DeepDrftContent.Models;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
using DeepDrftContent.Services;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftData;
|
using DeepDrftData;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace DeepDrftContent.Controllers;
|
namespace DeepDrftAPI.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class TrackController : ControllerBase
|
public class TrackController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DeepDrftContent.Data.TrackService _trackService;
|
private readonly DeepDrftContent.TrackContentService _trackContentService;
|
||||||
private readonly WavOffsetService _wavOffsetService;
|
private readonly WavOffsetService _wavOffsetService;
|
||||||
private readonly UnifiedTrackService _unifiedService;
|
private readonly UnifiedTrackService _unifiedService;
|
||||||
private readonly ITrackService _sqlTrackService;
|
private readonly ITrackService _sqlTrackService;
|
||||||
private readonly ILogger<TrackController> _logger;
|
private readonly ILogger<TrackController> _logger;
|
||||||
|
|
||||||
// FileDatabase is injected directly for PutTrack because that endpoint receives a pre-processed
|
// FileDatabase is injected directly for PutTrack because that endpoint receives a pre-processed
|
||||||
// AudioBinaryDto over the wire, not a WAV file path. TrackService.AddTrackFromWavAsync is
|
// AudioBinaryDto over the wire, not a WAV file path. TrackContentService.AddTrackFromWavAsync is
|
||||||
// file-path-oriented and not applicable here. If a file-upload flow is added in future,
|
// file-path-oriented and not applicable here. If a file-upload flow is added in future,
|
||||||
// route it through TrackService instead.
|
// route it through TrackContentService instead.
|
||||||
private readonly DeepDrftContent.Data.FileDatabase.Services.FileDatabase _fileDatabase;
|
private readonly DeepDrftContent.FileDatabase.Services.FileDatabase _fileDatabase;
|
||||||
|
|
||||||
public TrackController(
|
public TrackController(
|
||||||
DeepDrftContent.Data.TrackService trackService,
|
DeepDrftContent.TrackContentService trackContentService,
|
||||||
DeepDrftContent.Data.FileDatabase.Services.FileDatabase fileDatabase,
|
DeepDrftContent.FileDatabase.Services.FileDatabase fileDatabase,
|
||||||
WavOffsetService wavOffsetService,
|
WavOffsetService wavOffsetService,
|
||||||
UnifiedTrackService unifiedService,
|
UnifiedTrackService unifiedService,
|
||||||
ITrackService sqlTrackService,
|
ITrackService sqlTrackService,
|
||||||
ILogger<TrackController> logger)
|
ILogger<TrackController> logger)
|
||||||
{
|
{
|
||||||
_trackService = trackService;
|
_trackContentService = trackContentService;
|
||||||
_fileDatabase = fileDatabase;
|
_fileDatabase = fileDatabase;
|
||||||
_wavOffsetService = wavOffsetService;
|
_wavOffsetService = wavOffsetService;
|
||||||
_unifiedService = unifiedService;
|
_unifiedService = unifiedService;
|
||||||
@@ -318,11 +318,11 @@ public class TrackController : ControllerBase
|
|||||||
return File(innerStream, streamMimeType, enableRangeProcessing: false);
|
return File(innerStream, streamMimeType, enableRangeProcessing: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset path: route through TrackService.GetAudioBinaryAsync (Track B's
|
// Offset path: route through TrackContentService.GetAudioBinaryAsync (Track B's
|
||||||
// orchestrator boundary) so the controller stays out of FileDatabase directly.
|
// orchestrator boundary) so the controller stays out of FileDatabase directly.
|
||||||
// The buffered AudioBinary is required because WavOffsetService block-aligns
|
// The buffered AudioBinary is required because WavOffsetService block-aligns
|
||||||
// and reslices into a composite stream over the in-memory buffer.
|
// and reslices into a composite stream over the in-memory buffer.
|
||||||
var file = await _trackService.GetAudioBinaryAsync(trackId);
|
var file = await _trackContentService.GetAudioBinaryAsync(trackId);
|
||||||
if (file == null)
|
if (file == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Track not found: {TrackId}", trackId);
|
_logger.LogWarning("Track not found: {TrackId}", trackId);
|
||||||
@@ -366,9 +366,9 @@ public class TrackController : ControllerBase
|
|||||||
|
|
||||||
var audioBinary = AudioBinary.From(track);
|
var audioBinary = AudioBinary.From(track);
|
||||||
// Direct FileDatabase write: this endpoint receives an already-processed AudioBinaryDto,
|
// Direct FileDatabase write: this endpoint receives an already-processed AudioBinaryDto,
|
||||||
// not a WAV file, so TrackService.AddTrackFromWavAsync does not apply. See constructor comment.
|
// not a WAV file, so TrackContentService.AddTrackFromWavAsync does not apply. See constructor comment.
|
||||||
var success = await _fileDatabase.RegisterResourceAsync(
|
var success = await _fileDatabase.RegisterResourceAsync(
|
||||||
DeepDrftContent.Data.Constants.VaultConstants.Tracks, trackId, audioBinary);
|
DeepDrftContent.Constants.VaultConstants.Tracks, trackId, audioBinary);
|
||||||
return success ? Ok() : BadRequest("Failed to store audio track");
|
return success ? Ok() : BadRequest("Failed to store audio track");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||||
|
<!-- EF Core / Npgsql kept in sync with DeepDrftData / DeepDrftManager so the same DbContext registration compiles. -->
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
||||||
|
<ProjectReference Include="..\DeepDrftContent\DeepDrftContent.csproj" />
|
||||||
|
<ProjectReference Include="..\DeepDrftData\DeepDrftData.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Middleware\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace DeepDrftContent.Middleware
|
namespace DeepDrftAPI.Middleware
|
||||||
{
|
{
|
||||||
public class ApiKeyAuthenticationMiddleware
|
public class ApiKeyAuthenticationMiddleware
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Middleware
|
namespace DeepDrftAPI.Middleware
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
public class ApiKeyAuthorizeAttribute : Attribute
|
public class ApiKeyAuthorizeAttribute : Attribute
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Models
|
namespace DeepDrftAPI.Models
|
||||||
{
|
{
|
||||||
public class ApiKeySettings
|
public class ApiKeySettings
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Models;
|
namespace DeepDrftAPI.Models;
|
||||||
|
|
||||||
public class CorsSettings
|
public class CorsSettings
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Models
|
namespace DeepDrftAPI.Models
|
||||||
{
|
{
|
||||||
public class FileDatabaseSettings
|
public class FileDatabaseSettings
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Models;
|
namespace DeepDrftAPI.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Body of <c>PUT api/track/meta/{id}</c>. Metadata-only — EntryKey is immutable and never
|
/// Body of <c>PUT api/track/meta/{id}</c>. Metadata-only — EntryKey is immutable and never
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent;
|
using DeepDrftAPI;
|
||||||
using DeepDrftContent.Middleware;
|
using DeepDrftAPI.Middleware;
|
||||||
using DeepDrftContent.Models;
|
using DeepDrftAPI.Models;
|
||||||
using DeepDrftContent.Services;
|
using DeepDrftAPI.Services;
|
||||||
using DeepDrftData;
|
using DeepDrftData;
|
||||||
using DeepDrftData.Data;
|
using DeepDrftData.Data;
|
||||||
using DeepDrftData.Repositories;
|
using DeepDrftData.Repositories;
|
||||||
@@ -42,7 +42,7 @@ builder.Configuration.AddJsonFile(apiKeyPath, optional: false, reloadOnChange: f
|
|||||||
var apiKeySettings = builder.Configuration.GetSection(nameof(ApiKeySettings)).Get<ApiKeySettings>();
|
var apiKeySettings = builder.Configuration.GetSection(nameof(ApiKeySettings)).Get<ApiKeySettings>();
|
||||||
if (apiKeySettings is null) { throw new Exception("API key settings are not configured"); }
|
if (apiKeySettings is null) { throw new Exception("API key settings are not configured"); }
|
||||||
|
|
||||||
// SQL connection string — DeepDrftContent now owns both vault (FileDatabase) and SQL metadata.
|
// SQL connection string — DeepDrftAPI now owns both vault (FileDatabase) and SQL metadata.
|
||||||
var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json");
|
var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json");
|
||||||
builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false);
|
builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false);
|
||||||
|
|
||||||
+10
-10
@@ -1,32 +1,32 @@
|
|||||||
using DeepDrftContent.Data.Constants;
|
using DeepDrftContent;
|
||||||
|
using DeepDrftContent.Constants;
|
||||||
using DeepDrftData;
|
using DeepDrftData;
|
||||||
using DeepDrftModels.Entities;
|
using DeepDrftModels.Entities;
|
||||||
using NetBlocks.Models;
|
using NetBlocks.Models;
|
||||||
using ContentTrackService = DeepDrftContent.Data.TrackService;
|
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
|
||||||
using FileDb = DeepDrftContent.Data.FileDatabase.Services.FileDatabase;
|
|
||||||
|
|
||||||
namespace DeepDrftContent.Services;
|
namespace DeepDrftAPI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Host-internal orchestrator that makes DeepDrftContent the single authority over both the
|
/// Host-internal orchestrator that makes DeepDrftAPI the single authority over both the
|
||||||
/// vault (FileDatabase) and SQL metadata (DeepDrftData). Owns the two-database write/delete
|
/// vault (FileDatabase) and SQL metadata (DeepDrftData). Owns the two-database write/delete
|
||||||
/// flow so the controller stays a thin HTTP boundary and no caller coordinates the two stores.
|
/// flow so the controller stays a thin HTTP boundary and no caller coordinates the two stores.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UnifiedTrackService
|
public class UnifiedTrackService
|
||||||
{
|
{
|
||||||
internal const string TrackNotFoundMessage = "Track not found.";
|
internal const string TrackNotFoundMessage = "Track not found.";
|
||||||
private readonly ContentTrackService _contentTrackService;
|
private readonly TrackContentService _contentTrackContentService;
|
||||||
private readonly ITrackService _sqlTrackService;
|
private readonly ITrackService _sqlTrackService;
|
||||||
private readonly FileDb _fileDatabase;
|
private readonly FileDb _fileDatabase;
|
||||||
private readonly ILogger<UnifiedTrackService> _logger;
|
private readonly ILogger<UnifiedTrackService> _logger;
|
||||||
|
|
||||||
public UnifiedTrackService(
|
public UnifiedTrackService(
|
||||||
ContentTrackService contentTrackService,
|
TrackContentService contentTrackContentService,
|
||||||
ITrackService sqlTrackService,
|
ITrackService sqlTrackService,
|
||||||
FileDb fileDatabase,
|
FileDb fileDatabase,
|
||||||
ILogger<UnifiedTrackService> logger)
|
ILogger<UnifiedTrackService> logger)
|
||||||
{
|
{
|
||||||
_contentTrackService = contentTrackService;
|
_contentTrackContentService = contentTrackContentService;
|
||||||
_sqlTrackService = sqlTrackService;
|
_sqlTrackService = sqlTrackService;
|
||||||
_fileDatabase = fileDatabase;
|
_fileDatabase = fileDatabase;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -47,12 +47,12 @@ public class UnifiedTrackService
|
|||||||
long createdByUserId,
|
long createdByUserId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var unpersisted = await _contentTrackService.AddTrackFromWavAsync(
|
var unpersisted = await _contentTrackContentService.AddTrackFromWavAsync(
|
||||||
tempFilePath, trackName, artist, album, genre, releaseDate);
|
tempFilePath, trackName, artist, album, genre, releaseDate);
|
||||||
|
|
||||||
if (unpersisted is null)
|
if (unpersisted is null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("UploadAsync: content TrackService returned null for {TrackName}", trackName);
|
_logger.LogWarning("UploadAsync: content TrackContentService returned null for {TrackName}", trackName);
|
||||||
return ResultContainer<TrackEntity>.CreateFailResult("Failed to process and store WAV.");
|
return ResultContainer<TrackEntity>.CreateFailResult("Failed to process and store WAV.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
using DeepDrftContent.Data;
|
using DeepDrftAPI.Models;
|
||||||
using DeepDrftContent.Data.Audio;
|
using DeepDrftContent;
|
||||||
using DeepDrftContent.Data.Constants;
|
using DeepDrftContent.Audio;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.Constants;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.Processors;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
using DeepDrftContent.Models;
|
using DeepDrftContent.Processors;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetBlocks.Utilities.Environment;
|
using NetBlocks.Utilities.Environment;
|
||||||
|
|
||||||
namespace DeepDrftContent
|
namespace DeepDrftAPI
|
||||||
{
|
{
|
||||||
public static class Startup
|
public static class Startup
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ namespace DeepDrftContent
|
|||||||
// Audio services
|
// Audio services
|
||||||
builder.Services.AddSingleton<WavOffsetService>();
|
builder.Services.AddSingleton<WavOffsetService>();
|
||||||
builder.Services.AddSingleton<AudioProcessor>();
|
builder.Services.AddSingleton<AudioProcessor>();
|
||||||
builder.Services.AddSingleton<TrackService>();
|
builder.Services.AddSingleton<TrackContentService>();
|
||||||
|
|
||||||
// File Database
|
// File Database
|
||||||
var fileDatabasePath = CredentialTools.ResolvePathOrThrow("filedatabase", "environment/filedatabase.json");
|
var fileDatabasePath = CredentialTools.ResolvePathOrThrow("filedatabase", "environment/filedatabase.json");
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
# 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), WAV stream-with-offset, 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
|
|
||||||
├── Audio/
|
|
||||||
│ └── WavOffsetService.cs # Byte offset → valid WAV stream
|
|
||||||
├── 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.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
`AudioProcessor.ProcessWavFileAsync(filePath)`:
|
|
||||||
|
|
||||||
1. Validates the RIFF/WAVE/PCM structure.
|
|
||||||
2. Parses the fmt and data chunks.
|
|
||||||
3. Extracts duration (sample count / sample rate) and bitrate (file size / duration).
|
|
||||||
4. Returns `AudioBinary` with all metadata.
|
|
||||||
5. **Fallback**: If parsing fails, logs a warning and returns defaults (180s / 1411 kbps / 44.1 kHz / 16-bit stereo).
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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"` — 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).
|
|
||||||
|
|
||||||
## Service registration
|
|
||||||
|
|
||||||
In `DeepDrftContent/Startup.ConfigureDomainServices()` and `DeepDrftCli/Program.cs`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
services.AddSingleton<WavOffsetService>();
|
|
||||||
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.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.Audio;
|
namespace DeepDrftContent.Audio;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for creating WAV audio streams starting from a byte offset.
|
/// Service for creating WAV audio streams starting from a byte offset.
|
||||||
+110
-163
@@ -1,221 +1,168 @@
|
|||||||
# CLAUDE.md - DeepDrftContent
|
# CLAUDE.md - DeepDrftContent.Services
|
||||||
|
|
||||||
Guidance for working in the DeepDrftContent project (the binary content API host).
|
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.
|
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
|
||||||
|
|
||||||
## One-line purpose
|
## One-line purpose
|
||||||
|
|
||||||
The dual-database authority for tracks: SQL metadata and FileDatabase binary. Seven endpoints expose track CRUD with upload+persist, delete+cleanup, paged listing, and metadata operations. ApiKey middleware, CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent.Services`; SQL services in `DeepDrftData`.**
|
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).
|
||||||
|
|
||||||
## What lives here now (only)
|
## Layout
|
||||||
|
|
||||||
- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, middleware setup, port binding.
|
```
|
||||||
- `Services/UnifiedTrackService.cs`: Host-internal orchestrator. Coordinates vault write + SQL persist for upload (`UploadAsync`), and SQL delete + vault remove for delete (`DeleteAsync`).
|
DeepDrftContent.Services/
|
||||||
- `Controllers/TrackController.cs`: Seven endpoints (see below).
|
├── FileDatabase/ # The subsystem (port of TypeScript system)
|
||||||
- `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`: ApiKey validation logic.
|
│ ├── Abstractions/ # Interfaces
|
||||||
- `Models/`: Settings POCOs only (`ApiKeySettings`, `CorsSettings`, `FileDatabaseSettings`). No domain code.
|
│ ├── Models/ # Data models, DTOs, enums
|
||||||
- `environment/filedatabase.json`: FileDatabase vault path config (loaded via CredentialTools, not in repo).
|
│ ├── Services/ # FileDatabase, MediaVault, IndexSystem, IndexWatcher
|
||||||
- `environment/apikey.json`: API key (loaded via CredentialTools, not in repo, must be created locally or at deployment).
|
│ └── Utils/ # StructuralMap, StructuralSet, FileUtils
|
||||||
- `environment/connections.json`: SQL connection string (loaded via CredentialTools, not in repo, format: `{ "ConnectionStrings": { "DefaultConnection": "..." } }`).
|
├── Audio/
|
||||||
|
│ └── WavOffsetService.cs # Byte offset → valid WAV stream
|
||||||
|
├── Processors/
|
||||||
|
│ └── AudioProcessor.cs # WAV file parsing, metadata extraction
|
||||||
|
├── Constants/
|
||||||
|
│ └── VaultConstants.cs # Vault name definitions
|
||||||
|
├── TrackService.cs # Content-side orchestrator
|
||||||
|
└── DeepDrftContent.Services.csproj
|
||||||
|
```
|
||||||
|
|
||||||
## What does NOT live here anymore
|
## FileDatabase model (high-level)
|
||||||
|
|
||||||
- `FileDatabase/`, `Processors/`, media models (`AudioBinary`, `ImageBinary`, etc.), `WavOffsetService` — all in `DeepDrftContent.Services`.
|
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.
|
||||||
- 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 (seven endpoints)
|
### Core structure
|
||||||
|
|
||||||
### GET api/track/{trackId}?offset=0 (unauthenticated)
|
- **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`.
|
||||||
|
|
||||||
Returns the WAV bytes from the `tracks` vault with optional offset support.
|
### Binary hierarchy
|
||||||
|
|
||||||
- **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.
|
FileBinary (base: byte buffer)
|
||||||
- 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).
|
└── MediaBinary (+ Extension: string, MIME type inferred via MimeTypeExtensions)
|
||||||
- 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.
|
├── AudioBinary (+ Duration: double, Bitrate: int)
|
||||||
- Returns 404 if track not found. Returns 500 if vault operations fail (with error swallowing — the vault returns `null`).
|
└── ImageBinary (+ AspectRatio: double)
|
||||||
|
```
|
||||||
|
|
||||||
### PUT api/track/{trackId} ([ApiKeyAuthorize])
|
Each has a matching `*Dto` variant for base64 JSON transport (e.g., `AudioBinaryDto` with buffer encoded as base64).
|
||||||
|
|
||||||
**Authenticated endpoint.** Writes pre-processed audio bytes to the `tracks` vault.
|
### Index lifecycle
|
||||||
|
|
||||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
- **DirectoryIndex**: Root index file (at `{rootPath}/index`). Tracks which vaults exist.
|
||||||
- **Route parameter `trackId`** (string): the entry id to store under.
|
- **VaultIndex**: Per-vault index (at `{vaultPath}/index`). Records `MediaVaultType` and lists all entries in that vault.
|
||||||
- **Body**: `AudioBinaryDto` (base64 buffer + size + mime + duration + bitrate). This endpoint receives an already-processed audio DTO, not a raw WAV file.
|
- Both are JSON files. Created/loaded via `IndexFactoryService`.
|
||||||
- Validates MIME type (rejects unsupported types with `.bin` sentinel). Delegates to `FileDatabase.RegisterResourceAsync`.
|
- 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.
|
||||||
- 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])
|
## Error-handling philosophy (load-bearing)
|
||||||
|
|
||||||
**Authenticated endpoint.** Accepts a raw WAV upload + metadata as `multipart/form-data`, processes the WAV, stores it in the vault, and persists metadata to SQL. Returns the fully persisted `TrackEntity` with `Id` populated.
|
Public `Load*` / `Register*` operations **swallow exceptions and return `null` / `false`** to match the TypeScript original.
|
||||||
|
|
||||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
```csharp
|
||||||
- **Form fields**:
|
public async Task<T?> LoadResourceAsync<T>(string vaultId, string entryId) where T : FileBinary
|
||||||
- `wav` (`IFormFile`, required): the WAV bytes. File name must end in `.wav`.
|
{
|
||||||
- `trackName` (string, required)
|
try { /* load and deserialize */ }
|
||||||
- `artist` (string, required)
|
catch { return null; } // Swallow, return null
|
||||||
- `album` (string, optional)
|
}
|
||||||
- `genre` (string, optional)
|
|
||||||
- `releaseDate` (string, optional, format `YYYY-MM-DD`)
|
|
||||||
- `createdByUserId` (long, required): audit trail — who uploaded this track.
|
|
||||||
- The upload stream is copied to a `.wav`-suffixed temp file under `Path.GetTempPath()` (the audio processor requires that extension and reads from disk). The temp file is always deleted in a `finally` block — success or failure.
|
|
||||||
- `[RequestSizeLimit(1 GB)]` + `[RequestFormLimits(MultipartBodyLengthLimit = 1 GB)]` lift the per-request ceiling above the framework default (~28 MB) so production-sized WAVs are accepted. The body is streamed to the temp file, not buffered in memory.
|
|
||||||
- Calls `UnifiedTrackService.UploadAsync`, which orchestrates: `TrackService.AddTrackFromWavAsync` (vault write) → `TrackManager` (SQL persist with `createdByUserId`).
|
|
||||||
- Returns 200 with the **persisted** `TrackEntity` JSON (Id populated) on success. Returns 400 for missing/invalid form fields. Returns 500 if processing fails.
|
|
||||||
|
|
||||||
### DELETE api/track/{id:long} ([ApiKeyAuthorize])
|
public async Task<bool> RegisterResourceAsync(string vaultId, string entryId, FileBinary media)
|
||||||
|
{
|
||||||
|
try { /* store and update index */ }
|
||||||
|
catch { return false; } // Swallow, return false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Authenticated endpoint.** Removes a track: SQL row first, then vault entry. `UnifiedTrackService` owns the ordering.
|
**Callers must check return values.** Do not change this without a deliberate design pass — it's embedded in all FileDatabase tests and client code.
|
||||||
|
|
||||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
## WAV offset service
|
||||||
- **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.
|
|
||||||
|
|
||||||
### GET api/track/page ([ApiKeyAuthorize])
|
`WavOffsetService.CreateOffsetStream(buffer, byteOffset)`:
|
||||||
|
|
||||||
**Authenticated endpoint.** Paged metadata list from SQL. Used by CMS track browser.
|
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]`.
|
||||||
|
|
||||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
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.
|
||||||
- **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"`. Defaults to `Id`.
|
|
||||||
- `sortDescending` (bool, optional, default false): sort direction.
|
|
||||||
- Calls `ITrackService.GetPaged` (via DI), which is actually `TrackManager` from `DeepDrftData`.
|
|
||||||
- Returns 200 with `PagedResult<TrackEntity>` JSON (`Items`, `TotalCount`, `PageNumber`, `PageSize`). Returns 500 on query error.
|
|
||||||
|
|
||||||
### GET api/track/meta/{id:long} ([ApiKeyAuthorize])
|
**Block alignment is critical.** Do not bypass it. The WAV fmt chunk tells you the block size; use it.
|
||||||
|
|
||||||
**Authenticated endpoint.** Single track metadata from SQL by ID.
|
## Audio processor
|
||||||
|
|
||||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
`AudioProcessor.ProcessWavFileAsync(filePath)`:
|
||||||
- **Route parameter `id`** (long): the SQL track ID.
|
|
||||||
- Calls `ITrackService.GetById`, which returns the track or null.
|
|
||||||
- Returns 200 with `TrackEntity` JSON on success. Returns 404 if not found. Returns 500 on query error.
|
|
||||||
|
|
||||||
### PUT api/track/meta/{id:long} ([ApiKeyAuthorize])
|
1. Validates the RIFF/WAVE/PCM structure.
|
||||||
|
2. Parses the fmt and data chunks.
|
||||||
|
3. Extracts duration (sample count / sample rate) and bitrate (file size / duration).
|
||||||
|
4. Returns `AudioBinary` with all metadata.
|
||||||
|
5. **Fallback**: If parsing fails, logs a warning and returns defaults (180s / 1411 kbps / 44.1 kHz / 16-bit stereo).
|
||||||
|
|
||||||
**Authenticated endpoint.** Updates track metadata in SQL. EntryKey (the vault link) is immutable.
|
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.
|
||||||
|
|
||||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
## Content-side TrackService (orchestrator)
|
||||||
- **Route parameter `id`** (long): the SQL track ID.
|
|
||||||
- **Body**: `UpdateTrackMetadataRequest` with fields: `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`.
|
|
||||||
- Looks up SQL row by ID, updates the provided fields (nulls in the request clear optional fields), and persists via `ITrackService.Update`.
|
|
||||||
- Returns 200 on success. Returns 404 if track not found. Returns 500 on update error.
|
|
||||||
|
|
||||||
## ApiKey middleware behaviour
|
### AddTrackFromWavAsync(filePath)
|
||||||
|
|
||||||
`ApiKeyAuthenticationMiddleware` runs on every request but only enforces on endpoints with `[ApiKeyAuthorize]` metadata.
|
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).
|
||||||
|
|
||||||
- Reads header `ApiKey`.
|
**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).
|
||||||
- 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
|
### GetAudioBinaryAsync(entryKey)
|
||||||
|
|
||||||
`CorsSettings.AllowedOrigins` is **required** — the app throws on startup if missing. Policy is named `ContentApiPolicy`:
|
Reads audio from the `tracks` vault via `FileDatabase.LoadResourceAsync<AudioBinary>("tracks", entryKey)`. Returns `null` if not found or read fails.
|
||||||
|
|
||||||
- `AllowCredentials()`
|
### InitializeTracksVaultAsync()
|
||||||
- `AllowAnyMethod()`
|
|
||||||
- `AllowAnyHeader()`
|
|
||||||
|
|
||||||
Configured in `Startup.ConfigureDomainServices()`, applied to all endpoints via `UseCors()`.
|
Safety call to ensure the `tracks` vault exists (creates if missing). Called on host startup.
|
||||||
|
|
||||||
## Forwarded headers
|
## Vault constants
|
||||||
|
|
||||||
**Enabled only in `Production` mode** (via `if (app.Environment.IsProduction())`). This differs from `DeepDrftWeb`, which enables them always. Be aware when debugging proxy issues.
|
`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).
|
||||||
|
|
||||||
`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.
|
## Service registration
|
||||||
|
|
||||||
## Startup wiring (Startup.ConfigureDomainServices + Program.cs)
|
In `DeepDrftContent/Startup.ConfigureDomainServices()` and `DeepDrftCli/Program.cs`:
|
||||||
|
|
||||||
**In `Startup.ConfigureDomainServices`** (FileDatabase + binary services):
|
```csharp
|
||||||
|
services.AddSingleton<WavOffsetService>();
|
||||||
1. Load `environment/filedatabase.json` via `CredentialTools.ResolvePathOrThrow("filedatabase", ...)` and bind `FileDatabaseSettings`.
|
services.AddSingleton<FileDatabase>(/* from FileDatabase.FromAsync */);
|
||||||
2. Await `FileDatabase.FromAsync(VaultPath)` to load or create the database.
|
services.AddScoped<AudioProcessor>();
|
||||||
3. Register `FileDatabase` as singleton.
|
services.AddScoped<TrackService>(); // DeepDrftContent.Services.TrackService
|
||||||
4. Ensure the `tracks` vault exists (type `MediaVaultType.Audio`, created on first boot if missing).
|
```
|
||||||
5. Register singletons: `WavOffsetService`, `AudioProcessor`, `TrackService` (the `DeepDrftContent.Data` version for vault operations).
|
|
||||||
|
|
||||||
**In `Program.cs`** (SQL + wiring):
|
|
||||||
|
|
||||||
6. Load `environment/connections.json` via `CredentialTools.ResolvePathOrThrow("connections", ...)`.
|
|
||||||
7. Register `DbContext<DeepDrftContext>` (scoped) with connection string from config.
|
|
||||||
8. Register scoped: `TrackRepository`, `TrackManager`, `ITrackService` (factory resolves to `TrackManager`), `UnifiedTrackService`.
|
|
||||||
9. Configure forwarded headers (production-only) for reverse proxy support.
|
|
||||||
10. Load `environment/apikey.json` and register API key middleware.
|
|
||||||
11. Configure CORS policy (`ContentApiPolicy`): AllowAnyMethod, AllowAnyHeader, AllowCredentials, specific origins from config.
|
|
||||||
|
|
||||||
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, and CORS config. **Does not contain secrets.**
|
|
||||||
- `Logging`: standard ASP.NET structure.
|
|
||||||
- `CorsSettings.AllowedOrigins`: array of origin URLs allowed to call the API (required; throws on startup if missing).
|
|
||||||
- `environment/filedatabase.json` (required, loaded via CredentialTools, not in repo):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"FileDatabaseSettings": {
|
|
||||||
"VaultPath": "../Database/Vaults"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- `environment/apikey.json` (required at runtime, loaded via CredentialTools, not in repo):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ApiKeySettings": {
|
|
||||||
"ApiKey": "your-secret-key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- `environment/connections.json` (required, loaded via CredentialTools, not in repo):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Host=localhost;Database=deepdrft;Username=postgres;Password=..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development commands
|
## Development commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run the content API (default https://localhost:5002)
|
|
||||||
dotnet run --project DeepDrftContent
|
|
||||||
|
|
||||||
# Watch during development
|
|
||||||
dotnet watch run --project DeepDrftContent
|
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
dotnet build DeepDrftContent
|
dotnet build DeepDrftContent.Services
|
||||||
|
|
||||||
# Test endpoints (requires API key in environment/apikey.json)
|
# Run tests (FileDatabase tests cover vault/index/factory/utilities thoroughly)
|
||||||
curl -H "ApiKey: your-secret-key" -X PUT https://localhost:5002/api/track/test-id \
|
dotnet test DeepDrftTests/
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"buffer":"base64-encoded-audio","size":1000,"mime":"audio/wav"}'
|
|
||||||
|
|
||||||
curl https://localhost:5002/api/track/test-id?offset=0
|
# Run CLI (which consumes this service)
|
||||||
|
dotnet run --project DeepDrftCli -- add myfile.wav "Track Name" "Artist"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Important patterns
|
## 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.
|
- **Async/await**: All FileDatabase operations are async. No sync methods.
|
||||||
- **Error swallowing**: FileDatabase operations return `null` or `false` on failure. The controller surfaces this as 500. Never throw — check return values.
|
- **Type safety**: Generic `LoadResourceAsync<T>` ensures callers know what they're loading.
|
||||||
- **Async/await**: All operations are async.
|
- **Vault lifecycle**: Vaults are created on first boot, then reused. The `FileDatabase` singleton holds them in memory with live `IndexWatcher`es.
|
||||||
- **Vault operations**: Always use the injected `FileDatabase` singleton. Never construct a new one — it has the `IndexWatcher` and is the source of truth.
|
- **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.
|
||||||
|
|
||||||
## The FileDatabase import
|
## What does NOT live here
|
||||||
|
|
||||||
See `DeepDrftContent.Services/CLAUDE.md` for the FileDatabase API and semantics. This host only provides the HTTP surface over it.
|
- 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 HTTP surface (controllers, middleware, CORS, forwarded headers) and the wiring that connects the host to the FileDatabase. New domain logic goes in `DeepDrftContent.Services`.
|
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.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Data.Constants;
|
namespace DeepDrftContent.Constants;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constants for FileDatabase vault names
|
/// Constants for FileDatabase vault names
|
||||||
@@ -1,27 +1,17 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
||||||
<!-- EF Core / Npgsql kept in sync with DeepDrftData / DeepDrftManager so the same DbContext registration compiles. -->
|
</ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||||
<ProjectReference Include="..\DeepDrftContent.Data\DeepDrftContent.Data.csproj" />
|
</ItemGroup>
|
||||||
<ProjectReference Include="..\DeepDrftData\DeepDrftData.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Middleware\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+7
-6
@@ -1,7 +1,8 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using IndexType = DeepDrftContent.Data.FileDatabase.Services.IndexType;
|
using IndexType = DeepDrftContent.FileDatabase.Services.IndexType;
|
||||||
|
using Services_IndexType = DeepDrftContent.FileDatabase.Services.IndexType;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Abstractions;
|
namespace DeepDrftContent.FileDatabase.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for creating index instances
|
/// Interface for creating index instances
|
||||||
@@ -11,7 +12,7 @@ public interface IIndexFactory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads an existing index of the specified type
|
/// Loads an existing index of the specified type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IIndex?> LoadIndexAsync(IndexType type, string rootPath);
|
Task<IIndex?> LoadIndexAsync(Services_IndexType type, string rootPath);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a directory index
|
/// Creates a directory index
|
||||||
@@ -42,10 +43,10 @@ public interface IIndexDataFactory
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates index data for serialization
|
/// Creates index data for serialization
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object CreateIndexData(IndexType type, IIndex index);
|
object CreateIndexData(Services_IndexType type, IIndex index);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates index instance from data
|
/// Creates index instance from data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IIndex CreateIndexFromData(IndexType type, object indexData);
|
IIndex CreateIndexFromData(Services_IndexType type, object indexData);
|
||||||
}
|
}
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Abstractions;
|
namespace DeepDrftContent.FileDatabase.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for registering media type factories
|
/// Interface for registering media type factories
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Data.FileDatabase.Models;
|
namespace DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base interface for all index types - minimal contract
|
/// Base interface for all index types - minimal contract
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Models;
|
namespace DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for index data used in serialization
|
/// Base class for index data used in serialization
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Abstractions;
|
using DeepDrftContent.FileDatabase.Abstractions;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Models;
|
namespace DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shared media type registry instance — one allocation for all factory classes in this file.
|
/// Shared media type registry instance — one allocation for all factory classes in this file.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Data.FileDatabase.Models;
|
namespace DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parameters for creating a FileBinary
|
/// Parameters for creating a FileBinary
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DeepDrftContent.Data.FileDatabase.Models;
|
namespace DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enum representing different types of media vaults
|
/// Enum representing different types of media vaults
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Models;
|
namespace DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base metadata for media entries
|
/// Base metadata for media entries
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main file database class that orchestrates multiple media vaults.
|
/// Main file database class that orchestrates multiple media vaults.
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Abstractions;
|
using DeepDrftContent.FileDatabase.Abstractions;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
using IndexType = DeepDrftContent.Data.FileDatabase.Services.IndexType;
|
using IndexType = DeepDrftContent.FileDatabase.Services.IndexType;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory service for creating and managing indexes
|
/// Factory service for creating and managing indexes
|
||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Abstractions;
|
using DeepDrftContent.FileDatabase.Abstractions;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enum representing different types of indexes
|
/// Enum representing different types of indexes
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Watches index files for external modifications and triggers reloads.
|
/// Watches index files for external modifications and triggers reloads.
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abstract base class for media vaults that store and manage media files
|
/// Abstract base class for media vaults that store and manage media files
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory for creating media vaults
|
/// Factory for creating media vaults
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Abstractions;
|
using DeepDrftContent.FileDatabase.Abstractions;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Services;
|
namespace DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Simple dictionary-based registry for media type factories
|
/// Simple dictionary-based registry for media type factories
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Utils;
|
namespace DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Utility class for file I/O operations, matching the TypeScript file utilities
|
/// Utility class for file I/O operations, matching the TypeScript file utilities
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Utils;
|
namespace DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A map implementation that uses structural equality for keys by serializing them to JSON.
|
/// A map implementation that uses structural equality for keys by serializing them to JSON.
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.FileDatabase.Utils;
|
namespace DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A set implementation that uses structural equality for values by serializing them to JSON.
|
/// A set implementation that uses structural equality for values by serializing them to JSON.
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data.Processors;
|
namespace DeepDrftContent.Processors;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for processing audio files and extracting metadata
|
/// Service for processing audio files and extracting metadata
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
using DeepDrftContent.Data.Constants;
|
using DeepDrftContent.Constants;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
using DeepDrftContent.Data.Processors;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
using DeepDrftContent.Processors;
|
||||||
using DeepDrftModels.Entities;
|
using DeepDrftModels.Entities;
|
||||||
|
|
||||||
namespace DeepDrftContent.Data;
|
namespace DeepDrftContent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for managing tracks in both SQL and FileDatabase
|
/// Service for managing tracks in both SQL and FileDatabase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TrackService
|
public class TrackContentService
|
||||||
{
|
{
|
||||||
private readonly FileDatabase.Services.FileDatabase _fileDatabase;
|
private readonly FileDatabase.Services.FileDatabase _fileDatabase;
|
||||||
private readonly AudioProcessor _audioProcessor;
|
private readonly AudioProcessor _audioProcessor;
|
||||||
|
|
||||||
public TrackService(FileDatabase.Services.FileDatabase fileDatabase, AudioProcessor audioProcessor)
|
public TrackContentService(FileDatabase.Services.FileDatabase fileDatabase, AudioProcessor audioProcessor)
|
||||||
{
|
{
|
||||||
_fileDatabase = fileDatabase;
|
_fileDatabase = fileDatabase;
|
||||||
_audioProcessor = audioProcessor;
|
_audioProcessor = audioProcessor;
|
||||||
@@ -52,7 +53,7 @@ public class TrackService
|
|||||||
// Ensure tracks vault exists
|
// Ensure tracks vault exists
|
||||||
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
||||||
{
|
{
|
||||||
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.Data.FileDatabase.Models.MediaVaultType.Audio);
|
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the audio in FileDatabase
|
// Store the audio in FileDatabase
|
||||||
@@ -77,7 +78,7 @@ public class TrackService
|
|||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"TrackService.AddTrackFromWavAsync failed: {ex.Message}");
|
Console.WriteLine($"TrackContentService.AddTrackFromWavAsync failed: {ex.Message}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,9 +88,9 @@ public class TrackService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="trackId">Track ID (EntryKey)</param>
|
/// <param name="trackId">Track ID (EntryKey)</param>
|
||||||
/// <returns>Audio binary or null if not found</returns>
|
/// <returns>Audio binary or null if not found</returns>
|
||||||
public async Task<DeepDrftContent.Data.FileDatabase.Models.AudioBinary?> GetAudioBinaryAsync(string trackId)
|
public async Task<AudioBinary?> GetAudioBinaryAsync(string trackId)
|
||||||
{
|
{
|
||||||
return await _fileDatabase.LoadResourceAsync<DeepDrftContent.Data.FileDatabase.Models.AudioBinary>(VaultConstants.Tracks, trackId);
|
return await _fileDatabase.LoadResourceAsync<AudioBinary>(VaultConstants.Tracks, trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -107,7 +108,7 @@ public class TrackService
|
|||||||
{
|
{
|
||||||
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
||||||
{
|
{
|
||||||
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.Data.FileDatabase.Models.MediaVaultType.Audio);
|
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CorsSettings": {
|
|
||||||
"AllowedOrigins": [
|
|
||||||
"http://localhost:5070",
|
|
||||||
"https://localhost:5071",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:5070"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -6,13 +6,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftPublic.Client", "De
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftModels", "DeepDrftModels\DeepDrftModels.csproj", "{10CE5160-16C3-4CB1-9E2E-52467BA80B4B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftModels", "DeepDrftModels\DeepDrftModels.csproj", "{10CE5160-16C3-4CB1-9E2E-52467BA80B4B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent", "DeepDrftContent\DeepDrftContent.csproj", "{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftAPI", "DeepDrftAPI\DeepDrftAPI.csproj", "{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftTests", "DeepDrftTests\DeepDrftTests.csproj", "{47E99024-491B-47A6-BAF8-9E5814366DB2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftTests", "DeepDrftTests\DeepDrftTests.csproj", "{47E99024-491B-47A6-BAF8-9E5814366DB2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{1D1CE905-DAD0-4E93-9B09-326E8EC05877}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{1D1CE905-DAD0-4E93-9B09-326E8EC05877}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "DeepDrftContent.Data\DeepDrftContent.Data.csproj", "{4D025326-7F27-4C42-BE0F-92C1E64E0696}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent", "DeepDrftContent\DeepDrftContent.csproj", "{4D025326-7F27-4C42-BE0F-92C1E64E0696}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftManager", "DeepDrftManager\DeepDrftManager.csproj", "{E50071B2-A59F-4FB7-A435-5D966C538DDD}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftManager", "DeepDrftManager\DeepDrftManager.csproj", "{E50071B2-A59F-4FB7-A435-5D966C538DDD}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
// 1 GB ceiling matches DeepDrftContent's per-request limit on api/track/upload; the
|
// 1 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload; the
|
||||||
// streaming path means the limit caps the request, not in-memory buffering.
|
// streaming path means the limit caps the request, not in-memory buffering.
|
||||||
private const long MaxUploadBytes = 1_073_741_824L;
|
private const long MaxUploadBytes = 1_073_741_824L;
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
{
|
{
|
||||||
// OpenReadStream streams chunks from the browser via the SignalR circuit; the
|
// OpenReadStream streams chunks from the browser via the SignalR circuit; the
|
||||||
// service wraps it in StreamContent so the whole file is never materialised in
|
// service wraps it in StreamContent so the whole file is never materialised in
|
||||||
// memory before DeepDrftContent receives it.
|
// memory before DeepDrftAPI receives it.
|
||||||
await using var fileStream = _selectedFile.OpenReadStream(MaxUploadBytes);
|
await using var fileStream = _selectedFile.OpenReadStream(MaxUploadBytes);
|
||||||
|
|
||||||
var result = await CmsTrackService.UploadTrackAsync(
|
var result = await CmsTrackService.UploadTrackAsync(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ builder.Configuration.AddJsonFile(authBlocksPath, optional: false, reloadOnChang
|
|||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
// CMS track operations (read + mutate). Every track read and write goes over HTTP to the
|
// CMS track operations (read + mutate). Every track read and write goes over HTTP to the
|
||||||
// DeepDrftContent API via the named clients below — the Manager holds no in-process data layer.
|
// DeepDrftAPI API via the named clients below — the Manager holds no in-process data layer.
|
||||||
builder.Services.AddScoped<ICmsTrackService, CmsTrackService>();
|
builder.Services.AddScoped<ICmsTrackService, CmsTrackService>();
|
||||||
|
|
||||||
// AuthBlocks: JWT Bearer auth, Identity, EF schema, admin seeding.
|
// AuthBlocks: JWT Bearer auth, Identity, EF schema, admin seeding.
|
||||||
@@ -68,7 +68,7 @@ var baseUrl = GetKestrelUrl(builder);
|
|||||||
AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl);
|
AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl);
|
||||||
|
|
||||||
// Named HttpClient for unauthenticated Content API calls (CmsTrackService proxying WAV data
|
// Named HttpClient for unauthenticated Content API calls (CmsTrackService proxying WAV data
|
||||||
// to DeepDrftContent's POST api/track/upload). API key added per-request by the service.
|
// to DeepDrftAPI's POST api/track/upload). API key added per-request by the service.
|
||||||
var contentApiUrl = builder.Configuration["Api:ContentApiUrl"]
|
var contentApiUrl = builder.Configuration["Api:ContentApiUrl"]
|
||||||
?? throw new InvalidOperationException("Api:ContentApiUrl is required");
|
?? throw new InvalidOperationException("Api:ContentApiUrl is required");
|
||||||
builder.Services.AddHttpClient("DeepDrft.Content", client =>
|
builder.Services.AddHttpClient("DeepDrft.Content", client =>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ using NetBlocks.Models;
|
|||||||
namespace DeepDrftManager.Services;
|
namespace DeepDrftManager.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HTTP client over the DeepDrftContent API for all CMS track operations. The Manager is
|
/// HTTP client over the DeepDrftAPI API for all CMS track operations. The Manager is
|
||||||
/// InteractiveServer-only and holds no in-process data layer: every track read and write is a
|
/// InteractiveServer-only and holds no in-process data layer: every track read and write is a
|
||||||
/// network call to DeepDrftContent, which is the single authority over both the SQL metadata
|
/// network call to DeepDrftAPI, which is the single authority over both the SQL metadata
|
||||||
/// store and the binary audio vault. The ApiKey is baked into the <c>DeepDrft.Content.Cms</c>
|
/// store and the binary audio vault. The ApiKey is baked into the <c>DeepDrft.Content.Cms</c>
|
||||||
/// named client's default headers.
|
/// named client's default headers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -82,7 +82,7 @@ public class CmsTrackService : ICmsTrackService
|
|||||||
return ResultContainer<TrackEntity>.CreateFailResult("Upload failed on the content server. Please try again.");
|
return ResultContainer<TrackEntity>.CreateFailResult("Upload failed on the content server. Please try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4xx: body is user-friendly validation text from DeepDrftContent — relay as-is.
|
// 4xx: body is user-friendly validation text from DeepDrftAPI — relay as-is.
|
||||||
_logger.LogWarning("Content API rejected upload: {Status} {Body}", statusCode, body);
|
_logger.LogWarning("Content API rejected upload: {Status} {Body}", statusCode, body);
|
||||||
return ResultContainer<TrackEntity>.CreateFailResult(
|
return ResultContainer<TrackEntity>.CreateFailResult(
|
||||||
string.IsNullOrWhiteSpace(body) ? $"Upload rejected ({statusCode})." : body);
|
string.IsNullOrWhiteSpace(body) ? $"Upload rejected ({statusCode})." : body);
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ namespace DeepDrftManager.Services;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CMS-side track operations for the Manager host. Every read and write goes over HTTP to the
|
/// CMS-side track operations for the Manager host. Every read and write goes over HTTP to the
|
||||||
/// DeepDrftContent API, which is the single authority over both the SQL metadata store and the
|
/// DeepDrftAPI API, which is the single authority over both the SQL metadata store and the
|
||||||
/// binary audio vault. DeepDrftManager holds no in-process data layer.
|
/// binary audio vault. DeepDrftManager holds no in-process data layer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICmsTrackService
|
public interface ICmsTrackService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy a WAV upload to DeepDrftContent. The Content API owns the dual-database write and
|
/// Proxy a WAV upload to DeepDrftAPI. The Content API owns the dual-database write and
|
||||||
/// returns the persisted entity carrying the SQL-assigned <c>Id</c>. A vault-without-SQL
|
/// returns the persisted entity carrying the SQL-assigned <c>Id</c>. A vault-without-SQL
|
||||||
/// orphan is handled and logged server-side; here it surfaces as a failed result.
|
/// orphan is handled and logged server-side; here it surfaces as a failed result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ builder.Services.AddMudServices();
|
|||||||
// In dev: create the files under DeepDrftPublic/environment/ (gitignored).
|
// In dev: create the files under DeepDrftPublic/environment/ (gitignored).
|
||||||
// In prod: systemd CREDENTIALS_DIRECTORY points to encrypted credential blobs.
|
// In prod: systemd CREDENTIALS_DIRECTORY points to encrypted credential blobs.
|
||||||
// - environment/connections.json: { "ConnectionStrings": { "DefaultConnection": "..." } }
|
// - environment/connections.json: { "ConnectionStrings": { "DefaultConnection": "..." } }
|
||||||
// AuthBlocks and the DeepDrftContent API key now live on DeepDrftManager;
|
// AuthBlocks and the DeepDrftAPI API key now live on DeepDrftManager;
|
||||||
// the public host has no auth surface and no CMS upload proxy.
|
// the public host has no auth surface and no CMS upload proxy.
|
||||||
var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json");
|
var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json");
|
||||||
builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false);
|
builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DeepDrftContent.Data\DeepDrftContent.Data.csproj" />
|
<ProjectReference Include="..\DeepDrftContent\DeepDrftContent.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Abstractions;
|
using DeepDrftContent.FileDatabase.Abstractions;
|
||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
using DeepDrftContent.FileDatabase.Services;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using DeepDrftContent.Data.FileDatabase.Models;
|
using DeepDrftContent.FileDatabase.Models;
|
||||||
using DeepDrftContent.Data.FileDatabase.Utils;
|
using DeepDrftContent.FileDatabase.Utils;
|
||||||
|
|
||||||
namespace DeepDrftTests;
|
namespace DeepDrftTests;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user