Adds docs/README.md (index), api-conventions.md, spa-conventions.md, e2e-local.md + scripts/e2e-local.sh, blazor-route-parity.md (#91 phase-b tracker), domain-model.md, decisions.md. Rule (CLAUDE.md): read these at session start instead of re-recon; update in the same PR that changes a convention/route/decision. Part 2 = #185. Handoff: #180/#141/#158/#161 closed (PRs #181/#182/#183); #145 playback-only; #91 readiness plan posted; next prompt = #185 + quick wins + #155/#151/#152/#153. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
8.5 KiB
Markdown
100 lines
8.5 KiB
Markdown
# ErsatzTV Fork
|
|
|
|
Custom IPTV channel server for Jellyfin. Forked from [ErsatzTV/ErsatzTV](https://github.com/ErsatzTV/ErsatzTV) after upstream archival (Feb 2026, v26.3.0). Our fork lives on [Gitea](http://192.168.1.95:3000/timothy/ersatztv).
|
|
|
|
## Architecture
|
|
|
|
- **Language**: C# / .NET 10
|
|
- **UI**: ChicoryTV React SPA (`web/`, Vite, served at `/app`) over the REST API — the default UI; root `/` and migrated legacy routes 302 there (`ErsatzTV/LegacyUiRedirects.cs`). The legacy Blazor Server UI (MudBlazor) still serves the remaining un-migrated admin screens — playback troubleshooting, multi/rerun collections, and playlist editing depth; Blazor home = `/system/health`, reachable via the Settings → System "Classic UI" link. Media detail pages + image folder browser landed in the SPA via #141 (PR #183); its removal is #91 phase (b), gated on #145 (playback troubleshooting) and API gaps #151/#152/#153/#155 (scheduling parity #144/#162 DONE 2026-07-07: blocks/templates/decos/deco-templates/playout editors all in the SPA; #141/#158/#161/#180 also DONE)
|
|
- **Pattern**: CQRS via MediatR — queries/commands in `ErsatzTV.Application/`
|
|
- **Database**: EF Core (SQLite default, MySQL optional) — context in `ErsatzTV.Infrastructure/Data/TvContext.cs`
|
|
- **Media**: FFmpeg via CliWrap, SkiaSharp for logo generation
|
|
- **Functional C#**: Language Ext (Option, Either monads throughout)
|
|
|
|
### Project Layout
|
|
|
|
| Project | Role |
|
|
|---------|------|
|
|
| `ErsatzTV/` | ASP.NET Core host, API controllers, SPA static hosting, legacy Blazor pages, DI setup |
|
|
| `web/` | ChicoryTV React SPA (Vite + TypeScript; builds into `ErsatzTV/wwwroot/app`) |
|
|
| `ErsatzTV.Application/` | MediatR handlers (business logic) |
|
|
| `ErsatzTV.Core/` | Domain entities, interfaces, no infrastructure deps |
|
|
| `ErsatzTV.Infrastructure/` | EF Core repos, data access |
|
|
| `ErsatzTV.Infrastructure.Sqlite/` | SQLite-specific implementations |
|
|
| `ErsatzTV.FFmpeg/` | FFmpeg process wrapper |
|
|
| `ErsatzTV.Scanner/` | Media library scanning |
|
|
|
|
### Key Files
|
|
|
|
- **M3U generation**: `ErsatzTV.Core/Iptv/ChannelPlaylist.cs` → `ToM3U()`
|
|
- **XMLTV generation**: `ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs`
|
|
- **IPTV controller**: `ErsatzTV/Controllers/IptvController.cs` — `/iptv/*` routes
|
|
- **Logo generation**: `ErsatzTV.Core/Images/ChannelLogoGenerator.cs`
|
|
- **Channel entities**: `ErsatzTV.Core/Domain/Channel.cs`
|
|
- **DB context**: `ErsatzTV.Infrastructure/Data/TvContext.cs`
|
|
|
|
## Deployment
|
|
|
|
- **Docker host**: jazz (192.168.1.99), container `ersatztv`, port 8409
|
|
- **Config volume**: `~/downloadswarm/ersatztv/` on jazz → `/config` in container
|
|
- **SQLite DB**: `/config/ersatztv.sqlite3` (WAL mode, root-owned)
|
|
- **Images** (our fork, built by `.gitea/workflows/docker-build.yml` → `192.168.1.95:3000/timothy/ersatztv`): push to `main` → `:latest` + `:<sha>` (test image); push `v*` tag → `:prod` + `:<version>` + `:<sha>`. Prod deploys via **Komodo GitOps**: the `media-servers` compose in `timothy/server-management` (`docker/bumblebee/stacks/media-servers/compose.yaml`) pins the version tag (currently `26.5.0`, deployed 2026-07-07); releasing = tag here, wait for the image build, bump that pin and push (the Komodo pre-deploy hook backs up before recreating). Test container tracks `:latest`. Pipeline details: `docs/ci-cd.md`.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
# Build
|
|
dotnet build ErsatzTV.sln
|
|
|
|
# Run locally (needs FFmpeg in PATH)
|
|
dotnet run --project ErsatzTV
|
|
|
|
# Docker build
|
|
docker build -f docker/Dockerfile -t ersatztv:dev .
|
|
```
|
|
|
|
## Conventions
|
|
|
|
- **Read [`docs/contributing.md`](docs/contributing.md)** before non-trivial changes — it documents the established patterns (layering, CQRS handlers, LanguageExt, Blazor/MudBlazor, EF Core + dual-provider migrations, the FFmpeg pipeline, analyzers, testing) and the **deviation policy**: match the established style; diverge only with a concrete, stated reason.
|
|
- **Convention docs replace re-recon**: before API/SPA/E2E/parity work, read `docs/README.md` (index) → `docs/api-conventions.md`, `docs/spa-conventions.md`, `docs/e2e-local.md`, `docs/domain-model.md`, `docs/blazor-route-parity.md`, `docs/decisions.md`. Any PR that changes a convention, migrates a route, or reverses a decision MUST update the relevant doc in the same PR.
|
|
- Follow existing MediatR CQRS pattern for new features
|
|
- Domain logic in `ErsatzTV.Core`, infrastructure in `ErsatzTV.Infrastructure`
|
|
- Keep UI thin: the SPA talks to `/api/*` only; legacy Blazor pages delegate to MediatR handlers. New screens go in the SPA (`web/`), never in Blazor
|
|
- Test with **NUnit** + Shouldly + NSubstitute (the existing `*.Tests` projects); xUnit is **not** used here
|
|
- **Dependencies use Central Package Management**: versions live in the repo-root `Directory.Packages.props`; csproj reference packages by name only. Add/upgrade by editing the central `<PackageVersion>` — never put `Version=` back on a `<PackageReference>` (trips `NU1008`). See `docs/ci-cd.md` → Dependency management.
|
|
- **DB migrations target BOTH providers**: a `TvContext` model change needs a migration in `ErsatzTV.Infrastructure.Sqlite` **and** `ErsatzTV.Infrastructure.MySql` — run `scripts/add-migration.sh <Name>` (does both). CI's `migrations` job enforces model-drift + apply-to-fresh-DB per provider. See `docs/ci-cd.md` → Migration integrity.
|
|
- **Renovate** is live (`.gitea/workflows/renovate.yml`, weekly + `workflow_dispatch`): opens dependency-update + OSV vuln-fix PRs and a Dependency Dashboard issue; patch bumps to test/dev-only packages auto-merge once `Build & test` passes, the rest are manual. Cross-repo rollout: server-management#484. See `docs/ci-cd.md` → Dependency management.
|
|
- **Versioning**: release tags are `vYY.<release-seq>.<patch>` (year · sequential release-within-year · patch) — inherited from upstream, **not** year.month. `v26.3.1` = our infra rebuild of upstream 26.3.0 (no app changes); `v26.4.0` is reserved for the first release with app changes. Never `[skip ci]` a commit you'll tag (it suppresses the release build). Full policy: `docs/ci-cd.md` → Versioning & releases.
|
|
- Backlog tracked via [Gitea Issues](http://192.168.1.95:3000/timothy/ersatztv/issues)
|
|
|
|
## Task Completion Protocol
|
|
|
|
Every task that closes a Gitea issue MUST complete ALL of these before it is considered done. Use `/done <issue>` to run through this automatically.
|
|
|
|
1. **Root cause** (bug fixes / incidents only): Document WHY the problem existed, not just what was changed. If root cause is unknown, say so explicitly and open a follow-up investigation issue. Fixing symptoms without understanding causes creates recurring problems.
|
|
2. **Comment on issues** as you work — what you found, what approach you're taking, any deviations from the suggested fix.
|
|
3. **Push changes**: `git push` all commits before closing. Use `fixes #N` in commit messages to auto-close where appropriate.
|
|
4. **Close comment**: Add a structured closing comment on the issue covering: what was done, root cause (if applicable), files changed, anything deferred, follow-up issues created, and which docs were updated.
|
|
5. **Close the issue** via API or `fixes #N` commit. Leave open with a comment only if partially addressed.
|
|
6. **Update docs**: If the change affects operational behavior, update the relevant Obsidian docs (`~/homelab-docs/`), MEMORY.md, or CLAUDE.md inline — not as a follow-up.
|
|
7. **Reply to reviewer** (if from adversarial review): Summary of done/deferred/questions. This triggers the next review cycle.
|
|
|
|
## Project Boundaries
|
|
|
|
**ersatztv OWNS**: ErsatzTV fork code (C#/.NET), channel/collection/schedule management, M3U/XMLTV generation, the ErsatzTV skill in server-management.
|
|
|
|
**ersatztv does NOT own**:
|
|
- Docker compose configs → server-management (`~/downloadswarm/stacks/ersatztv/`)
|
|
- NFS mounts, Ansible, DNS, networking → server-management
|
|
- Content sourcing (yt-dlp downloads, Sonarr/Radarr libraries) → media-management (planned)
|
|
- Jellyfin skill → server-management (symlinked)
|
|
|
|
**For infrastructure changes** (Docker, NFS, ports, Authelia): open an issue in `timothy/server-management`.
|
|
|
|
**For content/media sourcing questions** (what goes into channels, yt-dlp pipelines): open an issue in `timothy/media-management` once it exists; for now, `timothy/server-management`.
|
|
|
|
**For plan/audit reviews**: open `~/adversarial-reviewer` before significant architecture changes.
|
|
|
|
**Full cross-project rules**: `~/homelab-docs/Operations/Project Boundaries.md` (https://docs.tblindustries.be).
|
|
**ErsatzTV docs**: `~/homelab-docs/Docker/ErsatzTV.md` + project-local `docs/` (fork strategy, channels, M3U/XMLTV).
|