Graft real FAST-provider EPG onto ErsatzTV live channels (merged XMLTV)
Find a file
xenarathon 42c0a246f0 README: beginner-friendly rewrite with collapsible explainers + personalization steps
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:28:56 -04:00
.gitignore Parameterize ErsatzTV URL via ETV_XMLTV_URL env; add .gitignore + config docs 2026-06-15 21:37:57 -04:00
docker-compose.yml Parameterize ErsatzTV URL via ETV_XMLTV_URL env; add .gitignore + config docs 2026-06-15 21:37:57 -04:00
LICENSE etv-epg-merge: graft FAST-provider EPG onto ErsatzTV live channels 2026-06-14 17:37:54 -04:00
merge.py Parameterize ErsatzTV URL via ETV_XMLTV_URL env; add .gitignore + config docs 2026-06-15 21:37:57 -04:00
README.md README: beginner-friendly rewrite with collapsible explainers + personalization steps 2026-06-15 22:28:56 -04:00

etv-epg-merge

Give your ErsatzTV live (FAST) channels a real TV guide. ErsatzTV only produces a proper on-screen guide for its scheduled channels — its live "remote-stream" channels just show one repeating placeholder block. This tool fetches the actual now/next listings from the FAST provider each channel mirrors (Pluto TV / Samsung TV Plus, plus broadcast PBS), grafts them onto your ErsatzTV channels, and serves the merged guide for your player (e.g. OTT Navigator).

It is read-only and safe — it never changes ErsatzTV or your media; it only fetches public guides and writes a single output file. It re-runs on a timer (every 4 hours by default) and keeps the last good guide if a refresh ever fails. It has no dependencies — just Python.

New to all this? No problem. The Quick start below is a few copy-paste steps, and every step has a click-to-expand explainer telling you what the command does and why.


Quick start

The intended way to run it is the included Docker Compose file: it builds nothing, runs the script on a timer, and serves the guide at a URL you paste into your player.

First time using a terminal? Start here

A "terminal" is a window where you type commands instead of clicking buttons.

  • Windows: install Docker Desktop, then open PowerShell (search for it in the Start menu).
  • macOS: install Docker Desktop, then open Terminal (in Applications → Utilities).
  • Linux: you likely already have a terminal and may already have Docker.

To run a command, copy it, paste it into the terminal window, and press Enter. That's it. "Docker" is just a tool that runs this program in a tidy box so you don't have to install Python or anything else yourself.

1. Get the code

git clone https://github.com/you/etv-epg-merge.git
cd etv-epg-merge
What did that do?

git clone downloads a copy of this project to your computer. cd etv-epg-merge ("change directory") moves you into the folder it created, so the next commands run in the right place. Replace the URL with wherever you got this from (e.g. your own Forgejo/GitHub address).

2. Point it at your ErsatzTV (and check the channel map)

Open docker-compose.yml and merge.py in a text editor and adjust two things — your ErsatzTV address and the channel mapping. See Personalize it to your setup for exactly what to change and how to find your values.

3. Start it

docker compose up -d
docker compose logs -f etv-epg
What do these two commands do?
  • docker compose up -d — starts the program in the background (-d = detached, so it keeps running after you close the terminal). It does not build anything; it just runs the official python image with this folder mounted inside.
  • docker compose logs -f etv-epg — shows its live output so you can watch what it's doing. You'll see lines like ch41: grafted 137 programmes from pluto/... and finally DONE: grafted real EPG onto N live channels. Press Ctrl+C to stop watching; the program keeps running.

If a channel is skipped, the log says why (e.g. skip ch20: not in ErsatzTV xmltv, or a provider returned 0 programmes). That's the place to confirm your map is correct.

4. Point your player's guide at it

In your player (e.g. OTT Navigator), keep using ErsatzTV's existing M3U for the channel list and streams, and set the EPG / XMLTV URL to:

http://<your-server>:8186/guide.xml
What's the difference between the M3U and the XMLTV URL?
  • The M3U (from ErsatzTV) is the channel list — what to play. Don't change it.
  • The XMLTV / EPG is the program guide — what's on now and next. That's what this tool produces. Because the merged guide reuses ErsatzTV's own channel ids (tvg-ids), the player lines the guide up with the right channels automatically — no extra mapping in the player.

Replace <your-server> with your server's LAN IP or hostname (e.g. 192.168.1.209). The tool serves the file on port 8186.


Personalize it to your setup

There are two things to set: where your ErsatzTV is, and which channels map to which provider feed.

