docs(skill): correct the ersatztv skill for the fork — it described upstream #549

Merged
timothy merged 1 commits from docs/skill-etv-refresh into main 2026-07-21 20:51:48 +02:00
+59 -11
View File
@@ -5,19 +5,55 @@ description: ErsatzTV custom IPTV channel management — REST API, SQLite DB, Je
# ErsatzTV Channel Management
Container: `ersatztv` | Port: `8409` | IP: `172.16.238.11` (may change on restart)
Web UI: internal only (`http://localhost:8409` via SSH)
SQLite DB: `~/downloadswarm/ersatztv/ersatztv.sqlite3` on jazz (owned by root — use `sudo sqlite3`)
Image: `ghcr.io/ersatztv/ersatztv:latest` (v26.3.0, repo archived Feb 2026)
Host: **jazz (192.168.1.29)**. Prod container `ersatztv` port **8409**; test `ersatztv-test` port
**8410** (tracks `:latest` via Komodo auto-update, daily 03:00 — a same-day validation needs the
manual pull below).
SQLite DB: `~/downloadswarm/ersatztv/ersatztv.sqlite3` (owned by root — use `sudo sqlite3`)
Image: **our fork**, `192.168.1.95:3000/timothy/ersatztv` (`:prod` / `:latest`). Upstream
`ghcr.io/ersatztv/ersatztv` was archived at v26.3.0 and is NOT what runs here.
## Architecture
ErsatzTV uses **MediatR + Blazor** (not REST for mutations). The REST API is limited:
- **GET endpoints**: channels, collections, schedules, playouts, shows, movies, artists, ffmpeg profiles, health, search, watermarks
- **POST endpoints**: library scan, playout reset, show scan
- **No REST CRUD for channels/collections/schedules** — must use SQLite DB directly
**This section described upstream v26.3.0 and was wrong for the fork — corrected 2026-07-21.**
## REST API
- The **Blazor UI is gone** (#91 phase b). The only UI is the ChicoryTV React SPA at `/app`; legacy
routes 302 there.
- There **is** a full versioned REST API under **`/api/v1`**, write paths included — channels,
collections, schedules, playouts and media sources have CRUD. **Do not hand-edit SQLite for
something the API can do.** The DB-scripting recipes below survive only for gaps with no endpoint.
- Controllers stay thin and delegate to MediatR handlers; the SPA talks to `/api/v1` only.
- Authoritative endpoint list: `docs/endpoint-index.md` (generated) + `docs/api-conventions.md`.
Prefer those over any list in this file — a hand-maintained copy drifts.
## REST API access (auth-gated — read before curling)
Calls need **`X-Api-Key`** (machine clients) or a browser session. An unauthenticated call returns a
401 JSON body that is easy to mistake for real data — see the silent-401 trap in Gotchas.
The key file is **root-owned `0600`**, so `cat` as `timothy` fails *silently* and yields an empty
header. Read it with `sudo`, inline, so the value is never printed:
```bash
# prod (8409); test is identical with .../ersatztv-test/api.key and port 8410
ssh timothy@192.168.1.29 'K=$(sudo -n cat /home/timothy/downloadswarm/ersatztv/api.key); \
curl -s -H "X-Api-Key: $K" http://localhost:8409/api/v1/channels'
```
Settings live under `/api/v1/settings/*``settings/ffmpeg` (`workAheadSegmenterLimit`,
`qsvExtraHardwareFrames`) and `settings/logging` (`streamingMinimumLogLevel`). Note the order: it is
`settings/ffmpeg`, **not** `ffmpeg/settings`.
Refresh test to the newest `:latest` without waiting for 03:00 — scope it to the service, since a
bare `up -d` would recreate everything else in the compose project:
```bash
D=/etc/komodo/stacks/ersatztv/docker/jazz/stacks/ersatztv
docker compose -f $D/compose.yaml pull ersatztv-test
docker compose -f $D/compose.yaml up -d --no-deps ersatztv-test
```
The unversioned `/api/*` endpoints below predate the `/api/v1` surface — verify one against
`docs/endpoint-index.md` before relying on it.
```bash
# Via docker exec
@@ -148,13 +184,25 @@ After creating: `POST /api/channels/{number}/playout/reset`
## Gotchas
- DB owned by root — always use `sudo sqlite3`
- **The api.key file is root-owned too, and an unsudo'd read fails SILENTLY.** `cat` returns nothing,
the header goes out empty, and the 401 body parses as a dict — so a naive script reports "0
channels" rather than an auth error. If a query returns a suspiciously empty result, check auth
before believing it. (Cost a wrong reading on 2026-07-21.)
- WAL mode: reads OK while running, stop container for writes
- No REST API for channel/collection/schedule CRUD — DB scripting only
- ~~No REST API for channel/collection/schedule CRUD~~**false since the fork's `/api/v1`**; use the
API, not DB scripting, wherever an endpoint exists
- **A container's OCI labels lie about what is running** — they are inherited from the linuxserver
base image (they claimed `2026-06-27` on an image built minutes earlier). To prove which build is
live, compare `docker inspect <c> --format '{{.Image}}'` to the registry's `Docker-Content-Digest`
for that tag
- **Container log lines carry a LOCAL-time bracket (`[18:48:13 DBG]`) while `docker logs -t` emits
UTC**, so `--since` windows silently mis-slice. For before/after measurements capture by line
offset instead (`wc -l` before, `tail -n +N` after)
- Secrets file uses PascalCase JSON (`Address`, `ApiKey`)
- Scanner is separate binary (`ErsatzTV.Scanner`) — check with `docker top ersatztv | grep Scanner`
- EF TPT inheritance: `ProgramScheduleItem` has subtype tables (`ProgramScheduleOneItem`, etc.) — MUST insert into subtype table
- External URL logos work for M3U but NOT for watermark burn-in (code checks `File.Exists()`)
- `/api/health` returns Blazor HTML, not JSON — use `/api/channels` to verify API
- `/api/health` predates the Blazor removal; verify the API with an authenticated `/api/v1/channels` instead
- PlaybackOrder enum: 3=Shuffle, 6=SeasonEpisode (use 3 for all channels)
- CollectionType enum: 0=Collection, 1=Show (direct show reference via MediaItemId)
- SubtitleMode: 0=None, 2=Burn-in. Set to 2 with PreferredSubtitleLanguageCode='eng' for non-music channels