ErsatzTV Infrastructure as Code¶
Version-controlled YAML configurations for ErsatzTV scheduling, collections, and channels.
Quick Reference¶
| Item | Path |
|---|---|
| Config Directory | /root/tower-fleet/configs/ersatztv/ |
| Sync Script | /root/tower-fleet/scripts/ersatztv-sync.sh |
| Collections YAML | configs/ersatztv/collections.yaml |
| Fillers YAML | configs/ersatztv/fillers.yaml |
| Schedules | configs/ersatztv/schedules/*.yaml |
Usage¶
# Check sync status
/root/tower-fleet/scripts/ersatztv-sync.sh status
# Sync smart collections to ErsatzTV (API)
/root/tower-fleet/scripts/ersatztv-sync.sh collections
# Deploy YAML schedules to ErsatzTV
/root/tower-fleet/scripts/ersatztv-sync.sh schedules
# Export current ErsatzTV state
/root/tower-fleet/scripts/ersatztv-sync.sh export
# Sync everything
/root/tower-fleet/scripts/ersatztv-sync.sh all
Architecture¶
YAML Configs ErsatzTV
───────────── ────────
collections.yaml ──[API]────► Smart Collections
fillers.yaml ──[SQLite]──► Filler Presets (manual for now)
schedules/*.yaml ──[SCP]────► /opt/arr-stack/configs/ersatztv/schedules/
│
▼
Channel Playout (manual assignment)
Sync Modes¶
| Component | Method | Notes |
|---|---|---|
| Smart Collections | API | Full CRUD, safe, additive-only |
| Filler Presets | SQLite | Manual creation in UI for now |
| Schedules | File copy | Native ErsatzTV YAML format |
| Channels | Manual | Configure via UI after schedule deploy |
File Structure¶
/root/tower-fleet/configs/ersatztv/
├── collections.yaml # Smart collection definitions
├── fillers.yaml # Filler preset definitions (reference)
├── schedules/
│ ├── toonami.yaml # Anime channel with bumps
│ └── cinema.yaml # Movie marathon channel
└── backups/ # Auto-created by sync script
Collections YAML¶
Defines smart collections that auto-update based on search queries:
smart_collections:
# Episode collections (from Jellyfin)
- name: "[episodes] Cowboy Bebop"
query: 'show_title:"Cowboy Bebop"'
# Bump collections (from local libraries)
- name: "[bumps] Cowboy Bebop"
query: 'library_name:"[bumps] Cowboy Bebop"'
# Movie collections (by franchise, director, genre)
- name: "[movies] Tarantino"
query: 'director:"quentin tarantino"'
- name: "[movies] Anime"
query: 'tag:"anime" AND type:movie'
Search Query Syntax¶
| Field | Example | Description |
|---|---|---|
show_title |
show_title:"Breaking Bad" |
Match TV show |
title |
title:Star Wars* |
Match movie/episode title |
library_name |
library_name:"[bumps]*" |
Match library |
director |
director:"quentin tarantino" |
Match director |
studio |
studio:"studio ghibli" |
Match studio |
genre |
genre:"documentary" |
Match genre |
type |
type:movie |
Filter by type |
tag |
tag:"anime" |
Match tag |
Operators: AND, OR, * (wildcard), quotes for exact match
Schedules (Sequential YAML)¶
ErsatzTV native format for sophisticated scheduling with day parting:
content:
- smart_collection: "[episodes] Cowboy Bebop"
key: COWBOY_BEBOP
order: chronological
- smart_collection: "[bumps] Cowboy Bebop"
key: CB_BUMPS
order: shuffle
playout:
# Wait until 6 PM
- wait_until: "18:00"
tomorrow: false
# Play show with bumps grouped in EPG
- epg_group: true
advance: true
- count: 1
content: CB_BUMPS
filler_kind: preroll
- count: 1
content: COWBOY_BEBOP
custom_title: "Cowboy Bebop"
- count: 2
content: CB_BUMPS
filler_kind: postroll
- epg_group: false
# Pad to :30 mark
- pad_to_next: 30
content: GENERIC_BUMPS
trim: true
# Loop forever
- repeat: true
Playout Instructions Reference¶
| Instruction | Description | Example |
|---|---|---|
wait_until |
Pause until time | wait_until: "18:00" |
count |
Play N items | count: 1 |
duration |
Play for time | duration: "2 hours" |
pad_to_next |
Fill to interval | pad_to_next: 30 |
pad_until |
Fill to time | pad_until: "22:00" |
epg_group |
Group EPG entries | epg_group: true |
repeat |
Loop schedule | repeat: true |
Themed Channels¶
Toonami (Channel 100)¶
Day-parted anime channel with show-specific bumps:
| Time | Block | Content |
|---|---|---|
| 06:00-14:00 | Morning | Nature docs, animated comedies |
| 14:00-18:00 | Afternoon | Animated comedies |
| 18:00-23:00 | Prime Toonami | Sequential anime with bumps |
| 23:00-02:00 | Adult Swim | Comedy, originals |
| 02:00-06:00 | Late Night | Anime movies |
Cinema (Channel 102)¶
Movie marathon channel with franchise rotations:
| Time | Block | Content |
|---|---|---|
| 06:00-12:00 | Family | Ghibli, animated features |
| 12:00-18:00 | Sci-Fi | Star Wars, Matrix, Terminator |
| 18:00-22:00 | Prime | Tarantino, LOTR |
| 22:00-02:00 | Horror | Alien, Friday 13th, VHS |
| 02:00-06:00 | Cult | David Lynch, anime films |
Setting Up a New Channel¶
-
Create schedule YAML:
-
Deploy schedule:
-
Configure channel in ErsatzTV UI:
- Go to https://ersatztv.bogocat.com
- Channels → Add Channel
- Set number, name, streaming mode
- Edit Playout → Change to "Sequential"
- Select schedule file:
/root/.local/share/ersatztv/schedules/newchannel.yaml
Path note: Use the container-internal path (
/root/.local/share/ersatztv/schedules/*.yaml), not the VM path.
- Verify:
- Preview channel in UI
- Check EPG for programming
Backup & Recovery¶
The sync script automatically backs up the SQLite database before writes:
# Backups stored in:
/root/tower-fleet/backups/ersatztv/ersatztv_YYYYMMDD_HHMMSS.sqlite3
# Restore (if needed):
scp /root/tower-fleet/backups/ersatztv/ersatztv_*.sqlite3 \
root@10.89.97.50:/opt/arr-stack/configs/ersatztv/ersatztv.sqlite3
ssh root@10.89.97.50 "cd /opt/arr-stack && docker compose restart ersatztv"
Troubleshooting¶
Collections not syncing¶
# Check API connectivity
curl -s http://10.89.97.50:8409/api/collections/smart | jq 'length'
# Verify YAML syntax
yq '.' /root/tower-fleet/configs/ersatztv/collections.yaml
Schedule not loading¶
-
Verify YAML syntax:
-
Check ErsatzTV logs:
-
Verify file deployed:
Channel shows "Offline"¶
- Ensure schedule has valid
contentdefinitions - Verify collections referenced in schedule exist
- Check playout has
repeat: trueat end
SQLite Reference¶
For advanced debugging or manual fixes:
# Copy database locally
scp root@10.89.97.50:/opt/arr-stack/configs/ersatztv/ersatztv.sqlite3 /tmp/
# Query collections
sqlite3 /tmp/ersatztv.sqlite3 "SELECT Id, Name FROM SmartCollection"
# Query channels
sqlite3 /tmp/ersatztv.sqlite3 "SELECT Id, Number, Name FROM Channel"
# Query fillers
sqlite3 /tmp/ersatztv.sqlite3 "SELECT Id, Name, FillerKind, FillerMode FROM FillerPreset"
Enum Reference¶
FillerKind: 1=PreRoll, 2=MidRoll, 3=PostRoll, 5=Tail, 6=Fallback
FillerMode: 1=None, 2=Count, 3=Duration, 4=Pad
PlaybackOrder: 1=Chronological, 2=Random, 3=MultiEpisodeShuffle, 4=ShuffleInOrder, 8=Shuffle
YAML Schedule Reference¶
File Locations¶
ErsatzTV runs on the arr-stack VM (Docker), NOT Kubernetes:
| Location | Path |
|---|---|
| Source (Proxmox host) | /root/tower-fleet/configs/ersatztv/schedules/*.yaml |
| arr-stack VM | /opt/arr-stack/configs/ersatztv/schedules/*.yaml |
| Inside container | /root/.local/share/ersatztv/schedules/*.yaml |
Access via SSH (not kubectl):
# Edit source
vim /root/tower-fleet/configs/ersatztv/schedules/toonami.yaml
# Deploy
/root/tower-fleet/scripts/ersatztv-sync.sh schedules
# View on VM
ssh root@10.89.97.50 "cat /opt/arr-stack/configs/ersatztv/schedules/toonami.yaml"
YAML Structure¶
# 1. CONTENT SOURCES - Define what media to use
content:
- smart_collection: "[episodes] Cowboy Bebop" # Reference existing smart collection
key: COWBOY_BEBOP # Unique key for playout reference
order: chronological # chronological or shuffle
- smart_collection: "[bumps] Generic"
key: GENERIC_BUMPS
order: shuffle
# 2. PLAYOUT INSTRUCTIONS - Define when/how to play
playout:
# Duration block - play content for specified time
- duration: "4 hours"
content: COWBOY_BEBOP # Reference key from content section
trim: true # Trim last item to fit duration
custom_title: "Anime Block" # EPG display title
# Count block - play specific number of items
- count: 1
content: COWBOY_BEBOP
custom_title: "Cowboy Bebop"
# Pad to next interval (e.g., :00 or :30)
- pad_to_next: 30
content: GENERIC_BUMPS
trim: true
# EPG grouping - combine items under one guide entry
- epg_group: true
advance: true
- count: 1
content: GENERIC_BUMPS
filler_kind: preroll
- count: 1
content: COWBOY_BEBOP
custom_title: "Cowboy Bebop"
- epg_group: false
# Loop forever
- repeat: true
Key Concepts¶
| Instruction | Purpose | Notes |
|---|---|---|
duration |
Play content for X time | "4 hours", "90 minutes" |
count |
Play N items | count: 1 plays one episode |
pad_to_next |
Fill to interval | pad_to_next: 30 pads to :00 or :30 |
pad_until |
Fill to clock time | pad_until: "6:00 PM" (requires tomorrow: true/false) |
wait_until |
Wait (gap) until time | Creates dead air - usually avoid |
epg_group |
Group EPG entries | Hides bumps under main show in guide |
repeat |
Loop schedule | Usually last instruction |
Filler Kinds¶
Used with count or duration to mark content type for EPG:
- none (default) - Normal content
- preroll - Before main content
- postroll - After main content
- midroll - Between segments
Duration Behavior¶
Important: duration blocks are relative, not anchored to clock time. If playout builds at 3 PM, a "4 hour" block runs 3-7 PM, not 6-10 AM.
To anchor to specific times, use pad_until (but this has parsing issues in some versions).
Common Issues¶
- Wrong content playing - Check smart collection queries in ErsatzTV UI
- Schedule not updating - Rebuild playout after YAML changes
- Times don't match intent -
durationis relative; usepad_untilfor clock anchoring - NCalc errors -
pad_untilmay have syntax issues; usedurationas fallback