Files
daniel-c-harvey 3da6591194 docs(phase-16): reflect live Plays card in stats CLAUDE.md
HomeStatsDto gains TotalPlays + UniqueListeners; StatsController now composes ITrackService + IEventService (best-effort play/listener reads).
2026-06-19 15:41:17 -04:00

8.1 KiB

CLAUDE.md - DeepDrftModels

Guidance for working in the DeepDrftModels project (shared contracts across all projects).

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

One-line purpose

Shared contracts. Entities, DTOs, pagination types. Every project references this; nothing else references the projects that reference this.

Layout

DeepDrftModels/
├── Entities/
│   └── TrackEntity.cs          # Database entity for tracks
├── DTOs/
│   ├── TrackDto.cs             # DTO mirror of TrackEntity
│   └── HomeStatsDto.cs         # Aggregate figures for the public home hero stat row
├── Models/
│   ├── PagingParameters.cs      # Pagination configuration (base + generic)
│   └── PagedResult.cs           # Paginated result wrapper
└── DeepDrftModels.csproj

TrackEntity (the core entity)

The single source of truth for track metadata. Fields:

public long Id { get; set; }                      // Primary key (auto-assigned by SQLite)
public required string EntryKey { get; set; }     // FileDatabase entry id (max 100)
public required string TrackName { get; set; }    // Track title (max 200)
public required string Artist { get; set; }       // Artist name (max 200)
public string? Album { get; set; }                // Optional album (max 200)
public string? Genre { get; set; }                // Optional genre (max 100)
public DateOnly? ReleaseDate { get; set; }        // Optional release date
public string? ImagePath { get; set; }            // Optional image URL (max 500)
public double? DurationSeconds { get; set; }      // Audio runtime in seconds (nullable; populated at upload, backfilled for older rows)

No MediaPath field exists. That was a legacy name. The field is EntryKey.

The EntryKey is the link to binary content — it's the entry id in the tracks vault inside FileDatabase. The database column is entry_key (snake_case).

Convention: required reference fields use required modifier; optional reference fields are ?. Don't relax required — it's the compile-time guarantee that prevents half-built entities from reaching the database.

TrackDto

Mirrors TrackEntity structure (identical fields, same nullability). Used where DTO/entity separation is needed for serialisation. In practice, both flow over the wire today, but the separation is available if APIs need to diverge (e.g., hide Id in responses).

HomeStatsDto

Aggregate figures behind the public home hero stat row (NowPlayingStats). A single round-trip returns everything the three cards need. Fields:

  • CutTrackCount (int): total non-deleted tracks on Cut-medium releases.
  • CutReleaseTypeCounts (List<CutReleaseTypeCount>): per-ReleaseType Cut release counts; zero-count types are absent (suppressed server-side).
  • MixReleaseCount (int): total non-deleted Mix-medium releases.
  • MixRuntimeSeconds (double): sum of DurationSeconds across all non-deleted tracks on Mix releases (null durations count as 0). Rendered as hh:mm by RuntimeFormat on the client.
  • TotalPlays (long): site-wide total plays — sum of every play_counter row's PartialCount + SampledCount + CompleteCount, all-time (Phase 16 §5). Zero until the play-telemetry migration is applied; that is expected, not an error. The Plays card's primary odometer figure.
  • UniqueListeners (int): site-wide distinct anonymous listeners — distinct non-null anon_id values across all play events, all-time (Phase 16 §3 / D7). Zero until the migration is applied. The Plays card's secondary line ("N listeners").

CutReleaseTypeCount is a nested type (ReleaseType, Count int) defined in the same file.

Pagination system

PagingParameters (base)

public class PagingParameters
{
    public int Page { get; set; } = 1;         // Current page (1-based)
    public int PageSize { get; set; } = 20;    // Items per page (default 20, max 100)
}

PageSize setter enforces a hard ceiling at 100 — prevents clients from requesting huge pages that would tank performance.

PagingParameters (generic)

public class PagingParameters<T> : PagingParameters
{
    public Expression<Func<T, object>>? OrderBy { get; set; }  // Type-safe sort expression
    public bool IsDescending { get; set; }                     // Sort direction
    public int Skip => (Page - 1) * PageSize;                  // Calculated (0-based)
}

Used by services to build type-safe LINQ queries. The service takes a string sort column from the API, maps it to an Expression<Func<T, object>>, and passes it in OrderBy. The repository applies the expression to the DbSet.

PagedResult

public class PagedResult<T>
{
    public IEnumerable<T> Items { get; set; }     // Current page items
    public int TotalCount { get; set; }           // Total items available
    public int Page { get; set; }                 // Current page
    public int PageSize { get; set; }             // Items per page
    
    // Calculated properties
    public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
    public bool HasNextPage => Page < TotalPages;
    public bool HasPreviousPage => Page > 1;
}

Returned by service GetPaged methods. Includes a static factory From<TOther> for cross-type mapping (e.g., PagedResult<TrackEntity>PagedResult<TrackDto>).

Key patterns

Required fields

Essential fields use the required modifier. This is a compile-time guarantee that prevents half-built entities from being instantiated.

public required string EntryKey { get; set; }
public required string Artist { get; set; }

Nullable reference types

Explicit nullability throughout:

public string? Album { get; set; }              // Optional, can be null
public required string Artist { get; set; }     // Required, must be assigned

Expression-based sorting

Type-safe LINQ expressions for dynamic sorting. No string-based OrderBy at runtime:

parameters.OrderBy = entity => entity.TrackName;  // Compile-time checked

Services map string column names to expressions in a switch, so only valid sorts are possible.

Pagination defaults

  • Page: 1 (1-based)
  • PageSize: 20 (per-item default)
  • PageSize max: 100 (hard cap, enforced by setter)

Client requests outside these bounds are clamped by the API controller before being passed to the service.

Integration points

  • EF Core: TrackEntity is configured in DeepDrftWeb.Services.Data.Configurations.TrackConfiguration.
  • API serialisation: All models JSON-serializable. Controllers return ActionResult<ApiResultDto<PagedResult<TrackEntity>>>.
  • Cross-project usage:
    • DeepDrftWeb: Server render preload, controller responses.
    • DeepDrftWeb.Client: HTTP client deserialization, UI display.
    • DeepDrftContent.Services: TrackService.AddTrackFromWavAsync returns populated TrackEntity.
    • DeepDrftCli: Entity display, table formatting.
    • DeepDrftTests: Test assertions.

NetBlocks transitivity

This project does not directly reference NetBlocks. The result types (Result, ResultContainer<T>, ApiResult<T>, ApiResultDto<T>) come into services and clients directly via their own NetBlocks references. Models stay independent of result wrappers — they are serializable POCOs.

Important conventions

  • Column naming: Entity property EntryKey maps to table column entry_key (snake_case, configured in EF).
  • Table name: track (singular, configured in EF).
  • Required fields: Properties with required modifier cannot be left unassigned.
  • Page numbers: 1-based (page 1 is the first page). The Skip calculation converts to 0-based for LINQ ((Page - 1) * PageSize).

Development commands

# Build
dotnet build DeepDrftModels

# No tests live here (models are simple POCOs)
# Tests that use these models live in DeepDrftTests/

When working with this project, maintain the required/optional distinction rigorously, keep the pagination contract tight, and remember that this project is the interface between all other projects — changes here ripple everywhere.