147 lines
3.8 KiB
JavaScript
147 lines
3.8 KiB
JavaScript
'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,
|
|
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);
|
|
});
|