'use strict'; const http = require('http'); const { settings, validate } = require('./src/settings'); const logger = require('./src/logger'); const { createScheduler } = require('./src/scheduler'); const { handleStream, handleHls } = require('./src/proxy'); const { buildPlaylist, buildIndex } = require('./src/playlist'); const { buildXmltv } = require('./src/epg'); const { healthPayload } = require('./src/health'); const { sendText, sendJson, sendHtml } = require('./src/http-utils'); const scheduler = createScheduler(); function sendM3u(res, body) { res.writeHead(200, { 'Content-Type': 'application/x-mpegURL; charset=utf-8', 'Cache-Control': 'no-store', 'Content-Length': Buffer.byteLength(body), }); res.end(body); } function sendXml(res, body) { res.writeHead(200, { 'Content-Type': 'application/xml; charset=utf-8', 'Cache-Control': 'no-store', 'Content-Length': Buffer.byteLength(body), }); res.end(body); } function statusPayload() { return { ...scheduler.summarize(), config: { searchTerm: settings.searchTerm, maxChannels: settings.maxChannels, language: settings.language, eventDurationMin: settings.eventDurationMin, startGraceMin: settings.startGraceMin, refreshIntervalSec: settings.refreshIntervalSec, sourceTimezone: settings.sourceTimezone, timezone: settings.timezone, }, }; } const server = http.createServer((req, res) => { let reqUrl; try { reqUrl = new URL(req.url, 'http://bundler.invalid'); } catch { sendText(res, 400, 'Bad request'); return; } const path = reqUrl.pathname.replace(/\/+$/, '') || '/'; if (req.method !== 'GET' && req.method !== 'HEAD') { sendText(res, 405, 'Method not allowed', { Allow: 'GET, HEAD' }); return; } if (path === '/health' || path === '/healthz') { sendJson(res, 200, healthPayload(scheduler)); return; } if (path === '/') { sendHtml(res, 200, buildIndex(req, scheduler)); return; } if (path === '/playlist.m3u' || path === '/channels.m3u') { sendM3u(res, buildPlaylist(req, scheduler)); return; } if (path === '/epg.xml' || path === '/xmltv.xml') { sendXml(res, buildXmltv(scheduler)); return; } if (path === '/status') { sendJson(res, 200, statusPayload()); return; } const streamMatch = /^\/stream\/(\d+)$/.exec(path); if (streamMatch) { handleStream(req, res, Number(streamMatch[1]), scheduler).catch((error) => { logger.error('Stream handler failed:', error.message || error); if (!res.headersSent) sendText(res, 500, 'Internal error'); }); return; } const hlsMatch = /^\/hls\/(\d+)\/([^/]+)$/.exec(path); if (hlsMatch) { let token = hlsMatch[2]; try { token = decodeURIComponent(token); } catch {} handleHls(req, res, Number(hlsMatch[1]), token).catch((error) => { logger.error('HLS handler failed:', error.message || error); if (!res.headersSent) sendText(res, 500, 'Internal error'); }); return; } sendText(res, 404, 'Not found'); }); function shutdown(signal) { logger.info(`Received ${signal}, shutting down`); scheduler.stopRefreshLoop(); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 5000).unref(); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); async function main() { try { validate(); } catch (error) { logger.error(error.message); process.exit(1); } await scheduler.refreshOnce(); server.listen(settings.listenPort, () => { logger.info(`Xtream PPV bundler listening on :${settings.listenPort}`); logger.info( `Search "${settings.searchTerm}", ${settings.maxChannels} slots, language ${settings.language}, refresh ${settings.refreshIntervalSec}s` ); scheduler.startRefreshLoop(); }); } main().catch((error) => { logger.error(error); process.exit(1); });