Skip to main content
ErsatzTV generates XMLTV (Electronic Program Guide) data for all channels. You can fully customize how programs appear in the EPG using Scriban templates.

Overview

The EPG system uses Scriban templates to generate XMLTV data. Each media type (movies, episodes, music videos, etc.) has its own template that controls what appears in the guide.

Template Locations

Default templates (included with ErsatzTV):
ErsatzTV/Resources/Templates/
  _channel.sbntxt        # Channel definition
  _episode.sbntxt        # TV episodes
  _movie.sbntxt          # Movies
  _musicVideo.sbntxt     # Music videos
  _song.sbntxt           # Audio-only songs
  _otherVideo.sbntxt     # Other videos
  _remoteStream.sbntxt   # Remote streams
Custom templates (user overrides):
/config/templates/
  channel.sbntxt         # Override channel template
  episode.sbntxt         # Override episode template
  movie.sbntxt           # Override movie template
  musicVideo.sbntxt      # Override music video template
  song.sbntxt            # Override song template
  otherVideo.sbntxt      # Override other video template
  remoteStream.sbntxt    # Override remote stream template
Default templates start with an underscore (_). Create custom templates WITHOUT the underscore to override defaults. Never edit the default templates directly.

Guide Mode

ErsatzTV supports two guide modes that control EPG visibility:
All scheduled content appears in the EPG guide.
GuideMode: Normal
Use cases:
  • Standard TV channels
  • When viewers should see all content

Setting Guide Mode

In Classic Schedules:
  1. Edit schedule item
  2. Set Guide Mode to Normal or Filler
  3. Filler items won’t appear in EPG but will still play
In Block Schedules: Guide mode is determined by the deco configuration and block item settings.

Movie Template

Default Movie Template

movie.sbntxt
<programme start="{{ programme_start }}" stop="{{ programme_stop }}" channel="{{ channel_id }}">
  {{ if has_custom_title }}
      <title lang="en">{{ custom_title }}</title>
  {{ else }}
      <title lang="en">{{ movie_title }}</title>
      {{ if movie_has_plot }}
        <desc lang="en">{{ movie_plot }}</desc>
      {{ end }}
      {{ if movie_has_year }}
        <date>{{ movie_year }}</date>
      {{ end }}
      <category lang="en">Movie</category>
      {{ for genre in movie_genres }}
        <category lang="en">{{ genre }}</category>
      {{ end }}
      {{ if movie_has_artwork }}
        <icon src="{{ movie_artwork_url }}" />
      {{ end }}
  {{ end }}
  {{ if movie_has_content_rating }}
    {{ for rating in movie_content_rating | string.split '/' }}
        {{ if rating | string.starts_with 'us:' }}
          <rating system="MPAA">
        {{ else }}
          <rating>
        {{ end }}
          <value>{{ rating | string.replace 'us:' '' }}</value>
        </rating>
    {{ end }}
  {{ end }}
  <previously-shown />
</programme>

Available Movie Variables

VariableTypeDescription
programme_startstringStart time (XMLTV format)
programme_stopstringStop time (XMLTV format)
channel_idstringChannel identifier
channel_id_legacystringLegacy channel ID
channel_numberstringChannel number
has_custom_titleboolTrue if custom title set
custom_titlestringCustom EPG title
movie_titlestringMovie title
movie_has_plotboolTrue if plot exists
movie_plotstringMovie plot/description
movie_has_yearboolTrue if year exists
movie_yearintRelease year
movie_genresarrayList of genres
movie_has_artworkboolTrue if artwork exists
movie_artwork_urlstringPoster URL
movie_has_content_ratingboolTrue if rating exists
movie_content_ratingstringContent rating
movie_guidsarrayExternal IDs (IMDb, TMDb, etc.)

Episode Template

Default Episode Template

