Self-hosted IPTV
Serves M3U playlists that randomly schedule video files from /movies and /series, with the same show never appearing twice in a row.
Setup
- Create
/data/channels.json(seedata/channels.json.example).
channels.json format:
channels: array of channel objects- Each channel:
id: unique id (used in URLs)name: display namepaths: list of path entries. Each entry can be:- String:
"movies"or"series/Show Name"— path to videos; EPG title and TMDB search use the folder name. - Object:
{"path": "series/W817", "display_name": "Phineas en Ferb", "tmdb_search": "Phineas and Ferb"}—pathis required;display_nameis shown in the EPG;tmdb_searchis used when querying TMDB. You can usetmdb_idinstead of (or to override)tmdb_searchfor a direct lookup, e.g.97596or"97596-kika-bob"(the numeric ID is extracted).
- String:
- Run the container with volumes:
docker build -t self-hosted-iptv .
docker run -d --name iptv -p 8080:8080 \
-v /path/to/movies:/movies:ro \
-v /path/to/series:/series:ro \
-v /path/to/data:/data \
self-hosted-iptv
When using TMDB metadata, mount /data read-write (as above) so the app can store and reuse metadata in /data/tmdb_cache/. Use -v /path/to/data:/data:ro only if you do not use TMDB.
EPG timespan: Set EPG_TIMESPAN to control how far ahead the guide is generated (default 14d). Examples: 2d, 7d, 48h. The randomized schedule is refreshed after (timespan − 3 hours) so a new block of content is ready before the current one ends. Example: -e EPG_TIMESPAN=2d gives 2 days of EPG and a new schedule every 45 hours.
Optional – EPG metadata (Dutch) from TMDB:
To show series titles, episode names, and descriptions in the TV guide in Dutch, set a TMDB API key (free after sign-up):
TMDB_API_KEY– your TMDB API key (v3).
Series are matched by folder name (e.g. series/W817 → search "W817"); episode info is matched from filenames like S01E01. All metadata is requested in Dutch (language=nl).
- Persistent cache: Fetched metadata is stored under
/data/tmdb_cache/(series search, series details, episode, episode credits). When generating the EPG, the app checks this cache first and only calls TMDB for missing entries. Mount/dataread-write (omit:ro) so the cache can be written. - Rate limit: TMDB allows 40 requests per 10 seconds. The app enforces this limit when calling the API; if the cache is warm, few or no requests are made during EPG generation.
Logging: Set LOG_LEVEL=DEBUG (e.g. in your docker run with -e LOG_LEVEL=DEBUG) to see where time is spent: schedule building (including ffprobe per video), EPG per-channel programme count and duration, and TMDB cache hits (memory/disk) vs API requests and rate-limit waits.
Stream crashes when switching episodes? If the live stream freezes or crashes when transitioning between videos (e.g. different episodes with different resolutions), enable normalization to re-encode everything to a uniform format:
NORMALIZE_LIVE_STREAM=1— enables re-encoding (uses more CPU, produces a stable continuous stream)NORMALIZE_TARGET_RES=1920x1080— target resolution (default). Use1280x720for lower CPU usage.
- In your IPTV client or Plex DVR, add:
- Playlist URL:
http://<host>:8080/playlist.m3u - EPG (XMLTV) URL:
http://<host>:8080/epg.xml
Plex will show programme titles and times for each channel.
- Playlist URL:
Each channel in the M3U points to a continuous live stream (/live/<id>): FFmpeg concatenates the channel’s randomized videos into one MPEG-TS stream and loops it. The schedule (and EPG) is built from video durations (via ffprobe) so start/stop times match what’s actually playing. The schedule is cached for (timespan − 3 h) so the EPG and live stream stay in sync and a new span is ready in time.
Endpoints
GET /— simple web index with playlist and EPG linksGET /playlist.m3u— master M3U for Plex DVR (one live stream URL per channel)GET /epg.xmlorGET /xmltv.xml— XMLTV EPG (duration set byEPG_TIMESPAN, UTC)GET /live/<channel_id>— continuous MPEG-TS stream for that channel (for tuning/recording)GET /channel/<id>/playlist.m3u— single channel M3U (list of individual video URLs)GET /stream?path=movies/...orpath=series/...— stream a single video file (audio track is auto-selected: Dutch → Flemish → default)
Shuffle behaviour
Videos are grouped by path (e.g. one group per show or the whole movies folder). Each group is shuffled, then items are interleaved round-robin so the same show never plays back-to-back.