| id | deployment | ||||||
|---|---|---|---|---|---|---|---|
| type | how-to | ||||||
| status | stable | ||||||
| tags |
|
See also:
- README
- Media server integration modes
- Emby and Jellyfin Support
- Plex ops patterns
- k3s deployment
- iptvtunerr-troubleshooting
See Platform requirements and installation for FFmpeg, FUSE, and platform-specific notes before continuing.
- Provider credentials: Set in
.env(copy from.env.example) or use a subscription file. You need at least:IPTV_TUNERR_PROVIDER_USER,IPTV_TUNERR_PROVIDER_PASS,IPTV_TUNERR_PROVIDER_URL(orIPTV_TUNERR_M3U_URL)IPTV_TUNERR_BASE_URL= the URL Plex/Emby/Jellyfin will use to reach this host (e.g.http://YOUR_SERVER_IP:5004)
- Media server: On the same machine or another.
- Plex, Emby, and Jellyfin can all use the same tuner and guide endpoints.
- If you only need a standard setup, stay on this page.
- If you need advanced Plex multi-DVR or injected-DVR workflows, switch to Plex ops patterns.
IPTV Tunerr is supported as a single reachable tuner process per Plex DVR identity. Run it as a local binary, a persistent systemd service, Docker/container on a host, or a k3s workload. The media server must reach IPTV_TUNERR_BASE_URL directly, and that URL should stay stable across restarts.
For Plex, this rule matters more than the process manager:
- one active Tunerr instance per Plex DVR device identity and friendly name
- one canonical
IPTV_TUNERR_BASE_URLfor that instance - one registration owner for a given Plex DVR row
- no duplicate background jobs registering the same tuner into the same Plex server
If you intentionally run multiple DVR buckets, give each bucket a distinct base URL or port, distinct device/friendly-name settings, and a non-overlapping lineup. See Plex ops patterns before doing that.
Avoid changing the base URL, device ID, friendly name, or lineup split casually. Plex treats those as part of the DVR identity/mapping surface, so accidental duplication can create empty or stale DVR rows.
For k3s-specific notes, manifests, and Plex reachability rules, see Deploy IPTV Tunerr on k3s.
Build once:
go build -o iptv-tunerr ./cmd/iptv-tunerrRun (refresh catalog, health check, then serve):
cp .env.example .env # edit with your provider and base URL
./iptv-tunerr run -addr :5004Optional for Plex:
-register-plex=apiwithIPTV_TUNERR_PMS_URL+IPTV_TUNERR_PMS_TOKENis the normal zero-touch path;PLEX_HOST+PLEX_TOKENremains a legacy alias- DB-path registration remains available only for legacy local-Plex setups where Plex is stopped intentionally
For Emby/Jellyfin registration, see Emby and Jellyfin Support.
Or run in steps:
./iptv-tunerr index # write catalog.json
./iptv-tunerr serve -addr :5004 # tuner only (no re-index)Add tuner in Plex: Settings -> Live TV & DVR -> Set up -> Device URL = your IPTV_TUNERR_BASE_URL, Guide URL = $IPTV_TUNERR_BASE_URL/guide.xml.
After registration, confirm Plex has exactly the DVR rows you expect. For a simple setup that means one IPTV Tunerr DVR. If Plex shows empty duplicates, stop extra Tunerr processes first, then remove the empty Plex DVR rows and register again from the single intended service.
If Plex sometimes keeps Live TV sessions running after a browser tab closes or a TV app is left playing in the background, enable the built-in reaper in the same app process:
IPTV_TUNERR_PMS_URL=http://YOUR_PLEX_HOST:32400 \
IPTV_TUNERR_PMS_TOKEN=YOUR_PLEX_TOKEN \
IPTV_TUNERR_PLEX_SESSION_REAPER=1 \
IPTV_TUNERR_PLEX_SESSION_REAPER_IDLE_S=15 \
IPTV_TUNERR_PLEX_SESSION_REAPER_RENEW_LEASE_S=20 \
IPTV_TUNERR_PLEX_SESSION_REAPER_HARD_LEASE_S=1800 \
./iptv-tunerr run -addr :5004Notes:
..._IDLE_Sis the main prune timeout after activity stops...._RENEW_LEASE_Sis a renewable heartbeat lease (extra safety)...._HARD_LEASE_Sis a backstop max lifetime.- Set
IPTV_TUNERR_PLEX_SESSION_REAPER=0while debugging playback if you want zero interference.
If your upstream XMLTV feed contains mixed-language programme titles/descriptions and Plex shows non-English guide text, you can normalize the XMLTV programme nodes in-app:
IPTV_TUNERR_XMLTV_PREFER_LANGS=en,eng \
IPTV_TUNERR_XMLTV_PREFER_LATIN=true \
IPTV_TUNERR_XMLTV_NON_LATIN_TITLE_FALLBACK=channel \
./iptv-tunerr run -addr :5004Notes:
...PREFER_LANGSonly helps when the XMLTV feed includes repeated<title>/<desc>nodes withlang=attributes....PREFER_LATIN=trueprefers a Latin-script variant when multiple variants exist but no preferredlang=matches....NON_LATIN_TITLE_FALLBACK=channelreplaces a mostly non-Latin programme title with the channel name (description remains source text).
To run multiple DVR buckets in one app/container instead of one pod per bucket:
./iptv-tunerr supervise -config ./supervisor.jsonThis starts multiple child iptv-tunerr run processes with per-instance args/env from a JSON config.
If you are using this for category DVR fleets or mixed wizard-plus-injected Plex layouts, see Plex ops patterns for when those patterns are worth the extra complexity.
Important HDHR note:
- Only one child should enable
IPTV_TUNERR_HDHR_NETWORK_MODE=trueon the default HDHR ports (65001), unless you intentionally assign different HDHR ports. - HDHR LAN discovery is UDP broadcast-based; expose the HDHR ports directly on the host when using network discovery.
Prerequisites: Copy .env.example to .env and set at least:
IPTV_TUNERR_PROVIDER_USER,IPTV_TUNERR_PROVIDER_PASS,IPTV_TUNERR_PROVIDER_URL(orIPTV_TUNERR_M3U_URL)IPTV_TUNERR_BASE_URL=http://YOUR_HOST_IP:5004(the URL Plex will use to reach this tuner; use your machine's IP or hostname)
Option A — Docker Compose (recommended):
cp .env.example .env
# edit .env with your provider and IPTV_TUNERR_BASE_URL
docker compose up -d
curl -s -o /dev/null -w "%{http_code}" http://localhost:5004/discover.json # expect 200Option B — Plain docker run:
docker build -t iptv-tunerr:local .
docker run -d --name iptvtunerr -p 5004:5004 --env-file .env iptv-tunerr:local
# Or pass args to override the default (run -addr :5004): e.g. iptv-tunerr:local serve -addr :5004To enable the built-in reaper in Docker, add these to .env (or pass -e flags): IPTV_TUNERR_PMS_URL, IPTV_TUNERR_PMS_TOKEN, and IPTV_TUNERR_PLEX_SESSION_REAPER=1.
If .env is missing, docker compose up will fail with "couldn't find env file". Create it from .env.example first.
If the container exits: Run docker compose logs iptvtunerr. Common causes: missing or invalid provider credentials in .env, or IPTV_TUNERR_BASE_URL not set. Fix .env and run docker compose up -d again.
Install:
# Copy binary and config to a dedicated directory
sudo mkdir -p /opt/iptvtunerr
sudo cp iptv-tunerr /opt/iptvtunerr/
sudo cp .env.example /opt/iptvtunerr/.env
sudo chown -R YOUR_USER:YOUR_GROUP /opt/iptvtunerr
# Edit /opt/iptvtunerr/.env with your provider and IPTV_TUNERR_BASE_URL
# Install unit file
sudo cp docs/systemd/iptvtunerr.service.example /etc/systemd/system/iptvtunerr.service
# Edit if needed: WorkingDirectory, EnvironmentFile, or add Environment="IPTV_TUNERR_BASE_URL=http://YOUR_IP:5004"
# Optional reaper envs for packaged/systemd use:
# IPTV_TUNERR_PMS_URL=http://127.0.0.1:32400
# IPTV_TUNERR_PMS_TOKEN=...
# IPTV_TUNERR_PLEX_SESSION_REAPER=1
sudo systemctl daemon-reload
sudo systemctl enable --now iptvtunerrCheck:
sudo systemctl status iptvtunerr
curl -s -o /dev/null -w "%{http_code}" http://localhost:5004/discover.json # expect 200Example unit: docs/systemd/iptvtunerr.service.example.
For our own always-on Plex/Tunerr host, use systemd as the source of truth:
sudo systemctl status iptvtunerr
curl -s http://127.0.0.1:5004/discover.json | jq .
curl -s http://127.0.0.1:5004/lineup_status.json | jq .Operational rules:
- keep the active
.envunder the service working directory - keep
IPTV_TUNERR_BASE_URLpointed at the stable LAN address Plex uses - run the Plex host container with the LinuxServer Plex Pass update channel enabled (
VERSION=latestonlinuxserver/plex) so Plex can fetch the newest build the signed-in Plex Pass account is entitled to; withoutVERSION, the LSIO update routine is skipped - roll back by installing the previous binary and restarting the same service
- do not start a second registration path while the service is enabled
- when testing another instance, use a different port and do not point it at production Plex registration
Useful checks:
sudo journalctl -u iptvtunerr -n 200 --no-pager
docker inspect plex-host --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^VERSION=latest$'
docker exec plex-host /usr/lib/plexmediaserver/Plex\ Media\ Server --version
curl -s -o /dev/null -w "%{http_code}\n" "$IPTV_TUNERR_BASE_URL/discover.json"
curl -s -o /dev/null -w "%{http_code}\n" "$IPTV_TUNERR_BASE_URL/guide.xml"k3s is supported for users and lab deployments. The important rule is the same as every other process manager: do not let k3s and another local service register the same Plex DVR identity at the same time.
Use the dedicated guide:
From the repo root you can run the local test script to run tuner vet/tests, start the tuner (serve or run), wait for readiness, and smoke-check endpoints:
./scripts/iptvtunerr-local-test.sh [qa|serve|run|smoke|all]| Command | What it does |
|---|---|
| qa | go mod download, go vet, go test for the tuner package. |
| serve | Start tuner with existing catalog.json (foreground). |
| run | Start full run mode (foreground; needs .env). |
| smoke | HTTP checks of discover, lineup, guide, etc. at IPTV_TUNERR_BASE_URL (default http://$(hostname):5004). |
| all (default) | qa → start serve or run in background → wait ready → smoke. |
Override: IPTV_TUNERR_BASE_URL, IPTV_TUNERR_ADDR, IPTV_TUNERR_CATALOG_PATH, WAIT_SECS.