episode.sbntxt
<programme start="{{ programme_start }}" stop="{{ programme_stop }}" channel="{{ channel_id }}">
  {{ if has_custom_title }}
      <title lang="en">{{ custom_title }}</title>
  {{ else }}
      <title lang="en">{{ show_title }}</title>
      {{ if episode_has_title }}
        <sub-title lang="en">{{ episode_title }}</sub-title>
      {{ end }}
      {{ if episode_has_plot }}
        <desc lang="en">{{ episode_plot }}</desc>
      {{ end }}
      <category lang="en">Series</category>
      {{ for genre in show_genres }}
        <category lang="en">{{ genre }}</category>
      {{ end }}
      {{ if episode_has_artwork }}
        <icon src="{{ episode_artwork_url }}" />
        <image type="poster" size="3">{{ episode_artwork_url }}</image>
      {{ end }}
      {{ if episode_has_thumbnail }}
        <image type="still" orient="L" size="3">{{ episode_thumbnail_url }}</image>
      {{ end }}
  {{ end }}
  <episode-num system="onscreen">S{{ season_number | math.format '00' }}E{{ episode_number | math.format '00' }}</episode-num>
  <episode-num system="xmltv_ns">{{ season_number - 1 }}.{{ episode_number - 1 }}.0/1</episode-num>
  {{ if show_has_content_rating }}
    {{ for rating in show_content_rating | string.split '/' }}
        {{ if rating | string.downcase | string.starts_with 'us:' }}
          <rating system="VCHIP">
        {{ else }}
          <rating>
        {{ end }}
          <value>{{ rating | string.replace 'us:' '' | string.replace 'US:' '' }}</value>
        </rating>
    {{ end }}
  {{ end }}
  <previously-shown />
</programme>

Available Episode Variables

VariableTypeDescription
programme_startstringStart time
programme_stopstringStop time
channel_idstringChannel identifier
has_custom_titleboolCustom title flag
custom_titlestringCustom EPG title
show_titlestringTV show title
episode_has_titleboolEpisode title exists
episode_titlestringEpisode title
episode_has_plotboolEpisode plot exists
episode_plotstringEpisode description
show_has_yearboolShow year exists
show_yearintShow release year
show_genresarrayShow genres
episode_has_artworkboolEpisode poster exists
episode_artwork_urlstringEpisode poster URL
episode_has_thumbnailboolEpisode thumbnail exists
episode_thumbnail_urlstringEpisode still URL
season_numberintSeason number
episode_numberintEpisode number
show_has_content_ratingboolShow rating exists
show_content_ratingstringShow content rating
show_guidsarrayShow external IDs
episode_guidsarrayEpisode external IDs

Music Video Template

Available Music Video Variables

VariableTypeDescription
artist_titlestringArtist name
music_video_titlestringSong/video title
music_video_has_plotboolDescription exists
music_video_plotstringVideo description
music_video_has_yearboolRelease year exists
music_video_yearintRelease year
music_video_genresarrayVideo genres
artist_genresarrayArtist genres
music_video_has_artworkboolArtwork exists
music_video_artwork_urlstringArtwork URL
music_video_has_trackboolTrack number exists
music_video_trackintTrack number
music_video_has_albumboolAlbum exists
music_video_albumstringAlbum name
music_video_has_release_dateboolRelease date exists
music_video_release_datestringRelease date
music_video_all_artistsarrayAll artists (featuring, etc.)
music_video_studiosarrayStudios/labels
music_video_directorsarrayVideo directors

Custom EPG Data

You can expose custom data to the graphics engine using the etv: XML namespace:
<programme start="{{ programme_start }}" stop="{{ programme_stop }}" channel="{{ channel_id }}">
  <title lang="en">{{ movie_title }}</title>
  
  <!-- Custom data for graphics engine -->
  <etv:custom_field>{{ movie_year }}</etv:custom_field>
  <etv:rating_text>{{ movie_content_rating }}</etv:rating_text>
</programme>
All etv: nodes are:
  • Passed to the graphics engine as EPG template data
  • Stripped from XMLTV when requested by clients
  • Available as strings (not numbers) in graphics templates

XMLTV Settings

Configure EPG behavior in Settings > General:

XMLTV Days to Build

Controls how many days of EPG data to generate.
XMLTV Days To Build: 3
  • Must be ≤ Playout Days To Build
  • Smaller values improve performance
  • Larger values provide more guide data
Set Playout Days To Build to 7 for scheduling, but XMLTV Days To Build to 1-2 for performance if your IPTV client doesn’t need a full week of EPG data.

XMLTV Time Zone

Controls time zone in generated XMLTV:
Uses local server time zone (from TZ environment variable)
XMLTV Time Zone: Local

