This project provides a simple IPTV proxy that aggregates multiple sources (M3U playlists and HDHomeRun tuners) and merges them into a unified M3U and XMLTV feed. This is ideal for use with media frontends like Plex, Jellyfin, or Emby.
- 🧩 Merge multiple M3U sources into a single playlist
- 🗓️ Merge multiple EPG sources (including local files) into a unified
xmltv.xml - 📺 Canonical channel workflows to publish channels, choose preferred streams, and control guide/output settings
- 🧠 Fallback guide info via guide number when
tvg_idis missing - 🔁 HTTP server that hosts
/lineup.m3uand/xmltv.xml - 🛡️ Robust error handling for malformed sources and network failures
- 🔄 Graceful handling of invalid M3U entries and XML data
- 🌐 Full reverse proxy support with
X-Forwarded-*headers - 💚 Health check endpoints for monitoring and orchestration (liveness, readiness)
- 🎯 NEW: Smart channel mapping with fuzzy matching suggestions
- 🔍 NEW: Automatic duplicate channel detection
- ✅ NEW: EPG validation with coverage analysis
- 🔧 NEW: Dynamic channel management API (reorder, rename, group)
- ⚡ NEW: Advanced caching system with configurable TTL for EPG and M3U data
- 👁️ NEW: Live preview API to test configuration changes before saving
- 💾 NEW: Config backup & restore API to snapshot and recover app state plus compatibility exports
- 📜 NEW: Stream usage history tracking with session duration
- 🔔 NEW: Webhook notifications on channel/EPG refresh events
- 🚦 NEW: Rate limiting on public playlist and guide endpoints
This project was inspired by xTeVe and Threadfin, but I wanted something a little lighter and had better control over using the feeds through reverse proxies.
git clone https://github.com/cbulock/iptv-proxy
cd iptv-proxy
npm installnpm startBy default, the server runs on http://localhost:34400 and serves:
http://localhost:34400/lineup.m3uhttp://localhost:34400/lineup.jsonhttp://localhost:34400/xmltv.xmlhttp://localhost:34400/mcp
These public endpoints serve the Default Output profile. Named profiles can also be published with profile-specific paths such as:
http://localhost:34400/profiles/bedroom-tv/lineup.m3uhttp://localhost:34400/profiles/bedroom-tv/lineup.jsonhttp://localhost:34400/profiles/bedroom-tv/xmltv.xml
To use a custom port, set the PORT environment variable:
PORT=8080 npm startIPTV Proxy exposes a Model Context Protocol endpoint at POST /mcp for AI agents and other MCP-compatible clients. The MCP surface is designed for discovery, diagnostics, and controlled channel/output management without scraping the admin UI.
The MCP interface includes tools for:
- understanding the recommended workflow (
get_agent_workflow) - checking current system readiness (
diagnose_agent_readiness) - listing providers, channels, canonical channels, bindings, guide bindings, and output profiles
- reading guide data and source status
- updating preferred streams, guide bindings, canonical publish state, and output-profile channel settings
- reloading channels and EPG data
/mcp has two modes:
- if
admin_authis not configured, you can callPOST /mcpdirectly - if
admin_authis configured and OAuth clients are configured,/mcprequires an OAuth bearer token with themcpscope
The admin UI remains session-based. OAuth is used specifically for MCP clients.
When app.yaml defines oauth.clients, IPTV Proxy exposes:
GET /.well-known/oauth-authorization-serverGET /.well-known/openid-configurationGET /.well-known/oauth-protected-resource/mcpGET /oauth/authorizePOST /oauth/tokenPOST /oauth/revoke
The server currently supports:
- authorization code flow
- PKCE (
S256andplain) - public clients (
token_endpoint_auth_method: none) - opaque bearer access tokens for the
mcpscope
Add OAuth client definitions to app.yaml:
oauth:
authorization_code_ttl_seconds: 300
access_token_ttl_seconds: 3600
clients:
- client_id: 'chatgpt'
client_name: 'ChatGPT'
redirect_uris:
- 'https://chat.openai.com/aip/oauth/callback'
scope: 'mcp'Optional:
oauth.issuer- override the externally visible issuer URL used in discovery metadata
If oauth.issuer is omitted, IPTV Proxy uses app.base_url when present, otherwise it derives the issuer from the incoming request headers.
- endpoint:
POST /mcp - protocol: JSON-RPC over MCP Streamable HTTP
- unsupported methods like
GET /mcpreturn405 Method Not Allowed - responses include human-readable
contentplus machine-friendlystructuredContent - when bearer auth is required and missing,
/mcpreturns401with aWWW-Authenticate: Bearer ...challenge
curl -X POST http://localhost:34400/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'curl http://localhost:34400/.well-known/oauth-authorization-servercurl -X POST http://localhost:34400/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&client_id=chatgpt&code=YOUR_CODE&redirect_uri=https%3A%2F%2Fchat.openai.com%2Faip%2Foauth%2Fcallback&code_verifier=YOUR_CODE_VERIFIER"curl -X POST http://localhost:34400/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_agent_workflow",
"arguments": {}
}
}'curl -X POST http://localhost:34400/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "diagnose_agent_readiness",
"arguments": {}
}
}'curl -X POST http://localhost:34400/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "list_channels",
"arguments": {
"source": "HDHomeRun",
"limit": 25
}
}
}'Successful tool calls return structuredContent with a stable envelope:
{
"ok": true,
"tool": "diagnose_agent_readiness",
"summary": "Readiness is review-recommended with 2 diagnostic issue(s).",
"data": {},
"sideEffects": [],
"nextSuggestedTools": ["list_channel_bindings", "list_guide_bindings"]
}Error results set isError: true and return:
{
"ok": false,
"tool": "get_guide",
"error": {
"code": "epg-not-loaded",
"message": "EPG data is not available yet. Try again after the EPG has been loaded."
},
"sideEffects": [],
"nextSuggestedTools": ["get_status", "reload_epg"]
}- Discover OAuth metadata (when auth is enabled)
- Complete the authorization code + PKCE flow and get a bearer token for the
mcpscope - Call
get_agent_workflow - Call
diagnose_agent_readiness - Inspect state with read tools such as
list_providers,list_channel_bindings,list_guide_bindings, andlist_output_profile_entries - Apply targeted mutations only after inspection
- Re-run diagnostics or status tools after changes
Runtime configuration lives in SQLite, with compatibility import/export through providers.yaml, channel-map.yaml, and app.yaml. The admin UI is the primary authoring surface: use Sources for upstream feeds and Channels for output-profile enabled state, preferred streams, guide bindings, and guide-number authoring.
This file remains the compatibility import/export format for application settings. The live app settings store is SQLite, and the server keeps app.yaml updated with the same shape for portability and backup workflows.
# Admin Authentication (optional)
# Enable to protect the admin UI and API endpoints
# Password MUST be a bcrypt hash - generate with: node scripts/hash-password.js your-password
admin_auth:
username: 'admin'
password: '$2a$10$XxXxXxXxXxXxXxXxXxXxXuXxXxXxXxXxXxXxXxXxXxXxXxXxXx' # bcrypt hash
# Session Secret (auto-generated and saved on first run if absent)
# Override to rotate the secret or migrate between instances.
# session_secret: "<auto-generated on first run>"
# Base URL (optional)
# Set when running behind a reverse proxy
# base_url: "https://iptv.example.com"
# Cache Configuration (optional)
# cache:
# epg_ttl: 21600 # EPG cache TTL in seconds (default: 6 hours)
# m3u_ttl: 3600 # M3U cache TTL in seconds (default: 1 hour)Authentication:
- When
admin_authis configured, the admin UI and all management API endpoints require session-based authentication - Access the admin UI at
/admin— you will be redirected to the login page if not authenticated - Protects endpoints:
/,/admin,/api/config/*,/api/reload/*,/api/scheduler/*,/api/mapping/*,/api/channel-health/*,/api/usage/*,/api/channels/*,/api/cache/* - When both
admin_authandoauth.clientsare configured,/mcprequires OAuth bearer tokens instead of the admin session cookie - Media endpoints (M3U playlist, XMLTV guide, streams) remain accessible without authentication
- Important: Passwords must be bcrypt hashed for security
Session Secret:
- The server automatically generates and saves a persistent session secret to
app.yamlon first run:session_secret: '<auto-generated 64-char hex value>'
- You can also set it manually (useful when rotating or migrating):
Then add to
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"app.yaml:session_secret: 'your-64-char-hex-value-here'
- Minimum length is 32 characters. Shorter values are ignored and a new secret is auto-generated
- If writing to
app.yamlfails (e.g., read-only filesystem), a warning is logged and a per-process random secret is used — existing session cookies will be invalidated on each restart because the signing key changes - Note: even with a persistent secret, session data (login state) is held in memory and lost on restart because the default MemoryStore is used. This is acceptable for single-instance deployments.
Password Hashing:
- Generate a bcrypt hash using the included utility script:
node scripts/hash-password.js your-password
- Copy the generated hash into your
app.yaml:admin_auth: username: 'admin' password: '$2a$10$XxXxXxXxXxXxXxXxXxXxXuXxXxXxXxXxXxXxXxXxXxXxXxXxXx'
- Plaintext passwords are not supported - they will be rejected with an error message
Security Best Practices:
- Treat
app.yamlas sensitive and never commit credentials to version control - Restrict file permissions on
app.yaml(e.g.,chmod 600 config/app.yaml) so only the service user can read it - For production deployments, consider loading credentials from environment variables or a secrets manager
- Always use HTTPS when accessing the admin UI remotely to protect credentials in transit
- Add
config/app.yamlto your.gitignoreif it contains real credentials
Define all channel sources here. Each provider combines an M3U or HDHomeRun channel source with an optional EPG (XMLTV) source.
providers:
- name: 'ErsatzTV'
url: 'https://ersatztv.local/iptv/channels.m3u'
type: 'm3u'
epg: 'https://ersatztv.local/iptv/xmltv.xml'
- name: 'HDHomeRun'
url: 'http://antenna.local'
type: 'hdhomerun'
- name: 'Premium IPTV'
url: 'https://example.com/playlist.m3u'
type: 'm3u'
epg: 'https://example.com/epg.xml'Provider fields:
name- Display name for the provider (used as group-title in the output)url- URL of the M3U playlist or HDHomeRun device base URLtype- Provider type:m3u(default) orhdhomerunepg- (Optional) XMLTV URL providing EPG data for this provider's channels
This file remains the compatibility import/export format for channel mappings. The primary editing workflow now lives in the admin UI's Channels tab and canonical/output-profile APIs; channel-map.yaml is mainly for portability, bootstrapping, and bulk compatibility edits.
'The Simpsons':
number: '104'
tvg_id: 'C3.147.ersatztv.org'
group: 'Entertainment'
'Evening Comedy':
number: '120'
tvg_id: 'C20.194.ersatztv.org'
name: 'Comedy Channel'
'FOX 47':
number: '47'
tvg_id: '47.1'
logo: 'http://example.com/logo.png'Available mapping fields:
name- Override the display namenumber- Set the guide/channel numbertvg_id- Set or override the tvg-idlogo- Set or override the logo URLgroup- Set the group-title (category) for the channelurl- Override the stream URL
nameis tried first when applying the mapping.- If no match is found,
tvg_idis tried. - If neither is matched, the original data is used.
- If no
tvg_idis present after mapping, theguideNumberis used as a fallback.
You can build and run IPTV-Proxy in a container. The folder /config contains your YAML compatibility files and /data contains the SQLite app store plus generated artifacts, so you must mount both directories from your host.
docker build -t iptv-proxy .docker run -d \
--name iptv-proxy \
-p 34400:34400 \
-v /absolute/path/to/your/project/config:/config \
-v /absolute/path/to/your/project/data:/data \
iptv-proxyPermissions are handled automatically — the container will ensure the mounted directories are writable on startup.
Optional: Run with a specific user ID (recommended for multi-user systems):
# Get your UID
id -u # e.g., 1000
docker run -d \
--name iptv-proxy \
-p 34400:34400 \
-e USER_ID=1000 \
-e GROUP_ID=1000 \
-v /absolute/path/to/your/project/config:/config \
-v /absolute/path/to/your/project/data:/data \
iptv-proxyThis will run the container as your user, so files are owned by you instead of root.
If a provider entry uses type: hdhomerun, the server will automatically:
- Fetch
discover.json - Build a fake M3U playlist
- Tag channels with device info
This allows you to use OTA tuners like any other playlist source.
IPTV Proxy is designed to work seamlessly behind reverse proxies like nginx, Caddy, or Traefik. The application automatically detects the correct base URL from forwarded headers.
server {
listen 80;
server_name iptv.example.com;
location / {
proxy_pass http://localhost:34400;
proxy_http_version 1.1;
# Forward client information
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# WebSocket support (for admin UI)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}For HTTPS with Let's Encrypt:
server {
listen 443 ssl http2;
server_name iptv.example.com;
ssl_certificate /etc/letsencrypt/live/iptv.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/iptv.example.com/privkey.pem;
location / {
proxy_pass http://localhost:34400;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Caddy automatically handles headers and SSL certificates:
iptv.example.com {
reverse_proxy localhost:34400
}With a subfolder path:
example.com {
reverse_proxy /iptv/* localhost:34400
}services:
iptv-proxy:
image: ghcr.io/cbulock/iptv-proxy:latest
container_name: iptv-proxy
volumes:
- ./config:/config
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.iptv.rule=Host(`iptv.example.com`)'
- 'traefik.http.routers.iptv.entrypoints=websecure'
- 'traefik.http.routers.iptv.tls.certresolver=letsencrypt'
- 'traefik.http.services.iptv.loadbalancer.server.port=34400'Here's a complete Docker Compose configuration:
version: '3.8'
services:
iptv-proxy:
image: ghcr.io/cbulock/iptv-proxy:latest
container_name: iptv-proxy
restart: unless-stopped
ports:
- '34400:34400'
volumes:
- ./config:/config
- ./data:/data
environment:
- TZ=America/New_York
# Optional: Set explicit base URL if auto-detection doesn't work
# - BASE_URL=https://iptv.example.com
healthcheck:
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:34400/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s- Go to Settings → Live TV & DVR
- Click "Set Up Plex DVR"
- Enter the tuner URL:
http://your-server:34400/ - Plex will auto-detect the HDHomeRun-compatible lineup
- Enter the EPG URL:
http://your-server:34400/xmltv.xml - Complete the channel mapping in Plex
- Go to Dashboard → Live TV
- Add a new "Tuner Device"
- Select "M3U Tuner"
- Enter the M3U URL:
http://your-server:34400/lineup.m3u - Enter the EPG URL:
http://your-server:34400/xmltv.xml - Save and refresh guide data
- Go to Settings → Live TV
- Click "Add" under TV Sources
- Select "M3U Playlist"
- Enter the M3U URL:
http://your-server:34400/lineup.m3u - Enter the EPG URL:
http://your-server:34400/xmltv.xml - Configure refresh intervals and save
IPTV Proxy includes an advanced caching system to improve performance and reduce load on upstream sources.
Add cache settings to your app.yaml:
cache:
# EPG cache TTL in seconds (default: 21600 = 6 hours)
epg_ttl: 21600
# M3U cache TTL in seconds (default: 3600 = 1 hour)
m3u_ttl: 3600Setting TTL to 0 disables automatic expiration (cache persists until manually cleared).
GET /api/cache/stats- View cache statistics and hit ratesPOST /api/cache/clear- Clear all cachesPOST /api/cache/clear/:name- Clear specific cache (e.g.,epg,m3u)PUT /api/cache/ttl/:name- Update TTL for specific cache
Example: View cache statistics
curl http://localhost:34400/api/cache/statsExample: Clear EPG cache
curl -X POST http://localhost:34400/api/cache/clear/epgExample: Update M3U cache TTL to 2 hours
curl -X PUT http://localhost:34400/api/cache/ttl/m3u \
-H "Content-Type: application/json" \
-d '{"ttl": 7200}'Test configuration changes before saving them with the preview API.
curl -X POST http://localhost:34400/api/preview/m3u \
-H "Content-Type: application/json" \
-d '{
"m3uConfig": {
"urls": [
{
"name": "Test Source",
"url": "https://example.com/playlist.m3u"
}
]
},
"channelMapConfig": {
"Channel Name": {
"number": "100",
"tvg_id": "custom-id"
}
}
}'Returns the merged M3U playlist with your temporary configuration applied.
curl -X POST http://localhost:34400/api/preview/channels \
-H "Content-Type: application/json" \
-d '{
"m3uConfig": { ... },
"channelMapConfig": { ... }
}'Returns channel data as JSON for inspection before saving.
curl -X POST http://localhost:34400/api/preview/epg \
-H "Content-Type: application/json" \
-d '{
"epgConfig": {
"urls": [
{
"name": "Test EPG",
"url": "https://example.com/xmltv.xml"
}
]
},
"channels": [...]
}'Returns the merged XMLTV with your temporary configuration applied.
Create timestamped snapshots of the SQLite app state and exported compatibility files, then restore them if needed. All endpoints require authentication.
Create a backup:
curl -X POST http://localhost:34400/api/config/backup \
-H "Cookie: <session-cookie>"
# Response: { "status": "created", "name": "backup-2026-01-01T12-00-00", "files": [...] }List backups:
curl http://localhost:34400/api/config/backups \
-H "Cookie: <session-cookie>"
# Response: { "backups": [{ "name": "backup-2026-01-01T12-00-00" }], "count": 1 }Restore a backup:
curl -X POST http://localhost:34400/api/config/backups/backup-2026-01-01T12-00-00/restore \
-H "Cookie: <session-cookie>"
# Response: { "status": "restored", "name": "...", "files": [...] }Delete a backup:
curl -X DELETE http://localhost:34400/api/config/backups/backup-2026-01-01T12-00-00 \
-H "Cookie: <session-cookie>"
# Response: { "status": "deleted", "name": "..." }Backups are stored under data/backups/, include the SQLite database snapshot plus compatibility exports, and are protected against path traversal attacks.
In addition to the active stream view, a history of recently completed stream sessions is available.
View recently completed sessions:
curl http://localhost:34400/api/usage/history \
-H "Cookie: <session-cookie>"Example response:
{
"history": [
{
"ip": "192.168.1.10",
"channelId": "23.1",
"name": "PBS",
"startedAt": "2026-01-01T12:00:00.000Z",
"endedAt": "2026-01-01T12:45:00.000Z",
"durationSeconds": 2700
}
],
"count": 1
}Sessions are returned in reverse-chronological order. The last 100 completed sessions are kept in memory.
Configure outbound HTTP webhooks to be notified when channels or the EPG are refreshed. This enables integration with external automation (e.g. Home Assistant, n8n, custom scripts).
Add a webhooks array to your app.yaml:
webhooks:
- url: https://example.com/hook
events: # optional – omit to receive all events
- channels.refreshed
- epg.refreshed
timeout_ms: 5000 # optional, default 5000Each webhook receives a POST request with the following JSON body:
{
"event": "channels.refreshed",
"timestamp": "2026-01-01T12:00:00.000Z",
"data": {}
}Available events: channels.refreshed, epg.refreshed. Delivery failures are logged but never interrupt the refresh operation.
Public playlist and guide endpoints have per-IP rate limiting to prevent abuse from misconfigured clients or scrapers. Requests from localhost (127.0.0.1 / ::1) are always exempt.
| Endpoint | Limit |
|---|---|
GET /lineup.json |
60 req/min |
GET /lineup.m3u |
60 req/min |
GET /xmltv.xml |
30 req/min |
When the limit is exceeded the server responds with 429 Too Many Requests.
PORT- HTTP server port (default: 34400)CONFIG_PATH- Configuration directory (default:./config)NODE_ENV- Node environment (default:production)
For more detailed configuration examples covering edge cases, see the config/examples/ directory:
providers.example.yaml- Comprehensive provider source examples (M3U, HDHomeRun, EPG)channel-map.example.yaml- Advanced channel mapping scenariosapp.example.yaml- Application settings and scheduler configuration
Problem: M3U playlist is empty or channels are missing.
Solutions:
- Check that your M3U sources are accessible:
curl -I http://your-source/playlist.m3u
- Review server logs for source fetch errors
- Verify config files are valid YAML (use a YAML validator)
- Ensure source URLs in
providers.yamlare correct - Check API status endpoint:
http://localhost:34400/status
Problem: Program guide is empty in your media player.
Solutions:
- Verify channel IDs match between M3U and XMLTV:
- M3U channels need
tvg-idattribute - XMLTV must have
<channel id="...">matching the tvg-id
- M3U channels need
- Check EPG sources are accessible and contain data
- Use the admin Channels tab to choose the output guide source and effective guide number, or update
channel-map.yamlwhen you need compatibility import/export - Force EPG refresh:
POST http://localhost:34400/api/reload/epg - Inspect the merged XMLTV:
curl http://localhost:34400/xmltv.xml | head -100
Problem: HDHomeRun tuner doesn't show up in channel list.
Solutions:
- Verify the device is on your network:
ping hdhomerun-device.local - Test the discover endpoint:
curl http://device-ip/discover.json - Ensure
type: "hdhomerun"is set inproviders.yaml - Check firewall rules aren't blocking access
- Try using IP address instead of hostname
Problem: Generated M3U contains wrong server addresses.
Solutions:
- Set explicit
base_urlinapp.yaml:base_url: 'https://iptv.example.com'
- Ensure reverse proxy forwards headers correctly:
- X-Forwarded-Proto
- X-Forwarded-Host
- X-Forwarded-For
- Check that your reverse proxy configuration matches the examples above
- Test URL generation:
curl -v http://localhost:34400/lineup.m3u
Problem: Updated config files but changes aren't visible.
Solutions:
- Reload channels:
POST http://localhost:34400/api/reload/channels - Reload EPG:
POST http://localhost:34400/api/reload/epg - Or restart the server:
docker restart iptv-proxy - Verify YAML syntax is valid (indentation matters!)
- Check server logs for validation errors
Problem: Source requires authentication and returns 401/403 errors.
Solutions:
- URL-encode credentials in the source URL:
url: 'https://username:password@provider.com/playlist.m3u'
- For complex authentication, consider using a local proxy
- Check if the service requires API keys or tokens (may need code modification)
- Test authentication separately with curl:
curl -u username:password http://provider.com/playlist.m3u
Problem: Server consumes too much RAM.
Solutions:
- Large EPG files can use significant memory - consider:
- Filtering to only needed channels
- Using smaller, source-specific EPG files
- Increasing server resources
- Check for memory leaks by monitoring over time
- Reduce the number of concurrent source fetches
- Consider pagination or streaming for very large files
Problem: Admin interface shows "Not Available" message.
Solutions:
- Build the admin UI:
npm run admin:build
- Or run in development mode with hot reload:
This starts both the API server and the Vite admin app. When the backend is running in this mode,
npm run dev
/adminredirects to the admin dev server instead of serving the built bundle. SetADMIN_DEV_PORTif you need a port other than5173, orADMIN_DEV_HOSTif you need a host other thanlocalhost. - For Docker, ensure you're using an image with the admin UI built
- Check that
public/admin/index.htmlexists
Problem: EPG doesn't auto-refresh or scheduled tasks don't execute.
Solutions:
- Check cron expression syntax in
app.yaml - Verify scheduler is running: check
/api/scheduler/jobsendpoint - Review server logs for scheduler errors
- Test cron expressions using an online validator
- Ensure time zone is set correctly (TZ environment variable)
Enable verbose logging for troubleshooting:
DEBUG=* npm startOr in Docker:
environment:
- DEBUG=*If you're still experiencing issues:
- Check the API documentation for endpoint details
- Review server logs for error messages
- Use the
/statusendpoint to check system health - Open an issue on GitHub with:
- Server logs
- Configuration files (remove sensitive data)
- Steps to reproduce the problem
- Expected vs actual behavior
The server provides several API endpoints for configuration and management. See API.md for complete documentation.
Key Endpoints:
GET /lineup.m3u- M3U playlist for the default output profileGET /lineup.json- JSON lineup for the default output profileGET /xmltv.xml- EPG data for the default output profileGET /profiles/:slug/lineup.m3u- M3U playlist for a named enabled output profileGET /profiles/:slug/lineup.json- JSON lineup for a named enabled output profileGET /profiles/:slug/xmltv.xml- EPG data for a named enabled output profileGET /status- System diagnosticsGET /health- Health checkPOST /api/reload/channels- Reload M3U sourcesPOST /api/reload/epg- Reload EPG dataGET /api/config/*- Get/update configuration
- Your XMLTV sources can be remote URLs or local files (use
file://prefix). - M3U sources also support
file://URLs for local files. - All
tvg_ids in channels must match the<channel id="...">in EPG sources to link correctly. - Duplicate
tvg_ids will be overwritten in favor of the last one processed. - Configuration files are automatically created with defaults on first run.
- The server automatically caches channels and EPG data for performance.
This project includes comprehensive test coverage and code quality tools:
- Unit Tests: Test individual functions in isolation
- Integration Tests: Test complete workflows with mocked dependencies
- Output Format Validation: Ensure M3U and XMLTV outputs are valid
- Linting: ESLint for JavaScript/ESM and Vue.js code
- Formatting: Prettier for consistent code style
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run linter
npm run lint
# Auto-fix lint issues
npm run lint:fix
# Check code formatting
npm run format:check
# Auto-format code
npm run formatFor detailed information about testing, see TESTING.md.
MIT