Files
tvapp2/tvapp2/index.js

2408 lines
106 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
Import Packages
*/
import fs from 'fs';
import path from 'path';
import http from 'http';
import https from 'https';
import zlib from 'zlib';
import chalk from 'chalk';
import ejs from 'ejs';
import moment from 'moment';
import * as child from 'child_process';
import cron, { schedule } from 'node-cron';
import * as crons from 'cron';
/*
Old CJS variables converted to ESM
*/
import { fileURLToPath } from 'url';
const cache = new Map();
/*
Import package.json values
*/
const { name, author, version, repository, discord, docs } = JSON.parse( fs.readFileSync( './package.json' ) );
const __filename = fileURLToPath( import.meta.url ); // get resolved path to file
const __dirname = path.dirname( __filename ); // get name of directory
/*
const gitHash = child.execSync( 'git rev-parse HEAD' ).toString().trim();
*/
/*
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
@note if you change `envWebFolder`; ensure you re-name the folder where the
website assets are stored.
*/
let FILE_URL;
let FILE_M3U;
let FILE_XML;
let FILE_GZP;
let FILE_M3U_SIZE = 0;
let FILE_XML_SIZE = 0;
let FILE_GZP_SIZE = 0;
let FILE_M3U_MODIFIED = 0;
let FILE_XML_MODIFIED = 0;
let FILE_GZP_MODIFIED = 0;
/*
Define > Environment Variables || Defaults
*/
const envAppRelease = process.env.RELEASE || 'stable';
const envUrlRepo = process.env.URL_REPO || 'https://git.binaryninja.net/binaryninja';
const envStreamQuality = process.env.STREAM_QUALITY || 'hd';
const envFileURL = process.env.FILE_URL || 'urls.txt';
const envFileM3U = process.env.FILE_M3U || 'playlist.m3u8';
const envFileXML = process.env.FILE_EPG || 'xmltv.xml';
const envFileGZP = process.env.FILE_GZP || 'xmltv.xml.gz';
const envApiKey = process.env.API_KEY || null;
const envWebIP = process.env.WEB_IP || '0.0.0.0';
const envWebPort = process.env.WEB_PORT || `4124`;
const envWebFolder = process.env.WEB_FOLDER || 'www';
const envWebEncoding = process.env.WEB_ENCODING || 'deflate, br';
const envProxyHeader = process.env.WEB_PROXY_HEADER || 'x-forwarded-for';
const envHealthTimer = process.env.HEALTH_TIMER || 600000;
const envTaskCronSync = process.env.TASK_CRON_SYNC || '0 0 */3 * *';
const envGitSHA1 = process.env.GIT_SHA1 || '0000000000000000000000000000000000000000';
const LOG_LEVEL = process.env.LOG_LEVEL || 4;
/*
Define > Externals
*/
const extURL = `${ envUrlRepo }/tvapp2-externals/raw/branch/main/urls.txt`;
const extXML = `${ envUrlRepo }/XMLTV-EPG/raw/branch/main/xmltv.1.xml`;
const extM3U = `${ envUrlRepo }/tvapp2-externals/raw/branch/main/formatted.dat`;
/*
Define > Defaults
*/
let urls = [];
const gCookies = {};
const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0';
/*
Web url shortcuts
using any of the following subdomains / subpaths will trigger the download for that specific file
@example http://127.0.0.1:4124/gzip
http://127.0.0.1:4124/gz
http://127.0.0.1:4124/playlist
http://127.0.0.1:4124/key
http://127.0.0.1:4124/channel?url=https://thetvapp.to/tv/bbc-america-live-stream/
http://127.0.0.1:4124/api/health
*/
const subdomainGZP = [ 'gzip', 'gz' ];
const subdomainM3U = [ 'playlist', 'm3u', 'm3u8' ];
const subdomainEPG = [ 'guide', 'epg', 'xml' ];
const subdomainKey = [ 'key', 'keys' ];
const subdomainChan = [ 'channels', 'channel', 'chan' ];
const subdomainHealth = [ 'api/status', 'api/health' ];
const subdomainRestart = [ 'api/restart', 'api/sync', 'api/resync' ];
/*
Container Information
these environment variables are defined from the s6-overlay layer of the docker image
*/
const fileIpGateway = '/var/run/s6/container_environment/IP_GATEWAY';
const fileIpContainer = '/var/run/s6/container_environment/IP_CONTAINER';
const envIpGateway = fs.existsSync( fileIpGateway ) ? fs.readFileSync( fileIpGateway, 'utf8' ) : `0.0.0.0`;
const envIpContainer = fs.existsSync( fileIpContainer ) ? fs.readFileSync( fileIpContainer, 'utf8' ) : `0.0.0.0`;
/*
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.verbose(`This is verbose`)
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 verbose( ...msg )
{
if ( LOG_LEVEL >= 6 )
console.debug( chalk.white.bgBlack.blackBright.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
}
static debug( ...msg )
{
if ( LOG_LEVEL >= 7 )
console.trace( chalk.white.bgMagenta.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.magentaBright( msg.join( ' ' ) ) );
else if ( LOG_LEVEL >= 5 )
console.debug( chalk.white.bgGray.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
}
static info( ...msg )
{
if ( LOG_LEVEL >= 4 )
console.info( chalk.white.bgBlueBright.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.blueBright( msg.join( ' ' ) ) );
}
static ok( ...msg )
{
if ( LOG_LEVEL >= 4 )
console.log( chalk.white.bgGreen.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.greenBright( msg.join( ' ' ) ) );
}
static notice( ...msg )
{
if ( LOG_LEVEL >= 3 )
console.log( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
}
static warn( ...msg )
{
if ( LOG_LEVEL >= 2 )
console.warn( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
}
static error( ...msg )
{
if ( LOG_LEVEL >= 1 )
console.error( chalk.white.bgRedBright.bold( ` ${ name } ` ), chalk.white( `` ), this.now(), chalk.redBright( msg.join( ' ' ) ) );
}
}
/*
Process
*/
if ( process.pkg )
{
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Starting server utilizing process.execPath` ) );
const basePath = path.dirname( process.execPath );
FILE_URL = path.join( basePath, envWebFolder, `${ envFileURL }` );
FILE_M3U = path.join( basePath, envWebFolder, `${ envFileM3U }` );
FILE_XML = path.join( basePath, envWebFolder, `${ envFileXML }` );
FILE_XML.length;
FILE_GZP = path.join( basePath, envWebFolder, `${ envFileGZP }` );
}
else
{
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Starting server utilizing processed locals` ) );
FILE_URL = path.resolve( __dirname, envWebFolder, `${ envFileURL }` );
FILE_M3U = path.resolve( __dirname, envWebFolder, `${ envFileM3U }` );
FILE_XML = path.resolve( __dirname, envWebFolder, `${ envFileXML }` );
FILE_GZP = path.resolve( __dirname, envWebFolder, `${ envFileGZP }` );
}
/*
Get Client IP
prioritize header.
*/
const clientIp = ( req ) =>
( req.headers && (
req.headers[envProxyHeader]?.split( ',' )?.shift() ||
req.headers['X-Forwarded-For']?.split( ',' )?.shift() ||
req.headers['x-forwarded-for']?.split( ',' )?.shift() ||
req.headers['cf-connecting-ip']?.split( ',' )?.shift() ||
req.headers['x-real-ip']?.split( ',' )?.shift() ||
req.headers['X-Real-IP']?.split( ',' )?.shift() ||
req.socket?.remoteAddress ) ||
envIpContainer );
/*
/*
Semaphore > Declare
allows multiple threads to work with the same shared resources
*/
class Semaphore
{
constructor( max )
{
this.max = max;
this.queue = [];
this.active = 0;
}
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();
}
}
}
/*
Check Service Status
this function attempts to see if a specified domain is up.
will first start with the URL you provide.
if try 1 fails, it will determine if that URL used protocol https or https and then flip to the other
if try 2 fails with the opposite protocol; domain is considered down
*/
async function serviceCheck( service, uri )
{
/* try 1 */
try
{
const response = await fetch( uri );
/* try 1 > domain down */
if ( response.status !== 200 )
{
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ response.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ) );
return;
}
/* try 1 > domain up */
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `` ), chalk.greenBright( `<msg>` ), chalk.gray( `Service Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ response.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uri }` ) );
}
catch ( err )
{
/*
try 2 > http
*/
if ( /^https:\/\//i.test( uri ) )
{
const uriRetry = uri.replace( /^https:\/\//ig, 'http://' );
Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service via SSL; trying http protocol` ), chalk.yellowBright( `<service>` ), chalk.gray( `${ service }` ), chalk.yellowBright( `<uriAttempt1>` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `<uriAttempt2>` ), chalk.gray( `${ uriRetry }` ), chalk.blueBright( `(pending)` ) );
try
{
const response = await fetch( uriRetry );
/* try 2 > http > domain down */
if ( response.status !== 200 )
{
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ response.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
return;
}
/* try 2 > http > domain up */
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `` ), chalk.greenBright( `<msg>` ), chalk.gray( `Service Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ response.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
}
catch ( err )
{
/* try 2 > http > domain not exist */
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, address does not exist` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ), chalk.redBright( `<message>` ), chalk.gray( `${ err }` ) );
}
}
/*
try 2 > https
*/
else if ( /^http:\/\//i.test( uri ) )
{
const uriRetry = uri.replace( /^http:\/\//ig, 'https://' );
Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service via SSL; trying https protocol` ), chalk.yellowBright( `<service>` ), chalk.gray( `${ service }` ), chalk.yellowBright( `<uriAttempt1>` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `<uriAttempt2>` ), chalk.gray( `${ uriRetry }` ), chalk.blueBright( `(pending)` ) );
try
{
const response = await fetch( uriRetry );
/* try 2 > https > domain down */
if ( response.status !== 200 )
{
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `<code>` ), chalk.gray( `${ response.status }` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
return;
}
/* try 2 > https > domain up */
Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `` ), chalk.greenBright( `<msg>` ), chalk.gray( `Service Online` ), chalk.greenBright( `<code>` ), chalk.gray( `${ response.status }` ), chalk.greenBright( `<service>` ), chalk.gray( `${ service }` ), chalk.greenBright( `<address>` ), chalk.gray( `${ uriRetry }` ) );
}
catch ( err )
{
/* try 2 > https > domain not exist */
Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `` ), chalk.redBright( `<msg>` ), chalk.gray( `Service Offline; failed to communicate with service, address does not exist` ), chalk.redBright( `<service>` ), chalk.gray( `${ service }` ), chalk.redBright( `<address>` ), chalk.gray( `${ uri }` ), chalk.redBright( `<message>` ), chalk.gray( `${ err }` ) );
}
}
}
}
/*
Semaphore > Initialize
@arg int threads_max
*/
const semaphore = new Semaphore( 5 );
/*
Func > Download File
@arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt
@arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt
@ret Promise<>
*/
async function downloadFile( url, filePath )
{
return new Promise( ( resolve, reject ) =>
{
Log.info( `file`, chalk.yellow( `[download]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Preparing to download external file` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
const isHttps = new URL( url ).protocol === 'https:';
const httpModule = isHttps ? https : http;
const file = fs.createWriteStream( filePath );
httpModule
.get( url, ( res ) =>
{
Log.info( `file`, chalk.yellow( `[retrieve]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Getting response from file download request` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ res.statusCode }` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
if ( res.statusCode !== 200 )
{
Log.error( `file`, chalk.redBright( `[download]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Attempt to download external file returned non-200 status` ),
chalk.redBright( `<code>` ), chalk.gray( `${ res.statusCode }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
return reject( new Error( `Failed to download file: ${ url }. Status code: ${ res.statusCode }` ) );
}
res.pipe( file );
file.on( 'finish', () =>
{
Log.ok( `file`, chalk.yellow( `[download]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Successfully downloaded external file` ),
chalk.greenBright( `<code>` ), chalk.gray( `${ res.statusCode }` ),
chalk.greenBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.greenBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
file.close( () => resolve( true ) );
});
})
.on( 'error', ( err ) =>
{
Log.error( `file`, chalk.redBright( `[download]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Failed to download external source file` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
fs.unlink( filePath, () => reject( err ) );
});
});
}
/*
Get Filesize and convert to human readable format
@arg str filename filename to get size in bytes for
@ret str 2025-03-23 04:11 am
*/
function getFileModified( filename )
{
return moment( fs.statSync( filename ).mtime ).format( 'YYYY-MM-DD h:mm a' );
}
/*
Func > Get Human Readable Filesize
Takes the total number of bytes in a file's size and converts it into
a human readable format.
@arg str filename filename to get size in bytes for
@arg bool si divides the bytes of a file by 1000 instead of 2024
@arg int decimal specifies the decimal point
@ret str 111.9 KB
*/
function getFileSizeHuman( filename, si = true, decimal = 1 )
{
let stats = [];
stats.size = 0;
if ( fs.existsSync( filename ) )
stats = fs.statSync( filename );
let bytes = stats.size;
const thresh = si ? 1000 : 1024;
if ( Math.abs( bytes ) < thresh )
return bytes + ' B';
const units = si
? [
'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'
]
: [
'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'
];
let u = -1;
const r = 10 ** decimal;
do
{
bytes /= thresh;
++u;
} while ( Math.round( Math.abs( bytes ) * r ) / r >= thresh && u < units.length - 1 );
return bytes.toFixed( decimal ) + ' ' + units[u];
}
/*
Func > Get Files
if file exists; start download from external website utilizing url and file path arguments; or
throw error to user that file does not exist via the URL.
If file cannot be obtained from external url; use local copy if available
@arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt
@arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt
@ret none
*/
async function getFile( url, filePath )
{
try
{
Log.debug( `file`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to download external file` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
await downloadFile( url, filePath );
}
catch ( err )
{
if ( fs.existsSync( filePath ) )
{
Log.warn( `file`, chalk.yellow( `[requests]` ), chalk.white( `⚠️` ),
chalk.yellowBright( `<msg>` ), chalk.gray( `Download failed - Using existing local file ${ filePath }` ),
chalk.yellowBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.yellowBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
}
else
{
Log.error( `file`, chalk.redBright( `[requests]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Download filed and no local backup exists, aborting` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ url }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ filePath }` ) );
throw err;
}
}
}
/*
Func > Create GZip
locates the xmltv.xml and packages it into a xmltv.gz archive
*/
async function createGzip( )
{
return new Promise( ( resolve, reject ) =>
{
Log.info( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Preparing to create compressed XML gz file` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
fs.readFile( FILE_XML, ( err, buf ) =>
{
Log.debug( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Reading source XML file` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
if ( err )
{
Log.error( `.gzp`, chalk.redBright( `[generate]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Could not read source XML file` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
return reject( new Error( `Could not read file ${ envFileXML }. Error: ${ err }` ) );
}
zlib.gzip( buf, ( err, buf ) =>
{
Log.debug( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Starting zlib.gzip` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
if ( err )
{
Log.error( `.gzp`, chalk.redBright( `[generate]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Could not create gz archive` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
return reject( new Error( `Could not create ${ envFileGZP }. Error: ${ err }` ) );
}
Log.info( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Started creating gz archive from XML source` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
fs.writeFile( `${ FILE_GZP }`, buf, ( err ) =>
{
if ( err )
{
Log.error( `.gzp`, chalk.redBright( `[generate]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Could not write to and create gz archive` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
return reject( new Error( `Could not write XML file ${ envFileXML } to ${ envFileGZP }. Error: ${ err }` ) );
}
Log.ok( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Successfully created compressed gz archive from XML source file` ),
chalk.greenBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.greenBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
resolve( true );
});
});
});
});
}
/*
Func > Get Gzip
try; catch to create a .gz compressed file from the .xml guide data
*/
async function getGzip( )
{
try
{
Log.debug( `.gzp`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to create compressed gzip from uncompressed XML data` ),
chalk.blueBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.blueBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
await createGzip( );
}
catch ( err )
{
if ( fs.existsSync( FILE_XML ) )
{
Log.warn( `.gzp`, chalk.yellow( `[requests]` ), chalk.white( `⚠️` ),
chalk.yellowBright( `<msg>` ), chalk.yellowBright( `Cannot get compressed gzip; but source XML file found and can be used` ),
chalk.yellowBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.yellowBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
}
else
{
Log.error( `.gzp`, chalk.redBright( `[requests]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Failed to get compressed gzip, and source XML file not found` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<src>` ), chalk.gray( `${ FILE_XML }` ),
chalk.redBright( `<dest>` ), chalk.gray( `${ FILE_GZP }` ) );
throw err;
}
}
}
/*
@note Jellyfin Users
Originally, this node webserver enabled gzip compression for the value `Accept-Encoding`. Doing this
may cause an error to appear in Jellyfin logs / console when attempting to fetch the latest guide data
from the tvapp2 xml file.
[ERR] [27] Jellyfin.LiveTv.Guide.GuideManager: Error getting programs for channel XXXXXXXXXXXXXXX (Source 2)
System.Xml.XmlException: '', hexadecimal value 0x1F, is an invalid character. Line 1, position 1.
To fix the error, we create a customizable env variable that allows the user to override the encoding header.
We change the following:
'Accept-Encoding': 'gzip, deflate, br'
to
'Accept-Encoding': 'deflate, br'
This error does not appear if you load the xml guide data into Cabernet, and then use a tuner to fetch the data from
cabernet to jellyfin
*/
async function fetchRemote( url, req )
{
return new Promise( ( resolve, reject ) =>
{
Log.info( `remo`, chalk.yellow( `[generate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Preparing to fetch remote request` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
const mod = url.startsWith( 'https' ) ? https : http;
mod
.get( url, {
headers: {
'Accept-Encoding': envWebEncoding
}
}, ( resp ) =>
{
Log.info( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Getting response from remote fetch request` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
if ( resp.statusCode !== 200 )
{
Log.error( `remo`, chalk.redBright( `[retrieve]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Remote fetch returned status code other than 200` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ url }` ) );
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 )
{
Log.error( `remo`, chalk.redBright( `[retrieve]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Remote fetch could not complete encoding type ${ encoding }` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ url }` ) );
return reject( err );
}
Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Remote fetch detected encoding type ${ encoding }; decoding` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
resolve( decoded );
});
}
else if ( encoding === 'deflate' )
{
zlib.inflate( buffer, ( err, decoded ) =>
{
if ( err )
{
Log.error( `remo`, chalk.redBright( `[retrieve]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Remote fetch could not complete encoding type ${ encoding }` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ url }` ) );
return reject( err );
}
Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Remote fetch detected encoding type ${ encoding }; decoding` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
resolve( decoded );
});
}
else if ( encoding === 'br' )
{
zlib.brotliDecompress( buffer, ( err, decoded ) =>
{
if ( err )
{
Log.error( `remo`, chalk.redBright( `[retrieve]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Remote fetch could not complete encoding type ${ encoding } (brotli decompress)` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ url }` ) );
return reject( err );
}
Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Remote fetch detected encoding type ${ encoding } (brotli decompress); decoding` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
resolve( decoded );
});
}
else
{
Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Remote fetch contains no headers to decode; resolving buffer` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<encoding>` ), chalk.gray( `${ encoding }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ resp.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
resolve( buffer );
}
});
})
.on( 'error', reject );
});
}
/*
Serve Keys
@url https://tvapp2.domain.lan/keys?uri=https://v16.thetvapp.to/hls/WABCDT1/tracks-v2a1/mono.m3u8?token=a0b2C-1ae-qaxAV5iKAd8g&expires=1746394920&user_id=EjLZVsIiJphafFxXRVWRdVWPvzTqpWBZbchvsTwpAlrQZzFuZMpdSn==
*/
async function serveKey( req, res )
{
try
{
const paramUrl = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'uri' );
if ( !paramUrl )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: 'Error: Missing "uri" parameter for key download.',
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 400,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `keys`, chalk.redBright( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveKey` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.redBright( `<param>` ), chalk.gray( `empty; missing var` ) );
return res.end( JSON.stringify( statusCheck ) );
}
Log.debug( `keys`, chalk.yellow( `[response]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Valid paramUrl specified; establishing connection to serve key to client` ),
chalk.blueBright( `<cat>` ), chalk.gray( `serveKey` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.blueBright( `<param>` ), chalk.gray( `${ paramUrl }` ) );
const keyData = await fetchRemote( paramUrl, req );
res.writeHead( 200, {
'Content-Type': 'application/octet-stream'
});
Log.ok( `keys`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Serving key to client` ),
chalk.greenBright( `<cat>` ), chalk.gray( `serveKey` ),
chalk.greenBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.greenBright( `<code>` ), chalk.gray( `200` ),
chalk.greenBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.greenBright( `<param>` ), chalk.gray( `${ paramUrl }` ),
chalk.greenBright( `<keyData>` ), chalk.gray( `${ keyData }` ) );
res.end( keyData );
}
catch ( err )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Failed to serve key; try{} failed. Ensure you specify a valid uri to tvapp / tvpass`,
error: `${ err.message }`,
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 500,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `keys`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveKey` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ statusCheck.error }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ req.url }` ) );
res.end( JSON.stringify( statusCheck ) );
}
}
/*
cookies > headers > parse
*/
function cookieHeadersSetParse( values )
{
if ( !Array.isArray( values ) ) return;
values.forEach( ( line ) =>
{
const [cookiePair] = line.split( ';' );
if ( cookiePair )
{
const [ key, val ] = cookiePair.split( '=' );
if ( key && val )
{
gCookies[key.trim()] = val.trim();
}
}
});
}
/*
cookies > headers > build
*/
function cookieHeadersBuild()
{
const pairs = [];
for ( const [ k, v ] of Object.entries( gCookies ) )
{
pairs.push( `${ k }=${ v }` );
}
return pairs.join( '; ' );
}
/*
fetch > page
*/
function fetchPage( url, req )
{
return new Promise( ( resolve, reject ) =>
{
Log.info( `http`, chalk.yellow( `[generate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Preparing to fetch remote page` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
const opts = {
method: 'GET',
headers: {
'User-Agent': USERAGENT,
Accept: '*/*',
Cookie: cookieHeadersBuild()
}
};
https
.get( url, opts, ( res ) =>
{
Log.info( `http`, chalk.yellow( `[retrieve]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Status code returned` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ res.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
if ( res.statusCode !== 200 )
{
Log.debug( `http`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Failed to load url; status 200 was not returned` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ res.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
return reject( new Error( `page did not return code 200 status ${ res.statusCode } => ${ url }` ) );
}
if ( res.headers['set-cookie'])
{
Log.debug( `http`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Setting headers` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<code>` ), chalk.gray( `${ res.statusCode }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ),
chalk.blueBright( `<header>` ), chalk.gray( `set-cookie` ) );
cookieHeadersSetParse( res.headers['set-cookie']);
}
let data = '';
res.on( 'data', ( chunk ) => ( data += chunk ) );
res.on( 'end', () => resolve( data ) );
})
.on( 'error', reject );
});
}
/*
tokenized url > get
*/
async function getTokenizedUrl( channelUrl, req )
{
try
{
const html = await fetchPage( channelUrl, req );
let streamName;
let streamHost;
Log.debug( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to get tokenize url` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ) );
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.error( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Cannot find streamNameMatch; returned empty` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<url>` ), chalk.grey( `${ channelUrl }` ),
chalk.redBright( `<streamNameMatch>` ), chalk.grey( `missing / var empty` ) );
return null;
}
streamName = streamNameMatch[1];
Log.debug( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `streamName found` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.blueBright( `<streamHost>` ), chalk.gray( `not yet assigned` ),
chalk.blueBright( `<streamName>` ), chalk.gray( `${ streamName }` ) );
}
if ( channelUrl.match( 'tvpass\.org' ) )
{
streamHost = 'tvpass.org';
};
if ( channelUrl.match( 'thetvapp\.to' ) )
{
streamHost = 'thetvapp.to';
};
const tokenUrl = `https://${ streamHost }/token/${ streamName }?quality=${ envStreamQuality.toLowerCase() }`;
const tokenResponse = await fetchPage( tokenUrl, req );
let tokenizedUrl;
Log.debug( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Generating tokenized final stream URL` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.blueBright( `<streamHost>` ), chalk.gray( `${ streamHost }` ),
chalk.blueBright( `<streamName>` ), chalk.gray( `${ streamName }` ),
chalk.blueBright( `<streamQuality>` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ),
chalk.blueBright( `<tokenUrl>` ), chalk.gray( `${ tokenUrl }` ),
chalk.blueBright( `<tokenizedUrl>` ), chalk.gray( `not yet assigned` ) );
try
{
const json = JSON.parse( tokenResponse );
tokenizedUrl = json.url;
Log.debug( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Returned token response in json format` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.blueBright( `<streamHost>` ), chalk.gray( `${ streamHost }` ),
chalk.blueBright( `<streamName>` ), chalk.gray( `${ streamName }` ),
chalk.blueBright( `<streamQuality>` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ),
chalk.blueBright( `<tokenUrl>` ), chalk.gray( `${ tokenUrl }` ),
chalk.blueBright( `<tokenResponse>` ), chalk.gray( `${ json }` ),
chalk.blueBright( `<tokenizedUrl>` ), chalk.gray( `${ tokenizedUrl }` ) );
}
catch ( err )
{
Log.error( `play`, chalk.redBright( `[tokenize]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Failed to parse token JSON for channel` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.redBright( `<streamHost>` ), chalk.gray( `${ streamHost }` ),
chalk.redBright( `<streamName>` ), chalk.gray( `${ streamName }` ),
chalk.redBright( `<streamQuality>` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ),
chalk.redBright( `<tokenUrl>` ), chalk.gray( `${ tokenUrl }` ),
chalk.redBright( `<tokenizedUrl>` ), chalk.gray( `not yet assigned` ) );
return null;
}
if ( !tokenizedUrl )
{
Log.error( `play`, chalk.redBright( `[tokenize]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `No URL found in token JSON for channel` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.redBright( `<streamHost>` ), chalk.gray( `${ streamHost }` ),
chalk.redBright( `<streamName>` ), chalk.gray( `${ streamName }` ),
chalk.redBright( `<streamQuality>` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ),
chalk.redBright( `<tokenUrl>` ), chalk.gray( `${ tokenUrl }` ),
chalk.redBright( `<tokenizedUrl>` ), chalk.gray( `missing` ) );
return null;
}
Log.ok( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Successfully generated token for stream` ),
chalk.greenBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.greenBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.greenBright( `<streamHost>` ), chalk.gray( `${ streamHost }` ),
chalk.greenBright( `<streamName>` ), chalk.gray( `${ streamName }` ),
chalk.greenBright( `<streamQuality>` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ),
chalk.greenBright( `<host>` ), chalk.gray( `${ streamHost }` ),
chalk.greenBright( `<url>` ), chalk.gray( `${ tokenizedUrl }` ),
chalk.greenBright( `<tokenUrl>` ), chalk.gray( `${ tokenUrl }` ) );
return tokenizedUrl;
}
catch ( err )
{
Log.error( `play`, chalk.redBright( `[tokenize]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Fatal error fetching token` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<channelUrl>` ), chalk.gray( `${ channelUrl }` ),
chalk.redBright( `<streamHost>` ), chalk.gray( `not defined` ),
chalk.redBright( `<streamName>` ), chalk.gray( `not defined` ),
chalk.redBright( `<streamQuality>` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ),
chalk.redBright( `<tokenUrl>` ), chalk.gray( `not defined` ) );
return null;
}
}
/*
serve > m3u playlist
*/
async function serveM3UPlaylist( req, res )
{
await semaphore.acquire();
try
{
const method = req.method || 'GET';
Log.debug( `plst`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to serve M3U playlist` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
/*
paramUrl > decodedUrl > tokenizedUrl
*/
const paramUrl = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'url' );
if ( !paramUrl )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Missing ?url= parameter for var paramUrl`,
status: `unhealthy`,
ref: req.url,
method: req.method || 'GET',
code: 404,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `plst`, chalk.redBright( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<expected>` ), chalk.gray( `http://${ req.headers.host }/channel?url=XXXX` ),
chalk.redBright( `<method>` ), chalk.gray( `${ method }` ) );
res.end( JSON.stringify( statusCheck ) );
return;
}
const decodedUrl = decodeURIComponent( paramUrl );
if ( decodedUrl.endsWith( '.ts' ) )
{
res.writeHead( 302, {
Location: decodedUrl
});
Log.notice( `plst`, chalk.yellow( `[response]` ), chalk.white( `📌` ),
chalk.yellowBright( `<msg>` ), chalk.gray( `decodedUrl ends with .ts script; (302) redirecting` ),
chalk.yellowBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.yellowBright( `<code>` ), chalk.gray( `302` ),
chalk.yellowBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.yellowBright( `<paramUrl>` ), chalk.gray( `${ paramUrl }` ),
chalk.yellowBright( `<decodedUrl>` ), chalk.gray( `${ decodedUrl }` ),
chalk.yellowBright( `<method>` ), chalk.gray( `${ method }` ) );
res.end();
return;
}
const cachedUrl = getCache( decodedUrl, req );
if ( cachedUrl )
{
const rewrittenPlaylist = await rewriteM3U( cachedUrl, req );
res.writeHead( 200,
{
'Content-Type': 'application/vnd.apple.mpegurl',
'Content-Disposition': 'inline; filename="' + envFileM3U
});
Log.debug( `plst`, chalk.yellow( `[response]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Serving cachedUrl m3u playlist to client` ),
chalk.blueBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.blueBright( `<code>` ), chalk.gray( `200` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<paramUrl>` ), chalk.gray( `${ paramUrl }` ),
chalk.blueBright( `<decodedUrl>` ), chalk.gray( `${ decodedUrl }` ),
chalk.blueBright( `<cachedUrl>` ), chalk.gray( `${ cachedUrl }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
res.end( rewrittenPlaylist );
return;
}
Log.info( `plst`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Request not cached; generating new tokenizedUrl for m3u playlist to client` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.blueBright( `<paramUrl>` ), chalk.gray( `${ paramUrl }` ),
chalk.blueBright( `<decodedUrl>` ), chalk.gray( `${ decodedUrl }` ),
chalk.blueBright( `<cachedUrl>` ), chalk.gray( `${ cachedUrl }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
/*
get tokenized url
*/
const tokenizedUrl = await getTokenizedUrl( decodedUrl, req );
if ( !tokenizedUrl )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Failed to retrieve tokenized URL.`,
status: `unhealthy`,
ref: req.url,
method: req.method || 'GET',
code: 500,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `plst`, chalk.redBright( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<decodedUrl>` ), chalk.gray( `${ decodedUrl }` ),
chalk.redBright( `<tokenizedUrl>` ), chalk.gray( `${ tokenizedUrl }` ),
chalk.redBright( `<method>` ), chalk.gray( `${ method }` ) );
res.end( JSON.stringify( statusCheck ) );
return;
}
setCache( decodedUrl, tokenizedUrl, 4 * 60 * 60 * 1000, req );
const hdUrl = tokenizedUrl.replace( 'tracks-v2a1', 'tracks-v1a1' );
const rewrittenPlaylist = await rewriteM3U( hdUrl, req );
res.writeHead( 200, {
'Content-Type': 'application/vnd.apple.mpegurl',
'Content-Disposition': 'inline; filename="' + envFileM3U
});
Log.ok( `plst`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Serving new tokenizedUrl with m3u playlist to client` ),
chalk.greenBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.greenBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.greenBright( `<code>` ), chalk.gray( `200` ),
chalk.greenBright( `<playlist>` ), chalk.gray( `${ envFileM3U }` ),
chalk.greenBright( `<paramUrl>` ), chalk.gray( `${ paramUrl }` ),
chalk.greenBright( `<decodedUrl>` ), chalk.gray( `${ decodedUrl }` ),
chalk.greenBright( `<tokenizedUrl>` ), chalk.gray( `${ tokenizedUrl }` ),
chalk.greenBright( `<method>` ), chalk.gray( `${ method }` ) );
res.end( rewrittenPlaylist );
}
catch ( err )
{
if ( !res.headersSent )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Cannot process request when fetching channel playlist`,
error: `${ err.message }`,
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 500,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `plst`, chalk.redBright( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Failed to serve m3u playlist` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveM3UPlaylist` ),
chalk.redBright( `<error>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) );
}
}
finally
{
semaphore.release();
}
}
/*
serve > health check
*/
async function serveHealthCheck( req, res )
{
await semaphore.acquire();
try
{
const paramUrl = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'api' );
if ( !paramUrl )
{
Log.debug( `/api`, chalk.yellow( `[health]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `No API key passed to health check` ) );
}
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `healthy`,
status: `healthy`,
ref: req.url,
method: req.method || 'GET',
code: 200,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.ok( `/api`, chalk.yellow( `[health]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Response` ),
chalk.greenBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.greenBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.greenBright( `<status>` ), chalk.gray( `${ statusCheck.status }` ),
chalk.greenBright( `<uptime>` ), chalk.gray( `${ process.uptime() }` ) );
res.end( JSON.stringify( statusCheck ) );
return;
}
catch ( err )
{
if ( !res.headersSent )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `health check failed`,
error: `${ err.message }`,
status: `unhealthy`,
ref: req.url,
method: req.method || 'GET',
code: 503,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `/api`, chalk.redBright( `[health]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message } response` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<status>` ), chalk.gray( `${ statusCheck.status }` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<uptime>` ), chalk.gray( `${ process.uptime() }` ) );
res.end( JSON.stringify( statusCheck ) );
}
}
finally
{
semaphore.release();
}
}
/*
Rewrites the URLs
*/
async function rewriteM3U( originalUrl, req )
{
const rawData = await fetchRemote( originalUrl, req );
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 ) }`;
});
}
/*
serve > m3u
Serves IPTV .m3u playlist
*/
async function serveM3U( res, req )
{
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( FILE_M3U, 'utf-8' );
const updatedContent = formattedContent
.replace( /(https?:\/\/[^\s]*thetvapp[^\s]*)/g, ( fullUrl ) =>
{
Log.debug( `.m3u`, chalk.yellow( `[rewriter]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Rewriting url for keyword` ),
chalk.blueBright( `<keyword>` ), chalk.gray( `*thetvapp` ),
chalk.blueBright( `<from>` ), chalk.gray( `${ fullUrl }` ),
chalk.blueBright( `<to>` ), chalk.gray( `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }` ) );
return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`;
})
.replace( /(https?:\/\/[^\s]*tvpass[^\s]*)/g, ( fullUrl ) =>
{
Log.debug( `.m3u`, chalk.yellow( `[rewriter]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Rewriting url for keyword` ),
chalk.blueBright( `<keyword>` ), chalk.gray( `*tvpass` ),
chalk.blueBright( `<from>` ), chalk.gray( `${ fullUrl }` ),
chalk.blueBright( `<to>` ), chalk.gray( `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }` ) );
return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`;
})
.replace( /(https?:\/\/[^\s]*fl2.moveonjoy[^\s]*)/g, ( fullUrl ) =>
{
const urlRewrite = fullUrl.replace( 'fl2.moveonjoy', 'fl6.moveonjoy' );
Log.debug( `.m3u`, chalk.yellow( `[rewriter]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Rewriting url for keyword` ),
chalk.blueBright( `<keyword>` ), chalk.gray( `*fl2.moveonjoy` ),
chalk.blueBright( `<from>` ), chalk.gray( `${ fullUrl }` ),
chalk.blueBright( `<to>` ), chalk.gray( `${ urlRewrite }` ) );
return `${ urlRewrite }`;
});
res.writeHead( 200, {
'Content-Type': 'application/x-mpegURL',
'Content-Disposition': 'inline; filename="' + envFileM3U
});
Log.ok( `.m3u`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Successfully served m3u8 channel playlist data` ),
chalk.greenBright( `<cat>` ), chalk.gray( `serveM3U` ),
chalk.greenBright( `<code>` ), chalk.gray( `200` ),
chalk.greenBright( `<host>` ), chalk.gray( `${ host }` ),
chalk.greenBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.greenBright( `<path>` ), chalk.gray( `${ FILE_M3U }` ),
chalk.greenBright( `<file>` ), chalk.gray( `${ envFileM3U }` ) );
res.end( updatedContent );
}
catch ( err )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Fatal serving m3u8 channel playlist data`,
error: `${ err.message }`,
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 500,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `.m3u`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveM3U` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.redBright( `<path>` ), chalk.gray( `${ FILE_XML }` ),
chalk.redBright( `<file>` ), chalk.gray( `${ envFileXML }` ) );
res.end( JSON.stringify( statusCheck ) );
}
}
/*
serve > xml
Serves IPTV uncompressed .xml guide data
*/
async function serveXML( res, req )
{
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( FILE_XML, 'utf-8' );
res.writeHead( 200, {
'Content-Type': 'application/xml',
'Content-Disposition': 'inline; filename="' + envFileXML
});
Log.ok( `.xml`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Successfully served uncompressed xml / epg guide data` ),
chalk.greenBright( `<cat>` ), chalk.gray( `serveXML` ),
chalk.greenBright( `<code>` ), chalk.gray( `200` ),
chalk.greenBright( `<host>` ), chalk.gray( `${ host }` ),
chalk.greenBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.greenBright( `<path>` ), chalk.gray( `${ FILE_XML }` ),
chalk.greenBright( `<file>` ), chalk.gray( `${ envFileXML }` ) );
res.end( formattedContent );
}
catch ( err )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Fatal serving uncompressed xml / epg guide data`,
error: `${ err.message }`,
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 500,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'text/plain'
});
Log.error( `.xml`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveXML` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.redBright( `<path>` ), chalk.gray( `${ FILE_XML }` ),
chalk.redBright( `<file>` ), chalk.gray( `${ envFileXML }` ) );
res.end( JSON.stringify( statusCheck ) );
}
};
/*
serve > gzip
Serves IPTV compressed .gz guide data
*/
async function serveGZP( res, req )
{
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( FILE_GZP );
res.writeHead( 200, {
'Content-Type': 'application/gzip',
'Content-Disposition': 'inline; filename="' + envFileGZP
});
Log.ok( `.gzp`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Successfully served compressed gzip xml/epg guide data` ),
chalk.greenBright( `<cat>` ), chalk.gray( `serveGZP` ),
chalk.greenBright( `<code>` ), chalk.gray( `200` ),
chalk.greenBright( `<host>` ), chalk.gray( `${ host }` ),
chalk.greenBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.greenBright( `<path>` ), chalk.gray( `${ FILE_GZP }` ),
chalk.greenBright( `<file>` ), chalk.gray( `${ envFileGZP }` ) );
res.end( formattedContent );
}
catch ( err )
{
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( req ),
message: `Fatal serving compressed gzip xml/epg guide data`,
error: `${ err.message }`,
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 500,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
res.writeHead( statusCheck.code, {
'Content-Type': 'text/plain'
});
Log.error( `.gzp`, chalk.yellow( `[response]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<cat>` ), chalk.gray( `serveGZP` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
chalk.redBright( `<url>` ), chalk.gray( `${ req.url }` ),
chalk.redBright( `<path>` ), chalk.gray( `${ FILE_GZP }` ),
chalk.redBright( `<file>` ), chalk.gray( `${ envFileGZP }` ) );
res.end( JSON.stringify( statusCheck ) );
}
};
/*
cache > set
*/
function setCache( key, value, ttl, req )
{
const expiry = Date.now() + ttl;
cache.set( key, {
value,
expiry
});
Log.debug( `cache`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `New key created` ),
chalk.blueBright( `<cat>` ), chalk.gray( `setCache` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<key>` ), chalk.gray( `${ key }` ),
chalk.blueBright( `<expire>` ), chalk.gray( `${ ttl / 1000 } seconds` ) );
}
/*
cache > get
*/
function getCache( key, req )
{
const cached = cache.get( key );
if ( cached && cached.expiry > Date.now() )
{
return cached.value;
}
else
{
if ( cached )
Log.debug( `cache`, chalk.yellow( `[get]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Key has expired, marked for deletion` ),
chalk.blueBright( `<cat>` ), chalk.gray( `getCache` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( req ) }` ),
chalk.blueBright( `<key>` ), chalk.gray( `${ key }` ) );
cache.delete( key );
return null;
}
}
/*
Initialization
this is the starting method to prepare tvapp2
*/
async function initialize()
{
const start = performance.now();
try
{
const validation = crons.validateCronExpression( envTaskCronSync );
if ( !validation.valid )
{
Log.error( `core`, chalk.yellow( `[schedule]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Specified cron schedule is not valid` ),
chalk.redBright( `<schedule>` ), chalk.whiteBright.bgBlack( ` ${ envTaskCronSync } ` ) );
}
else
{
const cronNextRunDt = new Date( crons.sendAt( envTaskCronSync ) );
const cronNextRun = moment( cronNextRunDt ).format( 'MM-DD-YYYY h:mm A' );
Log.info( `core`, chalk.yellow( `[schedule]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `TVApp2 will refresh channel and guide data at` ),
chalk.blueBright( `<schedule>` ), chalk.whiteBright.gray( ` ${ envTaskCronSync } ` ),
chalk.blueBright( `<nextrun>` ), chalk.whiteBright.gray( ` ${ cronNextRun } ` ),
chalk.blueBright( `<nextrunIso>` ), chalk.whiteBright.gray( ` ${ cronNextRunDt } ` ) );
}
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Starting TVApp2 container. Assigning bound IP to host network adapter` ),
chalk.blueBright( `<hostIp>` ), chalk.gray( `${ envWebIP }` ),
chalk.blueBright( `<containerIp>` ), chalk.gray( `${ envIpContainer }` ),
chalk.blueBright( `<port>` ), chalk.gray( `${ envWebPort }` ) );
/*
Debug > network
*/
Log.debug( `.net`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `IP_CONTAINER` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envIpContainer }` ) );
Log.debug( `.net`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `IP_GATEWAY` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envIpGateway }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `RELEASE` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envAppRelease }` ) );
/*
Debug > Verbose > environment vars
*/
const env = process.env;
Object.keys( env ).forEach( ( key ) =>
{
Log.verbose( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ key }` ), chalk.blueBright( `<value>` ), chalk.gray( `${ env[key] }` ) );
});
/*
Debug > environment vars
we could just loop process.env; but that will show every container env var. We just want this app
*/
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `URL_REPO` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envUrlRepo }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `WEB_IP` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envWebIP }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `WEB_PORT` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envWebPort }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `WEB_FOLDER` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envWebFolder }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `WEB_ENCODING` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envWebEncoding }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `WEB_PROXY_HEADER` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envProxyHeader }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `STREAM_QUALITY` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envStreamQuality }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `API_KEY` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envApiKey }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `FILE_URL` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envFileURL }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `FILE_M3U` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envFileM3U }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `FILE_EPG` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envFileXML }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `FILE_GZP` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envFileGZP }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `HEALTH_TIMER` ), chalk.blueBright( `<value>` ), chalk.gray( `${ envHealthTimer }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `LOG_LEVEL` ), chalk.blueBright( `<value>` ), chalk.gray( `${ LOG_LEVEL }` ) );
Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `USERAGENT` ), chalk.blueBright( `<value>` ), chalk.gray( `${ USERAGENT }` ) );
/*
Debug > vars > external urls
*/
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `extURL` ), chalk.blueBright( `<value>` ), chalk.gray( `${ extURL }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `extXML` ), chalk.blueBright( `<value>` ), chalk.gray( `${ extXML }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `extM3U` ), chalk.blueBright( `<value>` ), chalk.gray( `${ extM3U }` ) );
/*
Debug > vars > subdomain keywords
*/
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainGZP` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainGZP.join() }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainM3U` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainM3U.join() }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainEPG` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainEPG.join() }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainKey` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainKey.join() }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainChan` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainChan.join() }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainHealth` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainHealth.join() }` ) );
Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `<name>` ), chalk.gray( `subdomainRestart` ), chalk.blueBright( `<value>` ), chalk.gray( `${ subdomainRestart.join() }` ) );
/*
get files
*/
await getFile( extURL, FILE_URL );
await getFile( extXML, FILE_XML );
await getFile( extM3U, FILE_M3U );
await getGzip();
urls = fs.readFileSync( FILE_URL, 'utf-8' ).split( '\n' ).filter( Boolean );
if ( urls.length === 0 )
throw new Error( `No valid URLs found in ${ FILE_URL }` );
/*
Calculate Sizes
*/
FILE_M3U_SIZE = getFileSizeHuman( FILE_M3U );
FILE_XML_SIZE = getFileSizeHuman( FILE_XML );
FILE_GZP_SIZE = getFileSizeHuman( FILE_GZP );
FILE_M3U_MODIFIED = getFileModified( FILE_M3U );
FILE_XML_MODIFIED = getFileModified( FILE_XML );
FILE_GZP_MODIFIED = getFileModified( FILE_GZP );
const end = performance.now();
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `TVApp2 container is ready` ),
chalk.blueBright( `<time>` ), chalk.gray( `took ${ end - start }ms` ),
chalk.blueBright( `<ip>` ), chalk.gray( `${ envIpContainer }` ),
chalk.blueBright( `<gateway>` ), chalk.gray( `${ envIpGateway }` ),
chalk.blueBright( `<port>` ), chalk.gray( `${ envWebPort }` ) );
}
catch ( err )
{
const end = performance.now();
Log.error( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Could not start up TVApp2 container due to error` ),
chalk.redBright( `<time>` ), chalk.gray( `took ${ end - start }ms` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<ip>` ), chalk.gray( `${ envIpContainer }` ),
chalk.redBright( `<gateway>` ), chalk.gray( `${ envIpGateway }` ),
chalk.redBright( `<port>` ), chalk.gray( `${ envWebPort }` ) );
}
}
/*
Webserver
@todo possibility of switching out http.createserver with express
import express from 'express';
const app = express()
app.use(express.static('www'));
const server = app.listen(8000, function () {
const host = server.address().address
const port = server.address().port
console.log('Express listening at http://%s:%s', host, port)
})
*/
const server = http.createServer( ( request, response ) =>
{
/*
If request.url === '/'; load index.html as default page
request.url returns
/
/www/css/tvapp2.fonts.min.css
/www/css/tvapp2.min.css
*/
const method = request.method || 'GET';
let reqUrl = request.url;
if ( reqUrl === '/' )
reqUrl = 'index.html';
/*
Remove leading forward slash
*/
const loadFile = reqUrl.replace( /^\/+/, '' );
const handleRequest = async() =>
{
/*
Define the different routes.
Place the template system last. Getting TVApp data should take priority.
subdomainM3U array []
loadFile channel?url=https%3A%2F%2Ftvpass.org%2Fchannel%2Fabc-wabc-new-york-ny%2F
*/
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Request started` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<request.url>` ), chalk.gray( `${ request.url }` ),
chalk.blueBright( `<reqUrl>` ), chalk.gray( `${ reqUrl }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
if ( subdomainRestart.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) )
{
/*
Not highly technical, but good enough for starting out until Express is integrated.
if restart command is triggered using website, allow it to pass through without an API key.
if restart command is triggered by using
referer = if activated from webpage via clicking icon
no referer = if activated using URL
referer is check just as an added aspect of the api key, but really this doesn't even need to be here
as the referer can be easily spoofed. remove once express and the new api system are added. right now
it does no harm for a user to even bypass this.
@todo integrate real api system after express replaces node http
*/
const apiKey = new URL( request.url, `http://${ request.headers.host }` ).searchParams.get( 'key' );
const referer = request.headers.referer || null;
if ( ( !referer && envApiKey && !apiKey ) || ( referer && !referer.includes( request.headers.host ) ) )
{
const statusCheck =
{
ip: envIpContainer, gateway: envIpGateway, client: clientIp( request ),
message: `must specify api key: http://${ request.headers.host }/api/restart?key=XXXXXXXX`,
status: `unauthorized`, ref: request.url, method: method || 'GET', code: 401,
uptime: Math.round( process.uptime() ), timestamp: Date.now()
};
response.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Unauthorized (401): restart attempt did not specify api key using ?key=XXX parameter` ),
chalk.redBright( `<type>` ), chalk.gray( `api/restart` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.redBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.redBright( `<method>` ), chalk.gray( `${ method }` ) );
response.end( JSON.stringify( statusCheck ) );
return;
}
/*
no referer, api key in url specified, api key set up with tvapp2 do not match
*/
if ( !referer && ( envApiKey !== apiKey ) )
{
const statusCheck =
{
ip: envIpContainer, gateway: envIpGateway, client: clientIp( request ),
message: `incorrect api key specified: http://${ request.headers.host }/api/restart?key=XXXXXXXX`,
status: `unauthorized`, ref: request.url, method: method || 'GET', code: 401,
uptime: Math.round( process.uptime() ), timestamp: Date.now()
};
response.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Unauthorized (401): incorrect api key specified` ),
chalk.redBright( `<type>` ), chalk.gray( `api/restart` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.redBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.redBright( `<method>` ), chalk.gray( `${ method }` ) );
response.end( JSON.stringify( statusCheck ) );
return;
}
await initialize();
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( request ),
message: 'Restart command received',
status: 'ok',
ref: request.url,
method: method || 'GET',
code: 200,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
response.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access restart api` ),
chalk.blueBright( `<type>` ), chalk.gray( `api/restart` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
response.end( JSON.stringify( statusCheck ) );
return;
}
if ( subdomainM3U.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
{
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access m3u playlist` ),
chalk.blueBright( `<type>` ), chalk.gray( `m3u playlist` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveM3U( response, request );
return;
}
if ( subdomainChan.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) )
{
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access channel` ),
chalk.blueBright( `<type>` ), chalk.gray( `channel` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveM3UPlaylist( request, response );
return;
}
if ( subdomainKey.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
{
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access key` ),
chalk.blueBright( `<type>` ), chalk.gray( `key` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveKey( request, response );
return;
}
if ( subdomainEPG.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
{
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access epg (uncompressed)` ),
chalk.blueBright( `<type>` ), chalk.gray( `epg (uncompressed)` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveXML( response, request );
return;
}
if ( subdomainGZP.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
{
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access epg gzip (compressed)` ),
chalk.blueBright( `<type>` ), chalk.gray( `epg (compressed)` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveGZP( response, request );
return;
}
if ( subdomainHealth.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' )
{
Log.info( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Requesting to access health api` ),
chalk.blueBright( `<type>` ), chalk.gray( `api/health` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveHealthCheck( request, response );
return;
}
/*
the request wasn't part of any of our pre-defined subdomains; see if the request is to load a html / css / js file
*/
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Request not captured by subdomain keyword checks; sending request to ejs` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
/*
General Template & .html / .css / .js
read the loaded asset file
*/
ejs.renderFile( `./${ envWebFolder }/${ loadFile }`,
{
fileM3U: envFileM3U,
sizeM3U: FILE_M3U_SIZE,
dateM3U: FILE_M3U_MODIFIED,
fileXML: envFileXML,
sizeXML: FILE_XML_SIZE,
dateXML: FILE_XML_MODIFIED,
fileGZP: envFileGZP,
sizeGZP: FILE_GZP_SIZE,
dateGZP: FILE_GZP_MODIFIED,
healthTimer: envHealthTimer,
appRelease: envAppRelease,
appName: name,
appVersion: version,
appUrlGithub: repository.url.substr( 0, repository.url.lastIndexOf( '.' ) ),
appUrlDiscord: discord.url,
appUrlDocs: docs.url,
appGitHashShort: envGitSHA1.substring( 0, 9 ),
appGitHashLong: envGitSHA1
}, ( err, data ) =>
{
if ( !err )
{
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Request accepted by ejs` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
/*
This allows us to serve all files locally: css, js, etc.
the file loaded is dependent on what comes to the right of the period.
*/
const fileExt = loadFile.lastIndexOf( '.' );
const fileMime = fileExt === -1
? 'text/plain'
: {
'.html' : 'text/html',
'.htm' : 'text/html',
'.ico' : 'image/x-icon',
'.jpg' : 'image/jpeg',
'.png' : 'image/png',
'.gif' : 'image/gif',
'.css' : 'text/css',
'.scss' : 'text/x-sass',
'.gz' : 'application/gzip',
'.js' : 'text/javascript',
'.txt' : 'text/plain',
'.xml' : 'application/xml',
'.m3u' : 'text/plain',
'.m3u8' : 'text/plain'
}[loadFile.substring( fileExt )];
/*
ejs is only for templates; if we want to load an binary data (like images); we must use fs.readFile
*/
if ( fileMime !== 'text/html' )
data = fs.readFileSync( `./${ envWebFolder }/${ loadFile }` );
response.setHeader( 'Content-type', fileMime );
response.end( data );
Log.ok( `http`, chalk.yellow( `[requests]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `Request to load file` ),
chalk.greenBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.greenBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.greenBright( `<mime>` ), chalk.gray( `${ fileMime }` ),
chalk.greenBright( `<method>` ), chalk.gray( `${ method }` ) );
}
else
{
Log.debug( `http`, chalk.yellow( `[requests]` ), chalk.white( `⚙️` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Request rejected by ejs` ),
chalk.blueBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.blueBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
if ( loadFile === 'discovery.json' )
{
Log.notice( `http`, chalk.yellowBright( `[notice]` ), chalk.white( `📌` ),
chalk.yellowBright( `<msg>` ), chalk.gray( `If you are attempting to load TVApp2 using an HDHomeRun tuner, please switch to the` ), chalk.yellowBright( `M3U Tuner` ) );
}
const statusCheck =
{
ip: envIpContainer,
gateway: envIpGateway,
client: clientIp( request ),
message: 'Page not found',
status: 'healthy',
ref: request.url,
method: method || 'GET',
code: 404,
uptime: Math.round( process.uptime() ),
timestamp: Date.now()
};
response.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
});
Log.error( `http`, chalk.redBright( `[requests]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `${ statusCheck.message }` ),
chalk.redBright( `<client>` ), chalk.gray( `${ clientIp( request ) }` ),
chalk.redBright( `<code>` ), chalk.gray( `${ statusCheck.code }` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
chalk.redBright( `<file>` ), chalk.gray( `${ loadFile }` ),
chalk.redBright( `<method>` ), chalk.gray( `${ method }` ) );
response.end( JSON.stringify( statusCheck ) );
}
});
};
handleRequest().catch( ( err ) =>
{
response.writeHead( 500, {
'Content-Type': 'text/plain'
});
Log.error( `http`, chalk.redBright( `[requests]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Cannot handle request` ),
chalk.redBright( `<code>` ), chalk.gray( `500` ),
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ) );
response.end( 'Internal Server Error' );
});
});
/*
Initialize Webserver
*/
( async() =>
{
/*
check if api key has been provided as env var
*/
if ( !envApiKey )
Log.warn( `/api`, chalk.yellow( `[callback]` ), chalk.white( `⚠️` ),
chalk.yellowBright( `<msg>` ), chalk.gray( `API_KEY environment variable not defined for api, leaving blank` ) );
else
Log.ok( `/api`, chalk.yellow( `[callback]` ), chalk.white( `` ),
chalk.greenBright( `<msg>` ), chalk.gray( `API_KEY environment variable successfully assigned` ) );
/*
initialize
*/
await initialize();
/*
check service status that we depend on
*/
serviceCheck( 'TVPass.org', 'https://tvpass.org' );
serviceCheck( 'TheTVApp.to', 'https://thetvapp.to' );
serviceCheck( 'MoveOnJoy.com', 'http://moveonjoy.com' );
serviceCheck( 'Daddylive.dad', 'https://daddylive.dad' );
serviceCheck( 'Newkso.ru', 'https://zekonew.newkso.ru/zeko' );
/*
start web server
*/
server.listen( envWebPort, envWebIP, () =>
{
Log.ok( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Server is now running on` ),
chalk.blueBright( `<ipPublic>` ), chalk.whiteBright.bgBlack( ` ${ envWebIP }:${ envWebPort } ` ),
chalk.blueBright( `<ipDocker>` ), chalk.whiteBright.bgBlack( ` ${ envIpContainer }:${ envWebPort } ` ) );
Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Running TVApp2 version` ),
chalk.blueBright( `<version>` ), chalk.gray( ` ${ version } ` ),
chalk.blueBright( `<release>` ), chalk.gray( ` ${ envAppRelease } ` ) );
});
})();
/*
Crons > Next Sync
*/
cron.schedule( envTaskCronSync, async() =>
{
Log.ok( `task`, chalk.yellow( `[resync]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Ran cron / task to re-sync IPTV data` ),
chalk.blueBright( `<schedule>` ), chalk.whiteBright.bgBlack( ` ${ envTaskCronSync } ` ) );
await initialize();
});
/*
Crons > Announce Next Sync
should show every 30 minutes
*/
cron.schedule( '*/30 * * * *', async() =>
{
const validation = crons.validateCronExpression( envTaskCronSync );
if ( !validation.valid )
{
Log.error( `core`, chalk.yellow( `[schedule]` ), chalk.white( `` ),
chalk.redBright( `<msg>` ), chalk.gray( `Specified cron schedule is not valid. Re-write the cron so that it is properly formatted` ),
chalk.redBright( `<env>` ), chalk.gray( `TASK_CRON_SYNC` ),
chalk.redBright( `<schedule>` ), chalk.whiteBright.bgBlack( ` ${ envTaskCronSync } ` ) );
}
else
{
const cronNextRunDt = new Date( crons.sendAt( envTaskCronSync ) );
const cronNextRun = moment( cronNextRunDt ).format( 'MM-DD-YYYY h:mm A' );
Log.info( `core`, chalk.yellow( `[schedule]` ), chalk.white( `` ),
chalk.blueBright( `<msg>` ), chalk.gray( `Next IPTV data refresh at` ),
chalk.blueBright( `<schedule>` ), chalk.whiteBright.gray( ` ${ envTaskCronSync } ` ),
chalk.blueBright( `<nextrun>` ), chalk.whiteBright.gray( ` ${ cronNextRun } ` ),
chalk.blueBright( `<nextrunIso>` ), chalk.whiteBright.gray( ` ${ cronNextRunDt } ` ) );
}
});