The ChicoryTV React SPA (web/, served at /app) now has full parity for every route the Blazor UI served, so the legacy Blazor Server / MudBlazor UI is deleted. This is the milestone-capping removal of #91 phase (b). Deleted: ErsatzTV/Pages/**, Shared/**, ViewModels/** (39 edit VMs), Validators/** (10 edit-VM validators), App.razor, _Imports.razor, Locals/{Shared,Pages}/** (Blazor loc resx; Locals/Resources.* kept), wwwroot/css + wwwroot/lib, libman.json, and the orphaned MultiSelectBaseTests. Startup.cs (surgical, not wholesale): removed AddRazorPages/AuthorizeFolder, AddServerSideBlazor, AddMudServices, AddSortable, AddCourier, the HtmlSanitizer registration, the Blazor-attached OIDC UseAuthentication/UseAuthorization middleware (per the #206 auth-posture sign-off), MapBlazorHub, and MapFallbackToPage("/_Host"). Renamed the branch blazor->legacy; it still co-hosts MapControllers, /docs (Scalar), dev MapOpenApi and the redirect middleware. Replaced the _Host fallback with a catch-all (MapFallback -> 302 /app) that excludes /api|/artwork|/docs|/openapi (genuine 404) per #204. Kept all OIDC/JWT/API-key service wiring (inert unless configured; real auth is #197), ConditionalIptvAuthorizeFilter, ApiKeyAuthorizationFilter. Pruned 9 now-unused packages (all verified zero remaining consumers) from Directory.Packages.props + ErsatzTV.csproj: MudBlazor, Heron.MudCalendar, Blazored.FluentValidation, BlazorSortable, MediatR.Courier.DependencyInjection, Markdig, HtmlSanitizer, Chronic.Core, NaturalSort.Extension. Also removed the now-dead #25 razor-Sonar NoWarn. LegacyUiRedirects: added the 14 /media/sources/* -> /app/libraries/* redirects (SPA screens landed in #202) and lifted the #204-era /media/sources prefix ban. Tests: Release build clean; full solution suite green. Updated Startup source-text tests + added regression coverage that Blazor wiring is gone, the catch-all is wired, and all 14 media-sources routes redirect. Docs: blazor-route-parity.md (phase b COMPLETE), decisions.md (removal entry), CLAUDE.md, contributing.md, README.md all updated in this PR. Rollback: tag blazor-final is cut on pre-merge main as the first merge action. Part of #91. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.3 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
recordtypes named for the action —GetX/CreateX/UpdateX/DeleteX— implementingIRequest<TResult>, living under<Domain>/Queries/or<Domain>/Commands/next to their handler. - Result types (LanguageExt, see §3): queries return
Option<T>orList<T>; commands returnOption<BaseError>(void success) orEither<BaseError, T>(value success). - Handlers implement
IRequestHandler<TRequest, TResult>with primary-constructor DI and anasync 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-Applyvalidation →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 withMatch/MatchAsync(onSome, onNone); default withIfNone(...); transform withMap/Bind;foreach (var x in option)to unpack conditionally.Either<BaseError, T>/Validation<BaseError, T>— the error backbone.BaseError.New("…")for failures,Unit.Defaultfor void success. Accumulate multiple checks withValidation(tuple.Apply((a, b, c) => …)) thenToEither().MapLeftto 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.mdfor the SPA screen playbook. - Endpoints the screen calls: see
docs/api-conventions.mdfor 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 viaIDbContextFactory<TvContext>in handlers.- Two providers:
ErsatzTV.Infrastructure.Sqlite(the prod default,/config/ersatztv.sqlite3) and…MySql, each with its ownMigrations/set +TvContextModelSnapshot. The provider is chosen by theproviderconfig key inStartup. - A model change needs a migration in BOTH providers — run
scripts/add-migration.sh <Name>. CI'smigrationsjob enforces model-drift + apply-to-fresh-DB per provider (seedocs/ci-cd.md→ Migration integrity). Review non-transactional ops (e.g. SQLitePRAGMA foreign_keys) carefully. (ersatztv#13)
6. FFmpeg pipeline
- Step-based composition: each task is an
IPipelineStep(ErsatzTV.FFmpeg/IPipelineStep.cs) that contributesGlobalOptions/InputOptions/FilterOptions/OutputOptionsand aNextState()that threads frame metadata (pixel format, data location, dimensions).CommandGenerator.GenerateArgumentsassembles the steps into the final argument list. - Detect capabilities at runtime; don't hardcode —
IHardwareCapabilitiesimplementations (Vaapi/Nvidia/…HardwareCapabilities) report available codecs/profiles, and builders pick the encoder/decoder accordingly. - Version-gate features explicitly — e.g.
Capabilities/NvidiaHardwareCapabilities.csparsesFFmpegCapabilities.Versionand 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
.editorconfigis the source of truth for formatting + rule severities (plusErsatzTV.sln.DotSettingsfor ReSharper). Rundotnet formatbefore committing.TreatWarningsAsErrors=truein the app projects — a warning fails the build.NoWarncarries a small, documented exemption list (e.g.VSTHRD200,CA1873); NuGet-auditNU1901-1903are demoted to warnings inDirectory.Build.propswhileNU1904(critical) blocks.- Static-analysis packs (Roslynator, SonarAnalyzer, Meziantou, AsyncFixer) run at
suggestionand are promoted towarning/errorrule-by-rule; promotion is the enforcement (the TWAE build). New rules start at suggestion — never flip a wall of rules to error at once. (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 putVersion=back on a<PackageReference>(NU1008). Repo-wide MSBuild config is inDirectory.Build.props/Directory.Build.targets. (ersatztv#14) - The Gitea Actions pipeline (
.gitea/workflows/docker-build.yml):test→migrations→build(image + smoke/IPTV-E2E). Renovate opens dependency PRs; a weeklydependency-scansurfaces 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. Seedocs/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.