Plex Live TV / Threadfin EPG¶
The tv Namespace at a Glance¶
The EPG chain that fills most of this page is one corner of a larger namespace. tv runs the full media stack — acquisition, serving and transfer — around a set of shared Ceph volumes:
flowchart LR
subgraph acq["Acquisition"]
arr["sonarr · radarr · prowlarr<br/>bazarr · autobrr · flaresolverr<br/>seerr · tdarr"]
tr["transmission"]
end
subgraph serve["Serving"]
tf["Threadfin ×2"]
pms["Plex Media Server"]
end
subgraph xfer["Transfer"]
sftp["sftpgo"]
rs["rsync"]
end
subgraph cephfs["CephFS RWX — shared library"]
lib["tv-v3-ceph<br/>(working library)"]
finish["tv-finish-film-ceph<br/>tv-finish-serie-ceph<br/>(sorted output)"]
end
scratch["Ceph RBD RWO — per-app scratch<br/>tv-incomplete-ceph · pms-docker-transcode-ceph · tdarr-temp-ceph"]
tr --> scratch --> finish
arr --> finish
tf --> pms
lib --> pms
lib --> sftp
lib --> rs
pms -.-> scratch
| Component | Role | LoadBalancer |
|---|---|---|
pms |
Plex Media Server, including Live TV / DVR | 192.168.1.46 |
transmission |
Download client | .47 (peer port) |
sftpgo (+ web UI) |
SFTP ingest / egress | .57 |
rsync |
Bulk transfer endpoint | .56 |
| sonarr · radarr · prowlarr · bazarr · autobrr · flaresolverr · seerr · tdarr | Acquisition, indexing, subtitles, request handling, transcode management | cluster-internal |
threadfin · threadfin-bis |
HDHomeRun-emulating IPTV/EPG bridges | cluster-internal |
The media library now lives entirely on the in-cluster Ceph tier via Rook — no Longhorn volume is involved in the bulk-media path any more. Three CephFS (RWX) volumes carry the shared library: tv-v3-ceph, the working tree that Plex, Threadfin, sftpgo and rsync all read from, and the tv-finish-film-ceph / tv-finish-serie-ceph pair that the acquisition side sorts finished downloads into. A separate set of Ceph RBD (RWO) volumes gives each app its own scratch space — tv-incomplete-ceph for transmission's in-flight downloads, pms-docker-transcode-ceph for Plex's transcode cache, tdarr-temp-ceph for Tdarr's working files — so no app's temporary I/O contends with another's on the same volume. See Storage Architecture for the underlying storage classes.
The EPG Chain¶
Plex DVR pulls IPTV streams from two Threadfin instances (HDHomeRun emulators):
threadfin— WebTV M3U sourcethreadfin-bis— BlueTV M3U source
Each Threadfin instance ingests a large source XMLTV feed (~13 700 channels, ~1 M programs) and emits a filtered threadfin.xml that Plex's XMLTV grabber consumes. Plex stores the resulting airings in tv.plex.providers.epg.xmltv-<uuid>.db alongside its main com.plexapp.plugins.library.db.
Threadfin channel mapping (xepg.json)¶
Each entry in /home/threadfin/conf/xepg.json represents a lineup channel. The critical field is x-mapping: it must be a real channel ID from the source XMLTV (e.g. SRF1.ch, NDR.de, BBCTwo.ch). Threadfin only emits <programme> entries for channels whose x-mapping resolves to a <channel id="X"> in the source XML.
Auto-generated values like x-mapping: "threadfin-<hex>" are placeholders that do not match any source channel — the channel renders in the lineup but its XMLTV output contains zero programs, so Plex shows "Unknown Airing" for every slot.
Symptoms of broken mappings:
| Symptom | Likely cause |
|---|---|
| All channels show "Unknown Airing" in Plex Guide | Plex EPG DB empty, or every x-mapping is threadfin-<hex> |
| Many channels show the same program | Multiple x-mapping values point to a single source ID |
| One channel shows no EPG | x-mapping: "-" or refers to a stale source channel |
The match between tvg-name / name in xepg.json and the source's <display-name> requires normalization:
- German umlauts:
Züri↔Zueri↔Zuri(source usesü; M3U providers may use either digraph) - German number-words:
eins/zwei/drei↔1/2/3(e.g.ORF1↔ORFeins.ch) - Country tags on M3U names:
TMC CH→ sourceTMC.frorTMCMonteCarlo.ch - Resolution markers: strip
HD,UHD,720p,1080pbefore comparison
Appendix — Operations & Troubleshooting¶
Everything below is runbook material for the EPG chain, kept here because when the guide breaks these are the exact levers.
Threadfin UI channel-number display¶
The web UI multiplies x-channelID by 10 for display. Stored 1149 shows as 11490 in the Mapping page. Inactive channels typically appear at these high numbers — they are the deactivated entries in xepg.json (with x-active: false), not pending source channels.
Forcing a Plex EPG re-ingest¶
Plex's XMLTV grabber refreshes on its own ~every 12 h. A pod restart alone does not force a fresh ingest if Plex thinks the current EPG is recent. To force a full re-ingest, reset the timestamps in media_provider_resources (the EPG provider row, identifier='tv.plex.providers.epg.xmltv') and restart Plex:
-- Inside Plex SQLite, against com.plexapp.plugins.library.db
UPDATE media_provider_resources
SET extra_data = json_set(
json_set(
json_set(extra_data,
'$."pv:timeOfLastRefresh"', '0'),
'$."pv:lastChunkEndedAt"', '0'),
'$."url"',
REPLACE(REPLACE(json_extract(extra_data,'$."url"'),
'pv%3AlastChunkEndedAt=<old_value>', 'pv%3AlastChunkEndedAt=0'),
'pv%3AtimeOfLastRefresh=<old_value>', 'pv%3AtimeOfLastRefresh=0')
),
updated_at = strftime('%s','now')
WHERE identifier='tv.plex.providers.epg.xmltv';
The url field inside extra_data is a URL-encoded mirror of the same JSON values — both layers must be reset for the grabber to treat the EPG as stale.
Plex re-ingest of the full ~28k-program XMLTV from both Threadfins takes 5–7 minutes (~400 s database time). Watch for EPG[xmltv]: Step 1/1 and Total time to load EPG in Plex Media Server.log.
Operating on xepg.json¶
The file is JSON — but Threadfin reads it on startup and only writes back when changed via its own UI. Editing the file on disk + restarting the pod is safe:
POD=$(kubectl -n tv get pods -l app=threadfin -o jsonpath='{.items[0].metadata.name}')
kubectl -n tv cp "$POD":/home/threadfin/conf/xepg.json /tmp/xepg.json
# edit /tmp/xepg.json (keep ownership/perms intact)
kubectl -n tv exec "$POD" -- cp /home/threadfin/conf/xepg.json /home/threadfin/conf/xepg.json.bak
kubectl -n tv cp /tmp/xepg.json "$POD":/home/threadfin/conf/xepg.json
kubectl -n tv exec "$POD" -- chown root:root /home/threadfin/conf/xepg.json
kubectl -n tv rollout restart deployment/threadfin
Threadfin regenerates threadfin.xml on startup (~1–2 min for 13 700 source channels) — wait for XEPG: Ready to use in the pod logs before triggering a Plex re-ingest.
Plex Live TV API endpoints¶
Most /livetv/dvrs/... and /tv.plex.providers.epg.xmltv:NN/refresh endpoints return 404 in current Plex versions (1.43.x+). Use the SQL timestamp-reset above instead of looking for a refresh API.