Lightweight, mobile-first IPTV with auth, admin uploads/imports, EPG ingest, auto-import scheduling, and basic branding. Express + SQLite (better-sqlite3) + vanilla JS + hls.js.
- Auth & sessions (SQLite-backed) with admin/viewer roles; HttpOnly cookies, same-site by default, secure behind HTTPS.
- Jellyfin integration:
- “Log in with Jellyfin” on the sign-in page (uses
JELLYFIN_SERVER_URL). - Admin “Import from Jellyfin” button (requires
JELLYFIN_API_KEY) to sync users; admins stay admins; disabled users are skipped.
- “Log in with Jellyfin” on the sign-in page (uses
- Admin panels:
- Upload .m3u/.m3u8 (20MB cap).
- Import from URL and auto-split by
group-title; optional wipe before import; scheduled auto-import (default every 12h). - Manage users (create/update role/reset password/delete).
- Upload EPG (file or URL).
- Branding: set site name from the admin UI.
- Viewer app:
- Playlist/channel browsing with search, HLS playback (hls.js fallback), live/seek UI.
- Presence + connection limits (MAX_CONNECTIONS); activity/watch stats (admin endpoints).
- Stream proxy to avoid CORS/mixed-content issues; HLS manifests rewritten to proxied segment URLs.
- Mobile-friendly styling.
- Install deps
npm install- Configure
Copysample.envto.envand adjust as needed:
PORT=3000
SESSION_SECRET=change-me
ADMIN_USER=admin
ADMIN_PASS=changeme
DB_PATH=./data/theatercat.db
SESSION_COOKIE_SECURE=false # true behind HTTPS/proxy
TRUST_PROXY=0 # 1+ behind reverse proxy
MAX_CONNECTIONS=4
SITE_NAME=My IPTV # optional; also editable in admin UI
# Auto-import (optional)
AUTO_IMPORT_URL=
AUTO_IMPORT_GROUPS= # comma-separated; empty=all groups
AUTO_IMPORT_PREFIX=
AUTO_IMPORT_HOURS=12
AUTO_IMPORT_CLEAR=false # true = wipe playlists before import
# Jellyfin (auth + user import)
JELLYFIN_SERVER_URL=https://theater.cat
JELLYFIN_API_KEY= # required for import; admin key from Jellyfin
JELLYFIN_DEVICE_ID=tv.theater.cat # optional override
JELLYFIN_TIMEOUT_MS=8000 # optional
First run seeds an admin user if none exists.
- Run
npm run dev # nodemon
# or
npm start/viewer app (auth required to see playlists)/adminadmin panel (upload/import/EPG/branding)/usersadmin user management/api/admin/playlists/import-url(admin) — import & split by group/api/admin/epg(admin) — upload XMLTV (file or URL)/api/settings(public) — site name/api/admin/settings(admin) — set site name
- Set a strong
SESSION_SECRETand changeADMIN_PASS. - If behind HTTPS/proxy, set
SESSION_COOKIE_SECURE=trueandTRUST_PROXYto your hop count. - Keep
DB_PATHon persistent storage; SQLite WAL is enabled. - If using Cloudflare/NGINX/etc., proxy to your app port and forward
X-Forwarded-Proto.
- Jellyfin server:
https://theater.cat(defaults to this). IPTV app runs attv.theater.cat; device id defaults to that host. - Required env: set
JELLYFIN_SERVER_URL=https://theater.cat; optionalJELLYFIN_DEVICE_IDandJELLYFIN_TIMEOUT_MS(default 8000ms). - User import requires a Jellyfin admin API key: set
JELLYFIN_API_KEY=<token>to enable/api/admin/users/import-jellyfin(button in Users admin page). - Sign-in page includes a Log in with Jellyfin button; credentials post to the server, sessions stay HttpOnly with same-site cookies (
SESSION_COOKIE_SECURE=truein prod +TRUST_PROXYfor your proxy hops). - Disabled Jellyfin users are skipped; admins stay admins.
- Troubleshooting import: if you see “Jellyfin import is not configured (missing API key)”, ensure
.envlives in the same working directory you startnode server.jsfrom, containsJELLYFIN_API_KEY, and restart the server. Quick check:
node -e "require('dotenv').config(); console.log(process.env.JELLYFIN_API_KEY ? 'api key loaded' : 'missing api key')"should print “api key loaded”.
- Auth/session:
express-session+ SQLite store; roles are admin/viewer. Cookies are HttpOnly + same-site; setSESSION_COOKIE_SECURE=trueandTRUST_PROXYfor HTTPS/proxy. - Local login checks SQLite credentials; Jellyfin login posts to
/api/auth/login/jellyfin, calls Jellyfin/Users/AuthenticateByName, then mirrors user locally (admins stay admins). - Jellyfin import (
/api/admin/users/import-jellyfin) requiresJELLYFIN_API_KEYand syncs non-disabled users; admin policy preserved. - Playlists are stored as raw M3U in SQLite;
/api/playlistslists them with live connection counts;/api/playlists/:id/channelsparses M3U and enriches with EPG bytvg-id. - Streaming uses
/api/proxyto fetch/pipe media and rewrites HLS manifests so all segments go back through the proxy (handles CORS and referer/origin quirks). - Presence/limits: clients ping
/api/watch/ping; server tracks watchers per session and enforcesMAX_CONNECTIONS; activity pings keep sessions fresh. - EPG: XMLTV ingested via admin upload/URL, stored in SQLite, matched by
tvg-idfor “now playing” metadata.
- Playlists are snapshots; auto-import can re-fetch on a schedule if configured.
- Upload cap: 20MB for playlists. Auto-import cap: 10MB.
- Auto-import replaces playlists of the same name (and can optionally wipe all first).