Three problems: (1) the m3u8 URL is buried behind iframes and obfuscated JS, (2) tokens expire every few hours, and (3) the upstream server checks User-Agent and Referer headers on both the playlist and .ts segments — Jellyfin doesn't send these, so you get 403.
I ended up writing three scripts:
- detect-headers.sh: give it a page URL, it follows the iframe chain, extracts the m3u8, then brute-forces header combinations on both .m3u8 and .ts requests. Tells you exactly what the stream needs.
- hls-proxy.py: single-file Python reverse proxy (stdlib only, zero pip dependencies). Injects the required headers and rewrites the m3u8 so segment requests also go through the proxy.
- refresh-m3u.sh: extracts fresh URLs before tokens expire, outputs a Jellyfin-ready M3U with logos and channel groups. Runs on a systemd timer.
~200 lines of Python, ~100 lines of bash. The proxy is the interesting part technically — it has to handle relative and absolute segment URLs, rewrite URI= in EXT tags (for encryption keys), and add CORS headers since Jellyfin's web client makes cross-origin requests.
Happy to answer questions about the approach or implementation.
It’s similar for NFL, and I assume NHL and NBA, too. I’d pay to watch the stuff I watch if it were possible, but it’s not!
The auto-learned Referer per upstream host is a nice touch. How often do the upstream sources change their header requirements on you?
https://jellyfin.org/docs/general/server/live-tv/setup-guide...
Channels DVR also supports m3u:
https://getchannels.com/docs/channels-dvr-server/how-to/cust...
The README mentions Plex and Emby, but those don't support m3u, so you need to use a proxy which makes an m3u source appear like a local tuner such as:
https://github.com/Threadfin/Threadfin
Think it could be ran from within a docker container so I could add it to an existing docker compose media server setup?