diff --git a/README.md b/README.md
index 9aca71cb..1030276c 100644
--- a/README.md
+++ b/README.md
@@ -146,7 +146,7 @@ For the [environment variables](#environment-variables), you may specify these i
| `WEB_IP` | `0.0.0.0` | IP to use for webserver |
| `WEB_PORT` | `4124` | Port to use for webserver |
| `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. |
-| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.
⚠️ This should not be used unless you know what you're doing |
+| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.
⚠️ This should not be used unless you know what you're doing |
| `DIR_RUN` | `/usr/bin/app` | Path inside container where TVApp2 will be placed after it is built
⚠️ This should not be used unless you know what you're doing |
@@ -750,7 +750,7 @@ This docker container contains the following env variables:
| `WEB_IP` | `0.0.0.0` | IP to use for webserver |
| `WEB_PORT` | `4124` | Port to use for webserver |
| `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. |
-| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.
⚠️ This should not be used unless you know what you're doing |
+| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.
⚠️ This should not be used unless you know what you're doing |
| `DIR_RUN` | `/usr/bin/app` | Path inside container where TVApp2 will be placed after it is built
⚠️ This should not be used unless you know what you're doing |
diff --git a/tvapp2/index.js b/tvapp2/index.js
index a7e8bdd7..b4b7a66d 100755
--- a/tvapp2/index.js
+++ b/tvapp2/index.js
@@ -37,389 +37,411 @@ if (process.pkg) {
}
class Semaphore {
- constructor(max) {
- this.max = max;
- this.queue = [];
- this.active = 0;
- }
- async acquire() {
- if (this.active < this.max) {
- this.active++;
- return;
+ constructor(max) {
+ this.max = max;
+ this.queue = [];
+ this.active = 0;
}
- return new Promise((resolve) => this.queue.push(resolve));
- }
- release() {
- this.active--;
- if (this.queue.length > 0) {
- const resolve = this.queue.shift();
- this.active++;
- resolve();
+ async acquire() {
+ if (this.active < this.max) {
+ this.active++;
+ return;
+ }
+ return new Promise((resolve) => this.queue.push(resolve));
+ }
+ release() {
+ this.active--;
+ if (this.queue.length > 0) {
+ const resolve = this.queue.shift();
+ this.active++;
+ resolve();
+ }
}
- }
}
const semaphore = new Semaphore(5);
let urls = [];
let tokenData = {
- subdomain: null,
- token: null,
- url: null,
- validationUrl: null,
- cookies: null,
+ subdomain: null,
+ token: null,
+ url: null,
+ validationUrl: null,
+ cookies: null,
};
let lastTokenFetchTime = 0;
const log = (message) => {
- const now = new Date();
- console.log(`[${now.toLocaleTimeString()}] ${message}`);
+ const now = new Date();
+ console.log(`[${now.toLocaleTimeString()}] ${message}`);
};
async function downloadFile(url, filePath) {
- console.log(`Fetching ${url}`);
- return new Promise((resolve, reject) => {
- const isHttps = new URL(url).protocol === 'https:';
- const httpModule = isHttps ? require('https') : require('http');
- const file = fs.createWriteStream(filePath);
+ console.log(`Fetching ${url}`);
+ return new Promise((resolve, reject) => {
+ const isHttps = new URL(url).protocol === 'https:';
+ const httpModule = isHttps ? require('https') : require('http');
+ const file = fs.createWriteStream(filePath);
- httpModule
- .get(url, (response) => {
- if (response.statusCode !== 200) {
- console.error(`Failed to download file: ${url}. Status code: ${response.statusCode}`);
- return reject(new Error(`Failed to download file: ${url}. Status code: ${response.statusCode}`));
- }
- response.pipe(file);
- file.on('finish', () => {
- log(`Sucess: ${filePath}`);
- file.close(() => resolve(true));
- });
- })
- .on('error', (err) => {
- console.error(`Error downloading file: ${url}. Error: ${err.message}`);
- fs.unlink(filePath, () => reject(err));
- });
- });
+ httpModule
+ .get(url, (response) => {
+ if (response.statusCode !== 200) {
+ console.error(`Failed to download file: ${url}. Status code: ${response.statusCode}`);
+ return reject(new Error(`Failed to download file: ${url}. Status code: ${response.statusCode}`));
+ }
+ response.pipe(file);
+ file.on('finish', () => {
+ log(`Success: ${filePath}`);
+ file.close(() => resolve(true));
+ });
+ })
+ .on('error', (err) => {
+ console.error(`Error downloading file: ${url}. Error: ${err.message}`);
+ fs.unlink(filePath, () => reject(err));
+ });
+ });
}
async function ensureFileExists(url, filePath) {
- try {
- await downloadFile(url, filePath);
- } catch (error) {
- if (fs.existsSync(filePath)) {
- console.warn(`Using existing file for ${filePath} due to download failure.`);
- } else {
- console.error(`Critical: Failed to download ${url}, and no local file exists.`);
- throw error;
+ try {
+ await downloadFile(url, filePath);
+ } catch (error) {
+ if (fs.existsSync(filePath)) {
+ console.warn(`Using existing file for ${filePath} due to download failure.`);
+ } else {
+ console.error(`Critical: Failed to download ${url}, and no local file exists.`);
+ throw error;
+ }
}
- }
}
// REMOVED REFERENCE CALLS TO THIS FUNCTION
// TODO: UPDATES TO HANDLER FOR SPORT EVENTS
async function fetchSportsData() {
- return new Promise((resolve, reject) => {
- const isHttps = new URL(externalEvents).protocol === 'https:';
- const httpModule = isHttps ? require('https') : require('http');
- httpModule
- .get(url, (response) => {
- if (response.statusCode !== 200) {
- console.error(`Failed to fetch sports data. Status code: ${response.statusCode}`);
- return reject(new Error(`Failed to fetch sports data. Status code: ${response.statusCode}`));
- }
- let data = '';
- response.on('data', (chunk) => (data += chunk));
- response.on('end', () => {
- log('Fetched sports data successfully.');
- resolve(data);
- });
- })
- .on('error', (err) => {
- console.error(`Error fetching sports data: ${err.message}`);
- reject(err);
- });
- });
+ return new Promise((resolve, reject) => {
+ const isHttps = new URL(externalEvents).protocol === 'https:';
+ const httpModule = isHttps ? require('https') : require('http');
+ httpModule
+ .get(url, (response) => {
+ if (response.statusCode !== 200) {
+ console.error(`Failed to fetch sports data. Status code: ${response.statusCode}`);
+ return reject(new Error(`Failed to fetch sports data. Status code: ${response.statusCode}`));
+ }
+ let data = '';
+ response.on('data', (chunk) => (data += chunk));
+ response.on('end', () => {
+ log('Fetched sports data successfully.');
+ resolve(data);
+ });
+ })
+ .on('error', (err) => {
+ console.error(`Error fetching sports data: ${err.message}`);
+ reject(err);
+ });
+ });
}
async function fetchRemote(url) {
- return new Promise((resolve, reject) => {
- const mod = url.startsWith('https') ? https : http;
- mod
- .get(url, { headers: { 'Accept-Encoding': 'gzip, deflate, br' } }, (resp) => {
- if (resp.statusCode !== 200) {
- return reject(new Error(`HTTP ${resp.statusCode} for ${url}`));
- }
- const chunks = [];
- resp.on('data', (chunk) => chunks.push(chunk));
- resp.on('end', () => {
- const buffer = Buffer.concat(chunks);
- const encoding = resp.headers['content-encoding'];
- if (encoding === 'gzip') {
- zlib.gunzip(buffer, (err, decoded) => {
- if (err) return reject(err);
- resolve(decoded);
- });
- } else if (encoding === 'deflate') {
- zlib.inflate(buffer, (err, decoded) => {
- if (err) return reject(err);
- resolve(decoded);
- });
- } else if (encoding === 'br') {
- zlib.brotliDecompress(buffer, (err, decoded) => {
- if (err) return reject(err);
- resolve(decoded);
- });
- } else {
- resolve(buffer);
- }
- });
- })
- .on('error', reject);
- });
+ return new Promise((resolve, reject) => {
+ const mod = url.startsWith('https') ? https : http;
+ mod
+ .get(url, {
+ headers: {
+ 'Accept-Encoding': 'gzip, deflate, br'
+ }
+ }, (resp) => {
+ if (resp.statusCode !== 200) {
+ return reject(new Error(`HTTP ${resp.statusCode} for ${url}`));
+ }
+ const chunks = [];
+ resp.on('data', (chunk) => chunks.push(chunk));
+ resp.on('end', () => {
+ const buffer = Buffer.concat(chunks);
+ const encoding = resp.headers['content-encoding'];
+ if (encoding === 'gzip') {
+ zlib.gunzip(buffer, (err, decoded) => {
+ if (err) return reject(err);
+ resolve(decoded);
+ });
+ } else if (encoding === 'deflate') {
+ zlib.inflate(buffer, (err, decoded) => {
+ if (err) return reject(err);
+ resolve(decoded);
+ });
+ } else if (encoding === 'br') {
+ zlib.brotliDecompress(buffer, (err, decoded) => {
+ if (err) return reject(err);
+ resolve(decoded);
+ });
+ } else {
+ resolve(buffer);
+ }
+ });
+ })
+ .on('error', reject);
+ });
}
async function serveKey(req, res) {
- try {
- const uriParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('uri');
- if (!uriParam) {
- res.writeHead(400, { 'Content-Type': 'text/plain' });
- return res.end('Error: Missing "uri" parameter for key download.');
+ try {
+ const uriParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('uri');
+ if (!uriParam) {
+ res.writeHead(400, {
+ 'Content-Type': 'text/plain'
+ });
+ return res.end('Error: Missing "uri" parameter for key download.');
+ }
+ const keyData = await fetchRemote(uriParam);
+ res.writeHead(200, {
+ 'Content-Type': 'application/octet-stream'
+ });
+ res.end(keyData);
+ } catch (err) {
+ console.error('Error in serveKey:', err.message);
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+ res.end('Error fetching key.');
}
- const keyData = await fetchRemote(uriParam);
- res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
- res.end(keyData);
- } catch (err) {
- console.error('Error in serveKey:', err.message);
- res.writeHead(500, { 'Content-Type': 'text/plain' });
- res.end('Error fetching key.');
- }
}
let gCookies = {};
const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
function parseSetCookieHeaders(setCookieValues) {
- if (!Array.isArray(setCookieValues)) return;
- setCookieValues.forEach((line) => {
- const [cookiePair] = line.split(';');
- if (cookiePair) {
- const [key, val] = cookiePair.split('=');
- if (key && val) {
- gCookies[key.trim()] = val.trim();
- }
- }
- });
+ if (!Array.isArray(setCookieValues)) return;
+ setCookieValues.forEach((line) => {
+ const [cookiePair] = line.split(';');
+ if (cookiePair) {
+ const [key, val] = cookiePair.split('=');
+ if (key && val) {
+ gCookies[key.trim()] = val.trim();
+ }
+ }
+ });
}
function buildCookieHeader() {
- const pairs = [];
- for (const [k, v] of Object.entries(gCookies)) {
- pairs.push(`${k}=${v}`);
- }
- return pairs.join('; ');
+ const pairs = [];
+ for (const [k, v] of Object.entries(gCookies)) {
+ pairs.push(`${k}=${v}`);
+ }
+ return pairs.join('; ');
}
function fetchPage(url) {
- return new Promise((resolve, reject) => {
- const opts = {
- method: 'GET',
- headers: {
- 'User-Agent': USERAGENT,
- Accept: '*/*',
- Cookie: buildCookieHeader(),
- },
- };
- https
- .get(url, opts, (res) => {
- if (res.statusCode !== 200) {
- return reject(new Error(`Non-200 status ${res.statusCode} => ${url}`));
- }
- if (res.headers['set-cookie']) {
- parseSetCookieHeaders(res.headers['set-cookie']);
- }
- let data = '';
- res.on('data', (chunk) => (data += chunk));
- res.on('end', () => resolve(data));
- })
- .on('error', reject);
- });
+ return new Promise((resolve, reject) => {
+ const opts = {
+ method: 'GET',
+ headers: {
+ 'User-Agent': USERAGENT,
+ Accept: '*/*',
+ Cookie: buildCookieHeader(),
+ },
+ };
+ https
+ .get(url, opts, (res) => {
+ if (res.statusCode !== 200) {
+ return reject(new Error(`Non-200 status ${res.statusCode} => ${url}`));
+ }
+ if (res.headers['set-cookie']) {
+ parseSetCookieHeaders(res.headers['set-cookie']);
+ }
+ let data = '';
+ res.on('data', (chunk) => (data += chunk));
+ res.on('end', () => resolve(data));
+ })
+ .on('error', reject);
+ });
}
async function getTokenizedUrl(channelUrl) {
- try {
- const html = await fetchPage(channelUrl);
-
- let streamName;
- let streamHost;
- if (channelUrl.includes('espn-')) {
- streamName = 'ESPN';
- } else if (channelUrl.includes('espn2-')) {
- streamName = 'ESPN2';
- } else {
- const streamNameMatch = html.match(/id="stream_name" name="([^"]+)"/);
- if (!streamNameMatch) {
- log('No "stream_name" found');
- return null;
- }
- streamName = streamNameMatch[1];
- }
- if (channelUrl.match('tvpass\.org')) {
- streamHost = 'tvpass.org';
- };
- if (channelUrl.match('thetvapp\.to')) {
- streamHost = 'thetvapp.to';
- };
- const tokenUrl = `https://${streamHost}/token/${streamName}?quality=hd`;
- const tokenResponse = await fetchPage(tokenUrl);
- let finalUrl;
try {
- const json = JSON.parse(tokenResponse);
- finalUrl = json.url;
+ const html = await fetchPage(channelUrl);
+
+ let streamName;
+ let streamHost;
+ if (channelUrl.includes('espn-')) {
+ streamName = 'ESPN';
+ } else if (channelUrl.includes('espn2-')) {
+ streamName = 'ESPN2';
+ } else {
+ const streamNameMatch = html.match(/id="stream_name" name="([^"]+)"/);
+ if (!streamNameMatch) {
+ log('No "stream_name" found');
+ return null;
+ }
+ streamName = streamNameMatch[1];
+ }
+ if (channelUrl.match('tvpass\.org')) {
+ streamHost = 'tvpass.org';
+ };
+ if (channelUrl.match('thetvapp\.to')) {
+ streamHost = 'thetvapp.to';
+ };
+ const tokenUrl = `https://${streamHost}/token/${streamName}?quality=hd`;
+ const tokenResponse = await fetchPage(tokenUrl);
+ let finalUrl;
+ try {
+ const json = JSON.parse(tokenResponse);
+ finalUrl = json.url;
+ } catch (err) {
+ log('Failed to parse token JSON');
+ return null;
+ }
+ if (!finalUrl) {
+ log('No URL found in the token JSON');
+ return null;
+ }
+ log(`Tokenized URL: ${finalUrl}`);
+ return finalUrl;
} catch (err) {
- log('Failed to parse token JSON');
- return null;
+ log(`Fatal error fetching token: ${err.message}`);
+ return null;
}
- if (!finalUrl) {
- log('No URL found in the token JSON');
- return null;
- }
- log(`Tokenized URL: ${finalUrl}`);
- return finalUrl;
- } catch (err) {
- log(`Fatal error fetching token: ${err.message}`);
- return null;
- }
}
async function serveChannelPlaylist(req, res) {
- await semaphore.acquire();
- try {
- const urlParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('url');
- if (!urlParam) {
- log('Error: Missing URL parameter');
- res.writeHead(400, { 'Content-Type': 'text/plain' });
- res.end('Error: Missing URL parameter.');
- return;
+ await semaphore.acquire();
+ try {
+ const urlParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('url');
+ if (!urlParam) {
+ log('Error: Missing URL parameter');
+ res.writeHead(400, {
+ 'Content-Type': 'text/plain'
+ });
+ res.end('Error: Missing URL parameter.');
+ return;
+ }
+ const decodedUrl = decodeURIComponent(urlParam);
+ if (decodedUrl.endsWith('.ts')) {
+ res.writeHead(302, {
+ Location: decodedUrl
+ });
+ res.end();
+ return;
+ }
+ const cachedUrl = getCache(decodedUrl);
+ if (cachedUrl) {
+ const rewrittenPlaylist = await rewritePlaylist(cachedUrl, req);
+ res.writeHead(200, {
+ 'Content-Type': 'application/vnd.apple.mpegurl',
+ 'Content-Disposition': 'inline; filename="playlist.m3u8"',
+ });
+ res.end(rewrittenPlaylist);
+ return;
+ }
+ log(`Fetching stream: ${urlParam}`);
+ const finalUrl = await getTokenizedUrl(decodedUrl);
+ if (!finalUrl) {
+ log('Error: Failed to retrieve tokenized URL');
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+ res.end('Error: Failed to retrieve tokenized URL.');
+ return;
+ }
+ setCache(decodedUrl, finalUrl, 4 * 60 * 60 * 1000);
+ const hdUrl = finalUrl.replace('tracks-v2a1', 'tracks-v1a1');
+ const rewrittenPlaylist = await rewritePlaylist(hdUrl, req);
+ res.writeHead(200, {
+ 'Content-Type': 'application/vnd.apple.mpegurl',
+ 'Content-Disposition': 'inline; filename="playlist.m3u8"',
+ });
+ res.end(rewrittenPlaylist);
+ log('Served playlist');
+ } catch (error) {
+ log(`Error processing request: ${error.message}`);
+ if (!res.headersSent) {
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+ res.end('Error processing request.');
+ }
+ } finally {
+ semaphore.release();
}
- const decodedUrl = decodeURIComponent(urlParam);
- if (decodedUrl.endsWith('.ts')) {
- res.writeHead(302, { Location: decodedUrl });
- res.end();
- return;
- }
- const cachedUrl = getCache(decodedUrl);
- if (cachedUrl) {
- const rewrittenPlaylist = await rewritePlaylist(cachedUrl, req);
- res.writeHead(200, {
- 'Content-Type': 'application/vnd.apple.mpegurl',
- 'Content-Disposition': 'inline; filename="playlist.m3u8"',
- });
- res.end(rewrittenPlaylist);
- return;
- }
- log(`Fetching stream: ${urlParam}`);
- const finalUrl = await getTokenizedUrl(decodedUrl);
- if (!finalUrl) {
- log('Error: Failed to retrieve tokenized URL');
- res.writeHead(500, { 'Content-Type': 'text/plain' });
- res.end('Error: Failed to retrieve tokenized URL.');
- return;
- }
- setCache(decodedUrl, finalUrl, 4 * 60 * 60 * 1000);
- const hdUrl = finalUrl.replace('tracks-v2a1', 'tracks-v1a1');
- const rewrittenPlaylist = await rewritePlaylist(hdUrl, req);
- res.writeHead(200, {
- 'Content-Type': 'application/vnd.apple.mpegurl',
- 'Content-Disposition': 'inline; filename="playlist.m3u8"',
- });
- res.end(rewrittenPlaylist);
- log('Served playlist');
- } catch (error) {
- log(`Error processing request: ${error.message}`);
- if (!res.headersSent) {
- res.writeHead(500, { 'Content-Type': 'text/plain' });
- res.end('Error processing request.');
- }
- } finally {
- semaphore.release();
- }
}
async function rewritePlaylist(originalUrl, req) {
- const rawData = await fetchRemote(originalUrl);
- const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
- const host = req.headers.host;
- const baseUrl = `${protocol}://${host}`;
- const playlistContent = rawData.toString('utf8');
- return playlistContent
- .replace(/URI="([^"]+)"/g, (match, uri) => {
- const resolvedUri = new URL(uri, originalUrl).href;
- return `URI="${baseUrl}/key?uri=${encodeURIComponent(resolvedUri)}"`;
- })
- .replace(/^([^#].*\.m3u8)(\?.*)?$/gm, (match, uri) => {
- const resolvedUri = new URL(uri, originalUrl).href;
- return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`;
- })
- .replace(/^([^#].*\.ts)(\?.*)?$/gm, (match, uri) => {
- const resolvedUri = new URL(uri, originalUrl).href;
- return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`;
- });
+ const rawData = await fetchRemote(originalUrl);
+ const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
+ const host = req.headers.host;
+ const baseUrl = `${protocol}://${host}`;
+ const playlistContent = rawData.toString('utf8');
+ return playlistContent
+ .replace(/URI="([^"]+)"/g, (match, uri) => {
+ const resolvedUri = new URL(uri, originalUrl).href;
+ return `URI="${baseUrl}/key?uri=${encodeURIComponent(resolvedUri)}"`;
+ })
+ .replace(/^([^#].*\.m3u8)(\?.*)?$/gm, (match, uri) => {
+ const resolvedUri = new URL(uri, originalUrl).href;
+ return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`;
+ })
+ .replace(/^([^#].*\.ts)(\?.*)?$/gm, (match, uri) => {
+ const resolvedUri = new URL(uri, originalUrl).href;
+ return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`;
+ });
}
async function servePlaylist(response, req) {
- try {
+ try {
- const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
- const host = req.headers.host;
- const baseUrl = `${protocol}://${host}`;
- const formattedContent = fs.readFileSync(FORMATTED_FILE, 'utf-8');
- const updatedContent = formattedContent
- .replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => {
- return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`;
- })
- .replace(/(https?:\/\/[^\s]*tvpass[^\s]*)/g, (fullUrl) => {
- return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`;
+ const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
+ const host = req.headers.host;
+ const baseUrl = `${protocol}://${host}`;
+ const formattedContent = fs.readFileSync(FORMATTED_FILE, 'utf-8');
+ const updatedContent = formattedContent
+ .replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => {
+ return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`;
+ })
+ .replace(/(https?:\/\/[^\s]*tvpass[^\s]*)/g, (fullUrl) => {
+ return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`;
+ });
+
+ response.writeHead(200, {
+ 'Content-Type': 'application/x-mpegURL',
+ 'Content-Disposition': 'inline; filename="playlist.m3u8"',
});
+ response.end(updatedContent);
- response.writeHead(200, {
- 'Content-Type': 'application/x-mpegURL',
- 'Content-Disposition': 'inline; filename="playlist.m3u8"',
- });
- response.end(updatedContent);
+ } catch (error) {
- } catch (error) {
+ console.error('Error in servePlaylist:', error.message);
+ response.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+ response.end(`Error serving playlist: ${error.message}`);
- console.error('Error in servePlaylist:', error.message);
- response.writeHead(500, { 'Content-Type': 'text/plain' });
- response.end(`Error serving playlist: ${error.message}`);
-
- }
+ }
}
async function serveXmltv(response, req) {
- try {
+ try {
- const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
- const host = req.headers.host;
- const baseUrl = `${protocol}://${host}`;
- const formattedContent = fs.readFileSync(EPG_FILE, 'utf-8');
+ const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
+ const host = req.headers.host;
+ const baseUrl = `${protocol}://${host}`;
+ const formattedContent = fs.readFileSync(EPG_FILE, 'utf-8');
- response.writeHead(200, {
- 'Content-Type': 'application/xml',
- 'Content-Disposition': 'inline; filename="xmltv.1.xml"',
- });
- response.end(formattedContent);
+ response.writeHead(200, {
+ 'Content-Type': 'application/xml',
+ 'Content-Disposition': 'inline; filename="xmltv.1.xml"',
+ });
+ response.end(formattedContent);
- } catch (error) {
+ } catch (error) {
- console.error('Error in servePlaylist:', error.message);
- response.writeHead(500, { 'Content-Type': 'text/plain' });
- response.end(`Error serving playlist: ${error.message}`);
+ console.error('Error in servePlaylist:', error.message);
+ response.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+ response.end(`Error serving playlist: ${error.message}`);
- }
+ }
};
@@ -478,176 +500,202 @@ async function servePlaylist(response, req) {
*/
function setCache(key, value, ttl) {
- const expiry = Date.now() + ttl;
- cache.set(key, { value, expiry });
- log(`Cache set: ${key}, expires in ${ttl / 1000} seconds`);
+ const expiry = Date.now() + ttl;
+ cache.set(key, {
+ value,
+ expiry
+ });
+ log(`Cache set: ${key}, expires in ${ttl / 1000} seconds`);
}
function getCache(key) {
- const cached = cache.get(key);
- if (cached && cached.expiry > Date.now()) {
- return cached.value;
- } else {
- if (cached) log(`Cache expired for key: ${key}`);
- cache.delete(key);
- return null;
- }
+ const cached = cache.get(key);
+ if (cached && cached.expiry > Date.now()) {
+ return cached.value;
+ } else {
+ if (cached) log(`Cache expired for key: ${key}`);
+ cache.delete(key);
+ return null;
+ }
}
async function initialize() {
- try {
- log('Initializing server...');
- await ensureFileExists(externalURL, URLS_FILE);
- await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE);
- await ensureFileExists(externalEPG, EPG_FILE);
- urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean);
- if (urls.length === 0) {
- throw new Error(`No valid URLs found in ${URLS_FILE}`);
+ try {
+ log('Initializing server...');
+ await ensureFileExists(externalURL, URLS_FILE);
+ await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE);
+ await ensureFileExists(externalEPG, EPG_FILE);
+ urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean);
+ if (urls.length === 0) {
+ throw new Error(`No valid URLs found in ${URLS_FILE}`);
+ }
+ log('Initialization complete.');
+ } catch (error) {
+ console.error(`Initialization error: ${error.message}`);
}
- log('Initialization complete.');
- } catch (error) {
- console.error(`Initialization error: ${error.message}`);
- }
}
const server = http.createServer((req, res) => {
- const handleRequest = async () => {
- const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
- const host = req.headers.host;
- const baseUrl = `${protocol}://${host}`;
- if (req.url === '/' && req.method === 'GET') {
- const htmlContent = `
+ const handleRequest = async () => {
+ const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
+ const host = req.headers.host;
+ const baseUrl = `${protocol}://${host}`;
+ if (req.url === '/' && req.method === 'GET') {
+ const htmlContent = `
-