Your ErsatzTV address — set the ETV_XMLTV_URL environment variable to your ErsatzTV's XMLTV endpoint. The default is http://localhost:8409/iptv/xmltv.xml. In docker-compose.yml, add it under the service's environment: (or set it in your stack's env). The provider feeds (Pluto / Samsung / epgshare01) are public and need no config.

How do I find my ErsatzTV XMLTV URL?

It's your ErsatzTV server address plus /iptv/xmltv.xml. ErsatzTV usually listens on port 8409, so on the same machine it's http://localhost:8409/iptv/xmltv.xml, and from elsewhere on your network it's http://<ErsatzTV-IP>:8409/iptv/xmltv.xml (e.g. http://192.168.1.209:8409/iptv/xmltv.xml). Open that URL in a browser to confirm it returns XML before pasting it in.

The mount pathdocker-compose.yml mounts a folder into the container with ${DOCKER_PATH}etv-epg:/app. That folder must contain merge.py, and the tool writes its output to its out/ subfolder.

What is ${DOCKER_PATH} and what do I put there?

${DOCKER_PATH} is a placeholder for wherever you keep your Docker app folders. Either define DOCKER_PATH in your environment / stack so it ends in a slash (e.g. /srv/docker/), or just replace ${DOCKER_PATH}etv-epg in docker-compose.yml with a real path like /srv/docker/etv-epg. Put this repo's merge.py in that folder; the container creates out/ itself and writes guide.xml there.

The channel map — the per-channel mapping lives in the MAP dictionary near the top of merge.py. Each line maps your ErsatzTV channel number to a (provider, provider channel id). Edit it to add, remove, or repoint channels. If you add a brand-new source, also add it to the PROV dictionary above MAP.

How do I find a provider's channel id to put in MAP?

Each MAP entry looks like "41": ("pluto", "5d14fd1a252d35decbc4080c") — that's ErsatzTV channel 41 mirrors the Pluto channel with that id.

  • The left key ("41") is the channel number as it appears in ErsatzTV.
  • The provider is one of the keys in PROV (pluto, samsung, epgshare).
  • The provider channel id comes from that provider's own XMLTV. Open the provider URL listed in PROV (the .gz ones download a gzipped XML), and look at its <channel> elements: each has a human-readable <display-name> and an id. Find the channel by name, copy its id, and use that as the value.

If a mapped channel logs 0 programmes or not in ErsatzTV xmltv, the id or the channel number is off — fix it and the next refresh will pick it up.


Settings

Variable Default What it does
ETV_XMLTV_URL http://localhost:8409/iptv/xmltv.xml Your ErsatzTV XMLTV endpoint — the only required setting.

Everything else is edited directly in merge.py:

In merge.py What it does
MAP Maps each ErsatzTV channel number to a (provider, provider channel id). This is the main thing you'll edit.
PROV The provider guide sources (Pluto, Samsung, epgshare01). Add an entry here to introduce a new source.

And in docker-compose.yml:

Setting Default What it does
ports 8186:8186 The port the guide is served on. Change the left number to serve it elsewhere.
sleep (in command) 14400 Seconds between refreshes. 14400 = every 4 hours.
volumes ${DOCKER_PATH}etv-epg:/app The folder containing merge.py; output lands in its out/.

How it works

  1. Fetch ErsatzTV's /iptv/xmltv.xml and build a lookup from each channel number to its ErsatzTV tvg-id.
  2. Fetch the provider XMLTV guides listed in PROV.
  3. For each entry in MAP: drop ErsatzTV's placeholder programmes for that channel, then graft the provider's programmes in their place, retagged to the ErsatzTV tvg-id so the player maps them automatically.
  4. Write the merged XMLTV to standard output. The container saves that to out/guide.xml and serves it over HTTP on port 8186.

The refresh runs on a loop (every 4 hours). If a run fails, the previous guide.xml is kept so your player never sees an empty guide.

I'd rather just run the script directly, without Docker

It's plain Python (3.x), standard library only — no pip install needed. It prints the merged guide to standard output, so redirect it to a file:

ETV_XMLTV_URL="http://192.168.1.209:8409/iptv/xmltv.xml" python3 merge.py > guide.xml

Progress and any skipped channels are logged to standard error, so they show in your terminal while the guide goes to the file. Serve guide.xml however you like, or just re-run on your own schedule.


Good to know

  • Coverage: the shipped MAP covers a set of live remote-stream channels using FAST sources (Pluto / Samsung via i.mjh.nz) plus epgshare01 broadcast guides for real PBS national and Create TV feeds. Add or repoint channels by editing MAP (and PROV for a new source).
  • Provider feeds change. A provider can rename or drop a channel id; when that happens the log shows 0 programmes for it. Skim docker compose logs etv-epg after the first run and re-check any channel that didn't graft.
  • Guide data is courtesy of the i.mjh.nz grabbers and epgshare01 community feeds. They're public and read-only — be a good citizen and don't refresh more often than you need to (the default 4-hour interval is plenty).
  • The tool uses an unverified SSL context for these public, read-only feeds.

License

MIT — see LICENSE. Do what you like; no warranty.