Files
Bram d266a8258d
Build and Push Docker Images / build-and-push (push) Successful in 28s
timezone issues
2026-11-07 16:30:47 +01:00

148 lines
3.9 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,
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);
});