Skip to content

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

  1. Create schedule YAML:

    vim /root/tower-fleet/configs/ersatztv/schedules/newchannel.yaml
    

  2. Deploy schedule:

    /root/tower-fleet/scripts/ersatztv-sync.sh schedules
    

  3. Configure channel in ErsatzTV UI:

  4. Go to https://ersatztv.bogocat.com
  5. Channels → Add Channel
  6. Set number, name, streaming mode
  7. Edit Playout → Change to "Sequential"
  8. 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.

  1. Verify:
  2. Preview channel in UI
  3. 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

  1. Verify YAML syntax:

    yq '.' /root/tower-fleet/configs/ersatztv/schedules/toonami.yaml
    

  2. Check ErsatzTV logs:

    ssh root@10.89.97.50 "docker logs ersatztv --tail 50"
    

  3. Verify file deployed:

    ssh root@10.89.97.50 "ls -la /opt/arr-stack/configs/ersatztv/schedules/"
    

Channel shows "Offline"

  • Ensure schedule has valid content definitions
  • Verify collections referenced in schedule exist
  • Check playout has repeat: true at 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

  1. Wrong content playing - Check smart collection queries in ErsatzTV UI
  2. Schedule not updating - Rebuild playout after YAML changes
  3. Times don't match intent - duration is relative; use pad_until for clock anchoring
  4. NCalc errors - pad_until may have syntax issues; use duration as fallback

See Also