docs(skill): correct the ersatztv skill for the fork — it described upstream #549
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user