Overview
Plex DVR speaks HDHomeRun. There are good reasons to want live TV in Plex (unified UI, recording, mobile streaming) but most IPTV sources don't natively talk to it. This project bridges the gap — a Next.js app that ingests M3U playlists, classifies channels by tier, and exposes them through HDHomeRun's discovery and lineup endpoints so Plex sees a "tuner" with whatever channels you've curated.
Stack
- Framework: Next.js 15.1, React 19, TypeScript 5.7
- Styling: Tailwind CSS 3.4
- Database: SQLite (better-sqlite3)
- Stream Processing: FFmpeg (HLS remux)
- Deployment: Docker
Key Features
- M3U playlist parsing and import (iptv-org or custom URLs)
- HDHomeRun device emulation — Plex DVR recognizes it as a tuner
- Tier-based channel classification (Canadian → International News → International Sports → Rejected)
- Stream health checking and validation
- Duplicate stream detection
- XMLTV EPG export at
/epg.xml - Source management with sync history
- Plex DVR integration (device config, channel-number assignment)
Data Flow
- Sources — iptv-org or custom M3U URLs.
- Sync — fetch M3U, parse, classify channels by tier, filter rejects.
- Streams — stored in SQLite with tier, channel numbers, enable/disable.
- Lineup — exposed via HDHomeRun API for Plex.
- EPG — XMLTV export at
/epg.xml.
Channel Tier System
- Tier 1 — Canadian (CBC, CTV, CP24, Global, CPAC, TSN 1–5, Sportsnet variants)
- Tier 2 — International News (BBC News, Sky News, CNN, Al Jazeera, France 24, DW, Euronews, NHK World)
- Tier 3 — International Sports (ESPN, beIN Sports, Sky Sports News)
- Tier 0 — Rejected (everything else)
HDHomeRun Endpoints
| Endpoint | Purpose |
|---|---|
GET /device.xml | Device description for DLNA discovery |
GET /discover.json | Device info for Plex |
GET /lineup.json | Channel lineup |
GET /lineup_status.json | Tuner status |
GET /stream/[id] | Stream proxy endpoint (FFmpeg remux for HLS) |
Database Schema (SQLite)
| Table | Notes |
|---|---|
streams | URL, tier, channel_number, is_enabled |
sources | iptv_org or custom M3U source URLs |
epg_programs | TV-guide data |
plex_settings | Device ID, tuner count |
sync_log | Sync history per source |
Stream API
| Endpoint | Methods | Purpose |
|---|---|---|
/api/streams | GET / POST | List/create streams |
/api/streams/[id] | GET / PUT / DELETE | Single stream CRUD |
/api/streams/[id]/test | GET | Test stream health |
/api/streams/import | POST | Bulk import from M3U |
/api/streams/export | GET | Export as M3U |
/api/streams/duplicates | GET / POST | Detect/manage duplicates |
/api/streams/validate | GET / POST | Batch validation |
Lessons Learned
- HDHomeRun discovery is case-sensitive for the manufacturer string — Plex won't pick up a tuner that returns the wrong casing in
discover.json. - Plex caches the lineup aggressively. Force a refresh from the DVR settings after big channel changes.
- HLS-to-MPEG-TS remux via FFmpeg is the cleanest path to broad client compatibility, at the cost of a few seconds of latency per stream.