Files
tvheadend-nm3u8dl/server_nm3u8dl/server.js
2024-03-19 15:23:02 +01:00

202 lines
5.9 KiB
JavaScript

const express = require('express');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 8080;
const tmp_folder = '/tmp/ramdisk/';
let channelsPath = path.resolve(__dirname, 'channels.json');
let channels = JSON.parse(fs.readFileSync(channelsPath, 'utf8'));
const activeStreams = {}; // Store active stream processes with unique IDs
fs.watch(channelsPath, (eventType, filename) => {
if (eventType === 'change') {
console.log(`channels.json was updated, reloading...`);
try {
channels = JSON.parse(fs.readFileSync(channelsPath, 'utf8'));
} catch (error) {
console.log(error.message);
}
}
});
async function startStreamlink(channel, clientID) {
const useragent = channel.useragent;
const authorization = channel.authorization;
const referer = channel.referer;
const proxy = channel.proxy;
const key1 = channel.key1;
const key2 = channel.key2;
const key3 = channel.key3;
const key4 = channel.key4;
const key5 = channel.key5;
const audios = channel.audios;
const resolution = channel.resolution;
const delay = channel.delay;
let args = [
channel.url,
'--use-shaka-packager',
// '--log-level', 'INFO',
'--no-log',
// '--live-real-time-merge', 'true',
// '--mp4-real-time-decryption', 'true',
// '--live-wait-time', '1',
'--thread-count', '3',
'--concurrent-download',
'--live-pipe-mux',
'--live-keep-segments', 'false',
'--check-segments-count', 'false',
// '--live-take-count', '10',
'--tmp-dir', tmp_folder + `${channel.name}_${clientID}`, // Unique folder for each client
'--del-after-done', 'true'
];
if (useragent) {
args.splice(1, 0, '--header', `User-Agent: ${useragent}`);
} else {
args.splice(1, 0, '--header', `User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299`);
}
if (authorization) {
args.splice(1, 0, '--header', `Authorization: ${authorization}`);
}
if (referer) {
args.splice(1, 0, '--header', `Referer: ${referer}`);
}
if (proxy) {
args.splice(1, 0, '--custom-proxy', proxy);
}
if (key5) {
args.splice(1, 0, '--key', key5);
}
if (key4) {
args.splice(1, 0, '--key', key4);
}
if (key3) {
args.splice(1, 0, '--key', key3);
}
if (key2) {
args.splice(1, 0, '--key', key2);
}
if (key1) {
args.splice(1, 0, '--key', key1);
}
if (audios) {
args.splice(1, 0, '-sa', audios);
} else {
args.splice(1, 0, '-sa', 'best2');
}
if (resolution) {
args.splice(1, 0, '-sv', 'res=' + resolution);
} else {
args.splice(1, 0, '-sv', 'best');
}
if (delay) {
args.splice(1, 0, '--live-pipe-options', '-itsoffset ' + delay + ' -re -loglevel error {INPUTS} -c copy -f mpegts -fflags +genpts -shortest pipe:1');
}
console.log(`Args: ${args}`);
const streamlinkProcess = spawn('N_m3u8DL-RE', args);
const processID = `${channel.name}_${clientID}`;
activeStreams[processID] = streamlinkProcess;
return streamlinkProcess;
}
app.get('/stream/:channelName', async (req, res) => {
const channelName = req.params.channelName;
const clientID = req.ip;
const channel = channels[channelName];
console.log(`Received request for /stream/${channelName} from Client IP: ${clientID}`);
channels[channelName]['name'] = channelName;
if (!channel) {
res.status(404).send('Channel not found');
return;
}
console.log(`Starting Streamlink command for channel: ${channelName} (Client IP: ${clientID})`);
try {
let streamlinkProcess = await startStreamlink(channel, clientID);
res.setHeader('Content-Type', 'video/MP2T');
streamlinkProcess.stdout.pipe(res);
streamlinkProcess.stderr.on('data', data => {
console.error(`Streamlink: ${data}`);
});
streamlinkProcess.on('close', code => {
console.log(`Streamlink process exited with code ${code} for Client IP: ${clientID}`);
res.end();
const processID = `${channelName}_${clientID}`;
delete activeStreams[processID];
const tempFolderPath = tmp_folder + `${channel.name}_${clientID}`;
if (fs.existsSync(tempFolderPath)) {
fs.rmSync(tempFolderPath, { recursive: true });
}
logActiveConnections();
});
res.on('close', () => {
console.log(`Response closed, killing channel ${channel.name} for Client IP: ${clientID}`);
const processID = `${channelName}_${clientID}`;
if (activeStreams[processID]) {
activeStreams[processID].kill();
delete activeStreams[processID];
}
const tempFolderPath = tmp_folder + `${channel.name}_${clientID}`;
if (fs.existsSync(tempFolderPath)) {
fs.rmSync(tempFolderPath, { recursive: true });
}
logActiveConnections();
});
req.on('abort', () => {
console.log(`Request aborted, killing Streamlink process for Client IP: ${clientID}`);
const processID = `${channelName}_${clientID}`;
if (activeStreams[processID]) {
activeStreams[processID].kill();
delete activeStreams[processID];
}
const tempFolderPath = tmp_folder + `${channel.name}_${clientID}`;
if (fs.existsSync(tempFolderPath)) {
fs.rmSync(tempFolderPath, { recursive: true });
}
logActiveConnections();
});
logActiveConnections();
} catch (error) {
console.error(`Streamlink failed: ${error}`);
res.status(500).send('Streamlink failed');
}
});
function logActiveConnections() {
const activeConnections = Object.keys(activeStreams).length;
console.log(`Active connections: ${activeConnections}`);
}
app.listen(port, "0.0.0.0", () => {
console.log(`Server listening on port ${port}`);
});
process.on('SIGINT', () => {
for (const processID in activeStreams) {
if (activeStreams.hasOwnProperty(processID)) {
activeStreams[processID].kill();
delete activeStreams[processID];
}
}
process.exit();
});