XMLTV Block Behavior

Controls how block schedules appear in EPG:
Default behavior. Block time is split evenly among all visible items.
XMLTV Block Behavior: Split Time Evenly
Example:
  • 1-hour block with 3 visible items
  • Each item shows as 20 minutes in EPG

Instance ID

Disambiguate EPG data when running multiple ErsatzTV instances:
ETV_INSTANCE_ID=home
Channel identifiers become:
100.home.ersatztv.org  # Instead of 100.ersatztv.org

Accessing XMLTV Data

URLs

Without authentication:
http://localhost:8409/iptv/xmltv.xml
With access token:
http://localhost:8409/iptv/xmltv.xml?access_token=YOUR_TOKEN
Behind reverse proxy with base URL:
http://yourdomain.com/ersatztv/iptv/xmltv.xml

M3U Playlist

The channel playlist already includes the XMLTV URL:
http://localhost:8409/iptv/channels.m3u

Advanced Template Examples

Add Director to Movie EPG

<programme start="{{ programme_start }}" stop="{{ programme_stop }}" channel="{{ channel_id }}">
  <title lang="en">{{ movie_title }}</title>
  {{ if movie_has_plot }}
    <desc lang="en">{{ movie_plot }}</desc>
  {{ end }}
  
  <!-- Add director if available -->
  {{ if movie_directors }}
    {{ for director in movie_directors }}
      <credits>
        <director>{{ director }}</director>
      </credits>
    {{ end }}
  {{ end }}
  
  <category lang="en">Movie</category>
</programme>
Check default templates for available variables like movie_directors, movie_actors, etc. These may vary by media type.

Conditional Categories

{{ if movie_year >= 2020 }}
  <category lang="en">New Release</category>
{{ else if movie_year >= 2010 }}
  <category lang="en">Recent</category>
{{ else }}
  <category lang="en">Classic</category>
{{ end }}

Multiple Images

{{ if episode_has_artwork }}
  <icon src="{{ episode_artwork_url }}" />
  <image type="poster" size="3">{{ episode_artwork_url }}</image>
{{ end }}
{{ if episode_has_thumbnail }}
  <image type="still" orient="L" size="3">{{ episode_thumbnail_url }}</image>
{{ end }}

Scriban Functions

Useful Scriban functions for templates:

String Functions

{{ movie_title | string.upcase }}           # UPPERCASE
{{ movie_title | string.downcase }}         # lowercase
{{ movie_title | string.capitalize }}       # Capitalize
{{ movie_plot | string.truncate 100 }}      # Limit to 100 chars
{{ movie_title | string.replace "The" "" }} # Remove "The"

Math Functions

{{ season_number | math.format '00' }}      # Zero-pad: 01, 02
{{ movie_year + 10 }}                       # Add 10 to year
{{ episode_number | math.abs }}             # Absolute value

Array Functions

{{ movie_genres | array.first }}            # First genre
{{ movie_genres | array.last }}             # Last genre
{{ movie_genres | array.size }}             # Count genres

Troubleshooting

Template Syntax Errors

Check ErsatzTV logs for template parsing errors:
docker-compose logs -f ersatztv | grep template
Common issues:
  • Missing {{ end }} tags
  • Unclosed {{ if }} blocks
  • Typos in variable names

EPG Not Updating

  1. Trigger manual rebuild: Edit and save any schedule
  2. Check XMLTV URL: Visit http://localhost:8409/iptv/xmltv.xml directly
  3. Clear IPTV client cache: Some clients cache EPG data

Missing Program Information

  1. Verify metadata: Check if source media has the data (NFO files, media server)
  2. Deep scan library: Trigger a deep scan to update metadata
  3. Check template variables: Use {{ if variable_exists }} guards

500 Errors Serving XMLTV

Fixed in v26.2.0+. If experiencing issues:
  • Update to latest ErsatzTV version
  • Check for concurrent XMLTV reads/writes in logs
  • Ensure sufficient disk space in /config

Next Steps

Graphics Engine

Use EPG data in custom graphics overlays

Channel Configuration

Configure channel settings

Classic Schedules

Set guide mode and custom titles

Scriban Documentation

Learn more about Scriban template syntax

Build docs developers (and LLMs) love