From ersatztv-programmer
Senior-engineer mental model for the ErsatzTV ecosystem. Project history, repository layout, where things live on disk, common debugging paths, upstream entry points, key concepts. Loads when the user asks "where does X live," "why is Y not working," "what's the difference between Legacy and Next," or otherwise needs deep ErsatzTV context the schedule/reference skills don't cover.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ersatztv-programmer:knowledgeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When this skill is in context, you are operating with a senior ErsatzTV engineer's mental model. Every fact below is sourced — cite the source when surfacing a fact to the user so they can verify it. If a claim isn't here and you can't trace it to one of the listed sources, say "I don't know; let me check" and fetch upstream rather than guess.
When this skill is in context, you are operating with a senior ErsatzTV engineer's mental model. Every fact below is sourced — cite the source when surfacing a fact to the user so they can verify it. If a claim isn't here and you can't trace it to one of the listed sources, say "I don't know; let me check" and fetch upstream rather than guess.
ErsatzTV exists in two distinct projects right now:
ersatztv/legacy:develop and ghcr.io/ersatztv/legacy:developghcr.io/ersatztv/next:latestThis plugin targets Next. References to Legacy in the audit skill exist solely to help users migrate.
lineup.json ──► channels[N].config = ./channels/N/channel.json
│
├─► playout.folder ──► {start}_{finish}.json (one or many)
├─► ffmpeg.{paths}
└─► normalization.{audio,video}
| Layer | File | What it owns |
|---|---|---|
| Server | lineup.json | Bind address, output folder, the full channel list |
| Channel | channel.json (per-channel) | FFmpeg paths, normalization, where playout files live |
| Time slice | {start}_{finish}.json (per-window) | The actual items[] to play in that window |
This separation is the whole point of Next: a channel is a long-lived config, a playout is a short-lived schedule, and the streaming engine is dumb about everything except how to follow the playout.
Source of truth: https://github.com/ErsatzTV/next/blob/main/README.md + the three schemas under https://github.com/ErsatzTV/next/tree/main/schema.
If you have to read source, this is the order to read it in:
| Crate | What it does | Path |
|---|---|---|
ersatztv | Axum HTTP server. Serves /channels.m3u, /channel/{N}.m3u8, /session/{channel}/{file}. Spawns one ersatztv-channel subprocess per active channel. | crates/ersatztv |
ersatztv-channel | Per-channel worker. Reads playout JSON, builds FFmpeg pipelines, writes HLS segments. Has a 4-state buffering machine (SeekAndWorkAhead → ZeroAndWorkAhead → SeekAndRealtime → ZeroAndRealtime). | crates/ersatztv-channel |
ffpipeline | FFmpeg pipeline builder. Probes source media, picks hardware acceleration via the HwAccel trait (CUDA, QSV, VAAPI, VideoToolbox), constructs filter chains. | crates/ffpipeline |
ersatztv-playout | Playout JSON data models — serde + schemars. Schema generated here. | crates/ersatztv-playout |
ersatztv-core | Shared utilities: heartbeat/ready-file management, timing constants. | crates/ersatztv-core |
ersatztv-playout-generator | Dev tool: generates playout JSON from a video folder, or runs sync-channel against a Legacy SQLite DB. | crates/ersatztv-playout-generator |
Source: https://github.com/ErsatzTV/next/blob/main/CLAUDE.md (project's own AGENTS.md/CLAUDE.md, kept current by the upstream team).
Legacy (native macOS install), per ErsatzTV.Core/FileSystemLayout.cs in the upstream codebase:
| Thing | Path |
|---|---|
| App data | ~/.local/share/ersatztv/ |
| SQLite DB | ~/.local/share/ersatztv/ErsatzTV.db (with -wal and -shm siblings) |
| Channel guide cache | ~/.local/share/ersatztv/cache/channel-guide/{N}.xml |
| Logo cache | ~/.local/share/ersatztv/cache/artwork/logos/ |
| Watermark cache | ~/.local/share/ersatztv/cache/artwork/watermarks/ |
| FanArt cache | ~/.local/share/ersatztv/cache/artwork/fanart/ |
| Lucene search index | ~/.local/share/ersatztv/search-index/ |
| Logs | ~/.local/share/ersatztv/logs/ |
Folder declarations come from FileSystemLayout.cs. File-naming inside each cache folder (flat vs hash-bucketed) is a write-side convention — confirm against the specific writer when it matters. Source: https://github.com/ErsatzTV/legacy/blob/main/ErsatzTV.Core/FileSystemLayout.cs.
Next (typical Docker layout):
| Thing | Path inside container | Path on host (typical) |
|---|---|---|
| Lineup | /config/lineup.json | ~/ersatztv-stack/config/ersatztv-next/lineup.json |
| Per-channel config | /config/channels/{N}/channel.json | ~/ersatztv-stack/config/ersatztv-next/channels/{N}/channel.json |
| Per-channel playouts | {folder from channel.json playout.folder} | wherever the user pointed it (commonly ~/ersatztv-stack/config/ersatztv-next/channels/{N}/playout/) |
| HLS output | output.folder from lineup.json | ~/ersatztv-stack/hls/ or /tmp/hls |
Verbatim Dockerfile + path conventions: https://github.com/ErsatzTV/next/blob/main/docker/Dockerfile.
When a user says "my channel won't play," walk this list in order. Each step has a one-line check.
curl http://localhost:18409/channels.m3u — should return the M3U list. If 404, the server isn't running. If empty, no channels are in lineup.json.jq '.channels[] | select(.number=="42")' lineup.json — if no output, the channel is missing from the lineup.channel.json valid? jq . channel.json — JSON parse failure here surfaces as a silent omission in the lineup.ls channels/42/playout/. The compact-ISO-8601 filename's window must contain now, in the user's local timezone.python tools/playout-validate.py channels/42/playout/*.json (this plugin's bundled validator, hooked in via ${CLAUDE_PLUGIN_ROOT}/tools/playout-validate.py).local, stat "$path" from inside the container (docker exec ersatztv-next stat /media/...). For http, curl -I the URL.docker logs ersatztv-next --tail 200. A repeating "ffmpeg exited 1" usually means a probe failure on the source — bad codec, broken file, unreachable URL.ls /tmp/hls/. If empty more than ~30 s after channel hit, the worker isn't progressing.These are the steps the upstream Discord and forum recommend; collated from https://discuss.ersatztv.org/ and the project's CLAUDE.md.
When the user uses one of these terms, this is what they mean. Mismatch on these is the #1 cause of confusion in support threads.
| Term | Meaning |
|---|---|
| Lineup | The full channel roster. Lives in lineup.json. Next-only term. |
| Channel | A logical 24/7 stream. In Legacy: a row in the Channels SQLite table. In Next: a lineup.json entry pointing at a channel.json. |
| Playout | The time-coded list of items a channel plays. In Legacy: rows in the PlayoutItem table generated by a builder from a Schedule/Block/Sequential YAML. In Next: a JSON file under the channel's playout folder. |
| Block (Legacy) | A reusable group of scheduled items, e.g. "Saturday Morning Cartoons block." Block + Template + PlayoutTemplate is the Legacy scheduling stack. |
| Smart Collection (media-server side) | A saved query that resolves to a set of media items. Lives in Jellyfin/Plex/Emby, not in ErsatzTV. The plugin queries the user's server via MCP to resolve it. |
| Source (Next) | A local file, a lavfi synthetic, or an http URL. Each playout item has at least one. |
| Lavfi | FFmpeg's -f lavfi -i synthetic input — generates audio/video from a filter graph (silence, color bars, "be right back" cards). Use for fillers and gaps. |
| HLS | HTTP Live Streaming. Next outputs .m3u8 playlists + .ts (or .fmp4) segments under output.folder. The default segment length is 4 s; keyframe interval is 2 s — both per CLAUDE.md. |
When you don't know something and it's not here, go to one of these in order:
https://github.com/ErsatzTV/next/tree/main/schema (live truth for Next config).https://github.com/ErsatzTV/next/blob/main/README.md, https://github.com/ErsatzTV/next/blob/main/CLAUDE.md (project's own agent-oriented docs).https://ersatztv.org/ (entry point; community links and docs index).https://github.com/ErsatzTV/next/issues (known bugs, design discussions).https://old.reddit.com/r/ErsatzTV/ (announcements, occasional debugging threads).The bundled reference skill in this plugin pins the playout schema at version https://ersatztv.org/playout/version/0.0.1. The tools/check-updates.sh SessionStart hook polls https://raw.githubusercontent.com/ErsatzTV/next/main/schema/playout.json and warns if upstream's $id differs. If you see the warning surface in additionalContext:
reference skill is out of date — file an issue at https://github.com/MeridianVega/claude-marketplace/issues so the plugin maintainer can refresh it.ErsatzTV was created by jasondove as a C#/Blazor IPTV server with smart-collection scheduling. It accumulated a complex scheduling stack (Classic schedules → Block schedules → Sequential YAML → Scripted schedules) and a Lucene-backed search index. In April 2026 the project bifurcated: the original was archived briefly, then unarchived as "Legacy" and feature-frozen; a Rust rewrite called Next took over the streaming/transcoding role with a much smaller scope. The maintainer's stated direction: Next is the transcoding engine, third-party schedulers (this plugin among them) emit playout JSON for it, Legacy continues to exist as a reference scheduler and will eventually use Next as its transcoding backend. Source: https://old.reddit.com/r/ErsatzTV/comments/1sngryj/what_is_next_for_ersatztv/ and the unarchive notice on the Legacy repo.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub meridianvega/claude-marketplace --plugin ersatztv-programmer