This commit is contained in:
1 parent
d7aea0be65
commit
91f22b54b4
30 files changed
+2475
No files matched your search
@@ -0,0 +1,146 @@
|
||||
'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);
|
||||
});
|
||||
Reference in new issue
Block a user