feat: add new logging method; add env LOG_LEVEL

This commit is contained in:
2025-03-21 02:09:07 -07:00
parent 614a0a2daf
commit ceafd71461

View File

@@ -28,6 +28,26 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); // get resolved path to file const __filename = fileURLToPath(import.meta.url); // get resolved path to file
const __dirname = path.dirname(__filename); // get name of directory const __dirname = path.dirname(__filename); // get name of directory
/*
chalk.level
@ref https://npmjs.com/package/chalk
- 0 All colors disabled
- 1 Basic color support (16 colors)
- 2 256 color support
- 3 Truecolor support (16 million colors)
When assigning text colors, terminals and the windows command prompt can display any color; however apps
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
*/
chalk.level = 3;
/*
Define > General
*/
let URLS_FILE; let URLS_FILE;
let FORMATTED_FILE; let FORMATTED_FILE;
let EPG_FILE; let EPG_FILE;
@@ -37,9 +57,134 @@ const externalFORMATTED_1 = `${process.env.URL_REPO}/tvapp2-externals/raw/branch
const externalFORMATTED_2 = ''; const externalFORMATTED_2 = '';
const externalFORMATTED_3 = ''; const externalFORMATTED_3 = '';
const externalEvents = ''; const externalEvents = '';
const LOG_LEVEL = process.env.LOG_LEVEL || 8;
/*
Define > Logs
When assigning text colors, terminals and the windows command prompt can display any color; however apps
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
Various levels of logs with the following usage:
Log.trace(`This is trace`)
Log.debug(`This is debug`)
Log.info(`This is info`)
Log.ok(`This is ok`)
Log.notice(`This is notice`)
Log.warn(`This is warn`)
Log.error(
`Error fetching sports data with error:`,
chalk.white(` → `),
chalk.grey(`This is the error message`)
);
Level Type
-----------------------------------
6 Trace
5 Debug
4 Info
3 Notice
2 Warn
1 Error
*/
class Log {
static now() {
const now = new Date();
return chalk.gray(`[${now.toLocaleTimeString()}]`)
}
static trace(...message) {
if (LOG_LEVEL >= 6)
{
console.trace(
chalk.white.bgMagenta.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.magentaBright(message.join(" "))
)
}
}
static debug(...message) {
if (LOG_LEVEL >= 5)
{
console.debug(
chalk.white.bgGray.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.gray(message.join(" "))
)
}
}
static info(...message) {
if (LOG_LEVEL >= 4)
{
console.info(
chalk.white.bgBlueBright.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.blueBright(message.join(" "))
)
}
}
static ok(...message) {
if (LOG_LEVEL >= 4)
{
console.log(
chalk.white.bgGreen.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.greenBright(message.join(" "))
)
}
}
static notice(...message) {
if (LOG_LEVEL >= 3)
{
console.log(
chalk.white.bgYellow.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.yellowBright(message.join(" "))
)
}
}
static warn(...message) {
if (LOG_LEVEL >= 2)
{
console.warn(
chalk.white.bgYellow.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.yellow(message.join(" "))
)
}
}
static error(...message) {
if (LOG_LEVEL >= 1)
{
console.error(
chalk.white.bgRedBright.bold(` ${name} `),
chalk.white(``),
this.now(),
chalk.red(message.join(" "))
)
}
}
}
/*
Process
*/
if (process.pkg) { if (process.pkg) {
console.log('Process package'); Log.info(`Processing Package`);
const basePath = path.dirname(process.execPath); const basePath = path.dirname(process.execPath);
URLS_FILE = path.join(basePath, 'urls.txt'); URLS_FILE = path.join(basePath, 'urls.txt');
FORMATTED_FILE = path.join(basePath, 'formatted.dat'); FORMATTED_FILE = path.join(basePath, 'formatted.dat');
@@ -47,7 +192,7 @@ if (process.pkg) {
EPG_FILE = path.join(basePath, 'xmltv.1.xml'); EPG_FILE = path.join(basePath, 'xmltv.1.xml');
EPG_FILE.length; EPG_FILE.length;
} else { } else {
console.log('Process locals'); Log.info(`Processing Locals`);
URLS_FILE = path.resolve(__dirname, 'urls.txt'); URLS_FILE = path.resolve(__dirname, 'urls.txt');
FORMATTED_FILE = path.resolve(__dirname, 'formatted.dat'); FORMATTED_FILE = path.resolve(__dirname, 'formatted.dat');
EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml'); EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml');
@@ -94,7 +239,8 @@ const log = (message) => {
}; };
async function downloadFile(url, filePath) { async function downloadFile(url, filePath) {
console.log(`Fetching ${url}`); Log.info(`Fetching ${url}`)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const isHttps = new URL(url).protocol === 'https:'; const isHttps = new URL(url).protocol === 'https:';
const httpModule = isHttps ? require('https') : require('http'); const httpModule = isHttps ? require('https') : require('http');
@@ -103,17 +249,25 @@ async function downloadFile(url, filePath) {
httpModule httpModule
.get(url, (response) => { .get(url, (response) => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
console.error(`Failed to download file: ${url}. Status code: ${response.statusCode}`); Log.error(
`Failed to download file: ${url}`,
chalk.white(``),
chalk.grey(`Status code: ${response.statusCode}`)
);
return reject(new 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); response.pipe(file);
file.on('finish', () => { file.on('finish', () => {
log(`Success: ${filePath}`); Log.ok(`Successfully fetched ${filePath}`)
file.close(() => resolve(true)); file.close(() => resolve(true));
}); });
}) })
.on('error', (err) => { .on('error', (err) => {
console.error(`Error downloading file: ${url}. Error: ${err.message}`); Log.error(
`Error downloading file: ${url}`,
chalk.white(``),
chalk.grey(`Status code: ${err.message}`)
);
fs.unlink(filePath, () => reject(err)); fs.unlink(filePath, () => reject(err));
}); });
}); });
@@ -124,9 +278,10 @@ async function ensureFileExists(url, filePath) {
await downloadFile(url, filePath); await downloadFile(url, filePath);
} catch (error) { } catch (error) {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
console.warn(`Using existing file for ${filePath} due to download failure.`); Log.warn(`Using existing local file ${filePath}, download failed`, chalk.white(``), chalk.grey(`${url}`));
} else { } else {
console.error(`Critical: Failed to download ${url}, and no local file exists.`); Log.error(`Failed to download file, and no local file exists; aborting`, chalk.white(``), chalk.grey(`${url}`));
throw error; throw error;
} }
} }
@@ -141,18 +296,19 @@ async function fetchSportsData() {
httpModule httpModule
.get(url, (response) => { .get(url, (response) => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
console.error(`Failed to fetch sports data. Status code: ${response.statusCode}`); Log.error(`Failed to fetch sports data. Server returned status code other than 200`, chalk.white(``), chalk.grey(`${url} - ${response.statusCode}`));
return reject(new 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 = ''; let data = '';
response.on('data', (chunk) => (data += chunk)); response.on('data', (chunk) => (data += chunk));
response.on('end', () => { response.on('end', () => {
log('Fetched sports data successfully.'); Log.ok(`Fetched sports data successfully`);
resolve(data); resolve(data);
}); });
}) })
.on('error', (err) => { .on('error', (err) => {
console.error(`Error fetching sports data: ${err.message}`); Log.error(`Error fetching sports data:`, chalk.white(``), chalk.grey(`${err.message}`));
reject(err); reject(err);
}); });
}); });
@@ -167,10 +323,19 @@ async function fetchRemote(url) {
'Accept-Encoding': 'gzip, deflate, br' 'Accept-Encoding': 'gzip, deflate, br'
} }
}, (resp) => { }, (resp) => {
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
Log.error(
`Server returned status code other than 200`,
chalk.white(``),
chalk.grey(`${url} - ${resp.statusCode}`)
);
return reject(new Error(`HTTP ${resp.statusCode} for ${url}`)); return reject(new Error(`HTTP ${resp.statusCode} for ${url}`));
} }
const chunks = []; const chunks = [];
resp.on('data', (chunk) => chunks.push(chunk)); resp.on('data', (chunk) => chunks.push(chunk));
resp.on('end', () => { resp.on('end', () => {
const buffer = Buffer.concat(chunks); const buffer = Buffer.concat(chunks);
@@ -206,18 +371,34 @@ async function serveKey(req, res) {
res.writeHead(400, { res.writeHead(400, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
Log.error(
`Missing "uri" parameter for key download`,
chalk.white(``),
chalk.grey(`${req.url}`)
);
return res.end('Error: Missing "uri" parameter for key download.'); return res.end('Error: Missing "uri" parameter for key download.');
} }
const keyData = await fetchRemote(uriParam); const keyData = await fetchRemote(uriParam);
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/octet-stream' 'Content-Type': 'application/octet-stream'
}); });
res.end(keyData); res.end(keyData);
} catch (err) { } catch (err) {
console.error('Error in serveKey:', err.message); Log.error(
`ServeKey Error:`,
chalk.white(``),
chalk.grey(`${err.message}`)
);
res.writeHead(500, { res.writeHead(500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
res.end('Error fetching key.'); res.end('Error fetching key.');
} }
} }
@@ -261,9 +442,11 @@ function fetchPage(url) {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
return reject(new Error(`Non-200 status ${res.statusCode} => ${url}`)); return reject(new Error(`Non-200 status ${res.statusCode} => ${url}`));
} }
if (res.headers['set-cookie']) { if (res.headers['set-cookie']) {
parseSetCookieHeaders(res.headers['set-cookie']); parseSetCookieHeaders(res.headers['set-cookie']);
} }
let data = ''; let data = '';
res.on('data', (chunk) => (data += chunk)); res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(data)); res.on('end', () => resolve(data));
@@ -278,6 +461,7 @@ async function getTokenizedUrl(channelUrl) {
let streamName; let streamName;
let streamHost; let streamHost;
if (channelUrl.includes('espn-')) { if (channelUrl.includes('espn-')) {
streamName = 'ESPN'; streamName = 'ESPN';
} else if (channelUrl.includes('espn2-')) { } else if (channelUrl.includes('espn2-')) {
@@ -285,35 +469,42 @@ async function getTokenizedUrl(channelUrl) {
} else { } else {
const streamNameMatch = html.match(/id="stream_name" name="([^"]+)"/); const streamNameMatch = html.match(/id="stream_name" name="([^"]+)"/);
if (!streamNameMatch) { if (!streamNameMatch) {
log('No "stream_name" found'); Log.error(`Cannot find "stream_name"`, chalk.white(``), chalk.grey(`${channelUrl}`));
return null; return null;
} }
streamName = streamNameMatch[1]; streamName = streamNameMatch[1];
} }
if (channelUrl.match('tvpass\.org')) { if (channelUrl.match('tvpass\.org')) {
streamHost = 'tvpass.org'; streamHost = 'tvpass.org';
}; };
if (channelUrl.match('thetvapp\.to')) { if (channelUrl.match('thetvapp\.to')) {
streamHost = 'thetvapp.to'; streamHost = 'thetvapp.to';
}; };
const tokenUrl = `https://${streamHost}/token/${streamName}?quality=hd`;
const tokenUrl = `https://${streamHost}/token/${streamName}?quality=${envStreamQuality}`;
const tokenResponse = await fetchPage(tokenUrl); const tokenResponse = await fetchPage(tokenUrl);
let finalUrl; let finalUrl;
try { try {
const json = JSON.parse(tokenResponse); const json = JSON.parse(tokenResponse);
finalUrl = json.url; finalUrl = json.url;
} catch (err) { } catch (err) {
log('Failed to parse token JSON'); Log.error(`Failed to parse token JSON for channel`, chalk.white(``), chalk.grey(`${channelUrl} - ${err.message}`));
return null; return null;
} }
if (!finalUrl) { if (!finalUrl) {
log('No URL found in the token JSON'); Log.error(`No URL found in token JSON for channel`, chalk.white(``), chalk.grey(`${channelUrl}`));
return null; return null;
} }
log(`Tokenized URL: ${finalUrl}`);
Log.debug(`Tokenized URL:`, chalk.white(``), chalk.grey(`${finalUrl}`));
return finalUrl; return finalUrl;
} catch (err) { } catch (err) {
log(`Fatal error fetching token: ${err.message}`); Log.error(`Fatal error fetching token:`, chalk.white(``), chalk.grey(`${err.message}`));
return null; return null;
} }
} }
@@ -321,15 +512,17 @@ async function getTokenizedUrl(channelUrl) {
async function serveChannelPlaylist(req, res) { async function serveChannelPlaylist(req, res) {
await semaphore.acquire(); await semaphore.acquire();
try { try {
const urlParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('url'); const urlParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('url');
if (!urlParam) { if (!urlParam) {
log('Error: Missing URL parameter'); Log.error(`Missing parameter`, chalk.white(``), chalk.grey(`URL`));
res.writeHead(400, { res.writeHead(400, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
res.end('Error: Missing URL parameter.'); res.end('Error: Missing URL parameter.');
return; return;
} }
const decodedUrl = decodeURIComponent(urlParam); const decodedUrl = decodeURIComponent(urlParam);
if (decodedUrl.endsWith('.ts')) { if (decodedUrl.endsWith('.ts')) {
res.writeHead(302, { res.writeHead(302, {
@@ -338,6 +531,7 @@ async function serveChannelPlaylist(req, res) {
res.end(); res.end();
return; return;
} }
const cachedUrl = getCache(decodedUrl); const cachedUrl = getCache(decodedUrl);
if (cachedUrl) { if (cachedUrl) {
const rewrittenPlaylist = await rewritePlaylist(cachedUrl, req); const rewrittenPlaylist = await rewritePlaylist(cachedUrl, req);
@@ -348,16 +542,21 @@ async function serveChannelPlaylist(req, res) {
res.end(rewrittenPlaylist); res.end(rewrittenPlaylist);
return; return;
} }
log(`Fetching stream: ${urlParam}`);
Log.info(`Fetching stream:`, chalk.white(``), chalk.grey(`${urlParam}`));
const finalUrl = await getTokenizedUrl(decodedUrl); const finalUrl = await getTokenizedUrl(decodedUrl);
if (!finalUrl) { if (!finalUrl) {
log('Error: Failed to retrieve tokenized URL'); Log.error(`Failed to retrieve tokenized URL`);
res.writeHead(500, { res.writeHead(500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
res.end('Error: Failed to retrieve tokenized URL.'); res.end('Error: Failed to retrieve tokenized URL.');
return; return;
} }
setCache(decodedUrl, finalUrl, 4 * 60 * 60 * 1000); setCache(decodedUrl, finalUrl, 4 * 60 * 60 * 1000);
const hdUrl = finalUrl.replace('tracks-v2a1', 'tracks-v1a1'); const hdUrl = finalUrl.replace('tracks-v2a1', 'tracks-v1a1');
const rewrittenPlaylist = await rewritePlaylist(hdUrl, req); const rewrittenPlaylist = await rewritePlaylist(hdUrl, req);
@@ -365,16 +564,20 @@ async function serveChannelPlaylist(req, res) {
'Content-Type': 'application/vnd.apple.mpegurl', 'Content-Type': 'application/vnd.apple.mpegurl',
'Content-Disposition': 'inline; filename="playlist.m3u8"', 'Content-Disposition': 'inline; filename="playlist.m3u8"',
}); });
res.end(rewrittenPlaylist); res.end(rewrittenPlaylist);
log('Served playlist'); Log.ok(`Served playlist`);
} catch (error) { } catch (error) {
log(`Error processing request: ${error.message}`); Log.error(`Error processing request:`, chalk.white(``), chalk.grey(`${error.message}`));
if (!res.headersSent) { if (!res.headersSent) {
res.writeHead(500, { res.writeHead(500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
res.end('Error processing request.'); res.end('Error processing request.');
} }
} finally { } finally {
semaphore.release(); semaphore.release();
} }
@@ -421,9 +624,11 @@ async function servePlaylist(response, req) {
'Content-Type': 'application/x-mpegURL', 'Content-Type': 'application/x-mpegURL',
'Content-Disposition': 'inline; filename="playlist.m3u8"', 'Content-Disposition': 'inline; filename="playlist.m3u8"',
}); });
response.end(updatedContent); response.end(updatedContent);
} catch (error) { } catch (error) {
Log.error(`Error in servePlaylist:`, chalk.white(``), chalk.grey(`${error.message}`));
console.error('Error in servePlaylist:', error.message); console.error('Error in servePlaylist:', error.message);
response.writeHead(500, { response.writeHead(500, {
@@ -448,14 +653,17 @@ async function serveXmltv(response, req) {
'Content-Type': 'application/xml', 'Content-Type': 'application/xml',
'Content-Disposition': 'inline; filename="xmltv.1.xml"', 'Content-Disposition': 'inline; filename="xmltv.1.xml"',
}); });
response.end(formattedContent); response.end(formattedContent);
} catch (error) { } catch (error) {
console.error('Error in servePlaylist:', error.message); Log.error(`Error in servePlaylist:`, chalk.white(``), chalk.grey(`${error.message}`));
response.writeHead(500, { response.writeHead(500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
response.end(`Error serving playlist: ${error.message}`); response.end(`Error serving playlist: ${error.message}`);
} }
@@ -522,7 +730,7 @@ function setCache(key, value, ttl) {
value, value,
expiry expiry
}); });
log(`Cache set: ${key}, expires in ${ttl / 1000} seconds`);
} }
function getCache(key) { function getCache(key) {
@@ -530,7 +738,9 @@ function getCache(key) {
if (cached && cached.expiry > Date.now()) { if (cached && cached.expiry > Date.now()) {
return cached.value; return cached.value;
} else { } else {
if (cached) log(`Cache expired for key: ${key}`); if (cached)
Log.debug(`Cache expired for key`, chalk.white(``), chalk.grey(`${key}`));
cache.delete(key); cache.delete(key);
return null; return null;
} }
@@ -538,17 +748,19 @@ function getCache(key) {
async function initialize() { async function initialize() {
try { try {
log('Initializing server...'); Log.info(`Initializing server...`);
await ensureFileExists(externalURL, URLS_FILE);
await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE); await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE);
await ensureFileExists(externalEPG, EPG_FILE); await ensureFileExists(externalEPG, EPG_FILE);
urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean); urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean);
if (urls.length === 0) { if (urls.length === 0) {
throw new Error(`No valid URLs found in ${URLS_FILE}`); throw new Error(`No valid URLs found in ${URLS_FILE}`);
} }
log('Initialization complete.');
Log.info(`Initializing Complete`);
} catch (error) { } catch (error) {
console.error(`Initialization error: ${error.message}`); Log.error(`Initialization error:`, chalk.white(``), chalk.grey(`${error.message}`));
} }
} }
@@ -678,21 +890,47 @@ const server = http.createServer((req, res) => {
res.end(htmlContent); res.end(htmlContent);
return; return;
} }
if (req.url === '/playlist' && req.method === 'GET') { if (req.url === '/playlist' && req.method === 'GET') {
log('Playlist request received'); Log.info(
`Received request for playlist data`,
chalk.white(``),
chalk.grey(`/playlist`)
);
await servePlaylist(res, req); await servePlaylist(res, req);
return; return;
} }
if (req.url.startsWith('/channel') && req.method === 'GET') { if (req.url.startsWith('/channel') && req.method === 'GET') {
Log.info(
`Received request for channel data`,
chalk.white(``),
chalk.grey(`/channel`)
);
await serveChannelPlaylist(req, res); await serveChannelPlaylist(req, res);
return; return;
} }
if (req.url.startsWith('/key') && req.method === 'GET') { if (req.url.startsWith('/key') && req.method === 'GET') {
Log.info(
`Received request for key data`,
chalk.white(``),
chalk.grey(`/key`)
);
await serveKey(req, res); await serveKey(req, res);
return; return;
} }
if (req.url === '/epg' && req.method === 'GET') { if (req.url === '/epg' && req.method === 'GET') {
log('Epg request received'); Log.info(
`Received request for EPG data`,
chalk.white(``),
chalk.grey(`/epg`)
);
await serveXmltv(res, req); await serveXmltv(res, req);
return; return;
/*res.writeHead(302, { /*res.writeHead(302, {
@@ -701,16 +939,24 @@ const server = http.createServer((req, res) => {
res.end(); res.end();
return;*/ return;*/
} }
res.writeHead(404, { res.writeHead(404, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
res.end('Not Found'); res.end('Not Found');
}; };
handleRequest().catch((error) => { handleRequest().catch((error) => {
console.error('Error handling request:', error); Log.error(
`Error handling request:`,
chalk.white(``),
chalk.grey(`${error}`)
);
res.writeHead(500, { res.writeHead(500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
res.end('Internal Server Error'); res.end('Internal Server Error');
}); });
}); });
@@ -718,7 +964,7 @@ const server = http.createServer((req, res) => {
(async () => { (async () => {
await initialize(); await initialize();
const PORT = process.env.WEB_PORT; const PORT = process.env.WEB_PORT;
server.listen(PORT, `${process.env.WEB_IP}`, () => { Log.info(`Server is running on ${envWebIP}:${envWebPort}`)
log(`Server is running on port ${PORT}`); log(`Server is running on port ${PORT}`);
}); });
})(); })();