Files
ersatztv/docs/contributing.md
T
timothyandCodex 8f90cea5cc feat(analyzers): enable incremental latest-All enforcement
Centralize SDK and threading analyzers, baseline the .NET 10 All rule inventory at suggestion severity, and promote S3981 repo-wide. Fix the always-true worker count predicate and cover the idle/active branches.

Fixes #15

Co-Authored-By: Codex <codex@openai.com>
2026-07-16 21:52:27 +02:00

9.5 KiB

Contributing to the ErsatzTV fork

A descriptive guide to the conventions already in this codebase — written so future changes (human or AI) extend the established architecture instead of reinventing it. It's a living document: update it when a pattern legitimately evolves.

The guiding principle

Match the established style and patterns. Diverge only when there's a genuinely strong reason — when it's the real best or only option — even if that means rearchitecting. Never deviate gratuitously or just because a different approach is locally convenient. When a deviation is justified, state why (in the PR/commit, and in code comments where it'll surprise a reader).

Several of the rules below are now enforced in CI (layering tests, analyzers, formatting, migration checks) — see the pointers per section.

1. Architecture & layering

CQRS via MediatR. Dependency direction is enforced by ErsatzTV.Architecture.Tests (NetArchTest):

Project Responsibility May depend on
ErsatzTV.FFmpeg ffmpeg process/pipeline wrapper (nothing — lowest layer)
ErsatzTV.Core domain entities, interfaces, pure logic FFmpeg only — no EF Core, no Infrastructure/Application
ErsatzTV.Infrastructure EF Core data access, external clients Core
ErsatzTV.Infrastructure.Sqlite / .MySql provider-specific EF (migrations) Core, Infrastructure
ErsatzTV.Application MediatR handlers (business logic) Core, Infrastructure (the abstraction, not the concrete providers)
ErsatzTV.Scanner library scanning host Core, Infrastructure*
ErsatzTV ASP.NET Core host, controllers, DI composition root everything

If a test fails here, the fix is almost always to move the type, not to relax the rule. (ersatztv#12)

2. CQRS handler conventions

  • Requests are record types named for the action — GetX / CreateX / UpdateX / DeleteX — implementing IRequest<TResult>, living under <Domain>/Queries/ or <Domain>/Commands/ next to their handler.
  • Result types (LanguageExt, see §3): queries return Option<T> or List<T>; commands return Option<BaseError> (void success) or Either<BaseError, T> (value success).
  • Handlers implement IRequestHandler<TRequest, TResult> with primary-constructor DI and an async Task<TResult> Handle(TRequest request, CancellationToken cancellationToken).
  • Validation lives in the handler as private static methods returning Validation<BaseError, T>, composed with tuple .Apply(...)not FluentValidation or MediatR pipeline behaviors.
  • Examples: ErsatzTV.Application/Channels/Queries/GetChannelById.cs (+ …/GetChannelByIdHandler.cs, Option<ChannelViewModel>), ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs (tuple-Apply validation → Either<BaseError, Unit>).

3. Functional style (LanguageExt)

Every project globally imports using static LanguageExt.Prelude;. Use the monads instead of exceptions for control flow.

  • Option<T>Optional(x), Some/None, HeadOrNone() to pluck from a collection; branch with Match/MatchAsync(onSome, onNone); default with IfNone(...); transform with Map/Bind; foreach (var x in option) to unpack conditionally.
  • Either<BaseError, T> / Validation<BaseError, T> — the error backbone. BaseError.New("…") for failures, Unit.Default for void success. Accumulate multiple checks with Validation (tuple .Apply((a, b, c) => …)) then ToEither(). MapLeft to transform errors.
  • Examples: ErsatzTV.Core/FFmpeg/…, ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs.

4. Web UI (ChicoryTV SPA)

The legacy Blazor Server UI + MudBlazor were removed in #91 phase (b). The ChicoryTV React SPA in web/ (Vite + TypeScript, built into ErsatzTV/wwwroot/app, served at /app) is the only UI; it talks exclusively to the REST API. Every former Blazor route now 302-redirects to its /app equivalent (ErsatzTV/LegacyUiRedirects.cs + the Startup catch-all).

  • Adding a screen: see docs/spa-conventions.md for the SPA screen playbook.
  • Endpoints the screen calls: see docs/api-conventions.md for the endpoint checklist.
  • Keep the UI thin — screens call /api/*; business logic stays in MediatR handlers.

5. Data access & EF Core

  • TvContext (ErsatzTV.Infrastructure/Data/TvContext.cs) is the single DbContext; resolve it via IDbContextFactory<TvContext> in handlers.
  • Two providers: ErsatzTV.Infrastructure.Sqlite (the prod default, /config/ersatztv.sqlite3) and …MySql, each with its own Migrations/ set + TvContextModelSnapshot. The provider is chosen by the provider config key in Startup.
  • A model change needs a migration in BOTH providers — run scripts/add-migration.sh <Name>. CI's migrations job enforces model-drift + apply-to-fresh-DB per provider (see docs/ci-cd.md → Migration integrity). Review non-transactional ops (e.g. SQLite PRAGMA foreign_keys) carefully. (ersatztv#13)

6. FFmpeg pipeline

  • Step-based composition: each task is an IPipelineStep (ErsatzTV.FFmpeg/IPipelineStep.cs) that contributes GlobalOptions/InputOptions/FilterOptions/OutputOptions and a NextState() that threads frame metadata (pixel format, data location, dimensions). CommandGenerator.GenerateArguments assembles the steps into the final argument list.
  • Detect capabilities at runtime; don't hardcodeIHardwareCapabilities implementations (Vaapi/Nvidia/… HardwareCapabilities) report available codecs/profiles, and builders pick the encoder/decoder accordingly.
  • Version-gate features explicitly — e.g. Capabilities/NvidiaHardwareCapabilities.cs parses FFmpegCapabilities.Version and disables 10-bit H.264 decode hwaccel below ffmpeg 8. Follow this pattern for the ffmpeg 8 upgrade (ersatztv#9).
  • Examples: ErsatzTV.FFmpeg/Encoder/EncoderBase.cs, ErsatzTV.FFmpeg/Filter/ScaleFilter.cs.

7. Naming, formatting, analyzers

  • .editorconfig is the source of truth for formatting + rule severities (plus ErsatzTV.sln.DotSettings for ReSharper). Run dotnet format before committing.
  • Fix formatting as you touch it (no big-bang). ~2500 legacy .cs files inherited from upstream carry a UTF-8 BOM, which violates .editorconfig's charset=utf-8. We do not mass-reformat. Instead, when you modify a file for other work, normalize it in that same PRdotnet format ErsatzTV.sln --include <the files you touched>, which strips the BOM and fixes style. Files you did not touch stay as-is. This is enforced two ways so it can't be silently skipped (ersatztv#311): the pre-commit hook verifies staged .cs, and a blocking format CI job re-verifies the .cs this PR changed against .editorconfig (a .cs-free PR passes trivially). Never --no-verify past a format failure on a file you changed — de-BOM/format it instead. (A one-time repo-wide normalization is a separate, unmade decision; the touched-file rule is the standing one.)
  • TreatWarningsAsErrors=true in the app projects — a warning fails the build. NoWarn carries a small, documented exemption list (e.g. VSTHRD200, CA1873); NuGet-audit NU1901-1903 are demoted to warnings in Directory.Build.props while NU1904 (critical) blocks.
  • Static analysis is centralized: Directory.Build.props enables the SDK analyzers at latest-All plus the threading analyzer for every project; Directory.Build.targets adds Roslynator, SonarAnalyzer, Meziantou, and AsyncFixer. The checked-in SDK globalconfig and .editorconfig keep the broad baseline at suggestion. Promote one reviewed rule at a time by setting it to warning and adding its ID to the central WarningsAsErrors list; never flip a wall of rules to error at once. Refresh the SDK globalconfig deliberately when moving to a new .NET SDK major. (ersatztv#15)

8. Testing

NUnit ([TestFixture]/[Test]/[TestCase]) + Shouldly + NSubstitute; xUnit is not used. Golden-file tests guard the M3U/XMLTV output formats and a hard fail on a missing/changed golden means broken code, not a stale fixture — never regenerate via ETV_UPDATE_GOLDENS=1 in CI or from an agent. Full testing map (per-project coverage, golden-file details, timezone notes, how to run subsets, the per-PR gate): docs/testing.md.

9. Build / CI

  • Central Package Management: versions live in the repo-root Directory.Packages.props; csproj reference packages by name only — never put Version= back on a <PackageReference> (NU1008). Repo-wide MSBuild config is in Directory.Build.props / Directory.Build.targets. (ersatztv#14)
  • The Gitea Actions pipeline (.gitea/workflows/docker-build.yml): testmigrationsbuild (image + smoke/IPTV-E2E). Renovate opens dependency PRs; a weekly dependency-scan surfaces advisories. Full details: docs/ci-cd.md.
  • Versioning is CalVer vYY.<release-seq>.<patch> (not year.month); never [skip ci] a commit you'll tag. See docs/ci-cd.md → Versioning.

10. Deviation policy

Re-stating §0 because it's the whole point: follow the established pattern; diverge only with a concrete, stated reason. Record an intentional departure in the PR description and a code comment at the point of surprise (e.g. "using X instead of the usual Y because …"). If a pattern itself should change, change it deliberately and update this guide in the same PR.