diff --git a/docs/img/screenshots/01.gif b/docs/img/screenshots/01.gif deleted file mode 100644 index 51519051..00000000 Binary files a/docs/img/screenshots/01.gif and /dev/null differ diff --git a/tvapp2/index.js b/tvapp2/index.js deleted file mode 100755 index f055be99..00000000 --- a/tvapp2/index.js +++ /dev/null @@ -1,2407 +0,0 @@ -#!/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(); -*/ -const gitHash = `f6484e00dea57891cdeb3123aca124ca7388b22b`; - -/* - 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 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( `` ), 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( `` ), 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( `` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `` ), chalk.gray( `${ response.status }` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uri }` ) ); - return; - } - - /* try 1 > domain up */ - Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `` ), chalk.gray( `Service Online` ), chalk.greenBright( `` ), chalk.gray( `${ response.status }` ), chalk.greenBright( `` ), chalk.gray( `${ service }` ), chalk.greenBright( `
` ), 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( `` ), chalk.gray( `Service Offline; failed to communicate with service via SSL; trying http protocol` ), chalk.yellowBright( `` ), chalk.gray( `${ service }` ), chalk.yellowBright( `` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `` ), 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( `` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `` ), chalk.gray( `${ response.status }` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uriRetry }` ) ); - return; - } - - /* try 2 > http > domain up */ - Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `` ), chalk.gray( `Service Online` ), chalk.greenBright( `` ), chalk.gray( `${ response.status }` ), chalk.greenBright( `` ), chalk.gray( `${ service }` ), chalk.greenBright( `
` ), chalk.gray( `${ uriRetry }` ) ); - } - catch ( err ) - { - /* try 2 > http > domain not exist */ - Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Service Offline; failed to communicate with service, address does not exist` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uri }` ), chalk.redBright( `` ), 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( `` ), chalk.gray( `Service Offline; failed to communicate with service via SSL; trying https protocol` ), chalk.yellowBright( `` ), chalk.gray( `${ service }` ), chalk.yellowBright( `` ), chalk.gray( `${ uri }` ), chalk.redBright( `(failed)` ), chalk.yellowBright( `` ), 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( `` ), chalk.gray( `Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `` ), chalk.gray( `${ response.status }` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uriRetry }` ) ); - return; - } - - /* try 2 > https > domain up */ - Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `` ), chalk.gray( `Service Online` ), chalk.greenBright( `` ), chalk.gray( `${ response.status }` ), chalk.greenBright( `` ), chalk.gray( `${ service }` ), chalk.greenBright( `
` ), chalk.gray( `${ uriRetry }` ) ); - } - catch ( err ) - { - /* try 2 > https > domain not exist */ - Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Service Offline; failed to communicate with service, address does not exist` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uri }` ), chalk.redBright( `` ), 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( `` ), chalk.gray( `Preparing to download external file` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Getting response from file download request` ), - chalk.blueBright( `` ), chalk.gray( `${ res.statusCode }` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ), - chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ); - - if ( res.statusCode !== 200 ) - { - Log.error( `file`, chalk.redBright( `[download]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Attempt to download external file returned non-200 status` ), - chalk.redBright( `` ), chalk.gray( `${ res.statusCode }` ), - chalk.redBright( `` ), chalk.gray( `${ url }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Successfully downloaded external file` ), - chalk.greenBright( `` ), chalk.gray( `${ res.statusCode }` ), - chalk.greenBright( `` ), chalk.gray( `${ url }` ), - chalk.greenBright( `` ), chalk.gray( `${ filePath }` ) ); - - file.close( () => resolve( true ) ); - }); - }) - .on( 'error', ( err ) => - { - Log.error( `file`, chalk.redBright( `[download]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Failed to download external source file` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ url }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Requesting to download external file` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ), - chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ); - - await downloadFile( url, filePath ); - } - catch ( err ) - { - if ( fs.existsSync( filePath ) ) - { - Log.warn( `file`, chalk.yellow( `[requests]` ), chalk.white( `⚠️` ), - chalk.yellowBright( `` ), chalk.gray( `Download failed - Using existing local file ${ filePath }` ), - chalk.yellowBright( `` ), chalk.gray( `${ url }` ), - chalk.yellowBright( `` ), chalk.gray( `${ filePath }` ) ); - } - else - { - Log.error( `file`, chalk.redBright( `[requests]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Download filed and no local backup exists, aborting` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ url }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Preparing to create compressed XML gz file` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - - fs.readFile( FILE_XML, ( err, buf ) => - { - Log.debug( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Reading source XML file` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - - if ( err ) - { - Log.error( `.gzp`, chalk.redBright( `[generate]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Could not read source XML file` ), - chalk.redBright( `` ), chalk.gray( `${ err }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Starting zlib.gzip` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - - if ( err ) - { - Log.error( `.gzp`, chalk.redBright( `[generate]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Could not create gz archive` ), - chalk.redBright( `` ), chalk.gray( `${ err }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - - return reject( new Error( `Could not create ${ envFileGZP }. Error: ${ err }` ) ); - } - - Log.info( `.gzp`, chalk.yellow( `[generate]` ), chalk.white( `ℹ️` ), - chalk.blueBright( `` ), chalk.gray( `Started creating gz archive from XML source` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - - fs.writeFile( `${ FILE_GZP }`, buf, ( err ) => - { - if ( err ) - { - Log.error( `.gzp`, chalk.redBright( `[generate]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Could not write to and create gz archive` ), - chalk.redBright( `` ), chalk.gray( `${ err }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Successfully created compressed gz archive from XML source file` ), - chalk.greenBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `Requesting to create compressed gzip from uncompressed XML data` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.blueBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - - await createGzip( ); - } - catch ( err ) - { - if ( fs.existsSync( FILE_XML ) ) - { - Log.warn( `.gzp`, chalk.yellow( `[requests]` ), chalk.white( `⚠️` ), - chalk.yellowBright( `` ), chalk.yellowBright( `Cannot get compressed gzip; but source XML file found and can be used` ), - chalk.yellowBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.yellowBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); - } - else - { - Log.error( `.gzp`, chalk.redBright( `[requests]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Failed to get compressed gzip, and source XML file not found` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Preparing to fetch remote request` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Getting response from remote fetch request` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ) ); - - if ( resp.statusCode !== 200 ) - { - Log.error( `remo`, chalk.redBright( `[retrieve]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Remote fetch returned status code other than 200` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Remote fetch could not complete encoding type ${ encoding }` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ err }` ), - chalk.redBright( `` ), chalk.gray( `${ encoding }` ), - chalk.redBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.redBright( `` ), chalk.gray( `${ url }` ) ); - - return reject( err ); - } - - Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Remote fetch detected encoding type ${ encoding }; decoding` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ encoding }` ), - chalk.blueBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Remote fetch could not complete encoding type ${ encoding }` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ err }` ), - chalk.redBright( `` ), chalk.gray( `${ encoding }` ), - chalk.redBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.redBright( `` ), chalk.gray( `${ url }` ) ); - - return reject( err ); - } - - Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Remote fetch detected encoding type ${ encoding }; decoding` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ encoding }` ), - chalk.blueBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Remote fetch could not complete encoding type ${ encoding } (brotli decompress)` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ err }` ), - chalk.redBright( `` ), chalk.gray( `${ encoding }` ), - chalk.redBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.redBright( `` ), chalk.gray( `${ url }` ) ); - - return reject( err ); - } - - Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Remote fetch detected encoding type ${ encoding } (brotli decompress); decoding` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ encoding }` ), - chalk.blueBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ) ); - - resolve( decoded ); - }); - } - else - { - Log.debug( `remo`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Remote fetch contains no headers to decode; resolving buffer` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ encoding }` ), - chalk.blueBright( `` ), chalk.gray( `${ resp.statusCode }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveKey` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ req.url }` ), - chalk.redBright( `` ), chalk.gray( `empty; missing var` ) ); - - return res.end( JSON.stringify( statusCheck ) ); - } - - Log.debug( `keys`, chalk.yellow( `[response]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Valid paramUrl specified; establishing connection to serve key to client` ), - chalk.blueBright( `` ), chalk.gray( `serveKey` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ req.url }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Serving key to client` ), - chalk.greenBright( `` ), chalk.gray( `serveKey` ), - chalk.greenBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.greenBright( `` ), chalk.gray( `200` ), - chalk.greenBright( `` ), chalk.gray( `${ req.url }` ), - chalk.greenBright( `` ), chalk.gray( `${ paramUrl }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveKey` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.error }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Preparing to fetch remote page` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Status code returned` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ res.statusCode }` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ) ); - - if ( res.statusCode !== 200 ) - { - Log.debug( `http`, chalk.yellow( `[retrieve]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Failed to load url; status 200 was not returned` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ res.statusCode }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Setting headers` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ res.statusCode }` ), - chalk.blueBright( `` ), chalk.gray( `${ url }` ), - chalk.blueBright( `
` ), 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( `` ), chalk.gray( `Requesting to get tokenize url` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Cannot find streamNameMatch; returned empty` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.grey( `${ channelUrl }` ), - chalk.redBright( `` ), chalk.grey( `missing / var empty` ) ); - - return null; - } - - streamName = streamNameMatch[1]; - - Log.debug( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `streamName found` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.blueBright( `` ), chalk.gray( `not yet assigned` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Generating tokenized final stream URL` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ streamHost }` ), - chalk.blueBright( `` ), chalk.gray( `${ streamName }` ), - chalk.blueBright( `` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ), - chalk.blueBright( `` ), chalk.gray( `${ tokenUrl }` ), - chalk.blueBright( `` ), chalk.gray( `not yet assigned` ) ); - - try - { - const json = JSON.parse( tokenResponse ); - tokenizedUrl = json.url; - - Log.debug( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `⚙️` ), - chalk.blueBright( `` ), chalk.gray( `Returned token response in json format` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ streamHost }` ), - chalk.blueBright( `` ), chalk.gray( `${ streamName }` ), - chalk.blueBright( `` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ), - chalk.blueBright( `` ), chalk.gray( `${ tokenUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ json }` ), - chalk.blueBright( `` ), chalk.gray( `${ tokenizedUrl }` ) ); - } - catch ( err ) - { - Log.error( `play`, chalk.redBright( `[tokenize]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Failed to parse token JSON for channel` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.redBright( `` ), chalk.gray( `${ streamHost }` ), - chalk.redBright( `` ), chalk.gray( `${ streamName }` ), - chalk.redBright( `` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ), - chalk.redBright( `` ), chalk.gray( `${ tokenUrl }` ), - chalk.redBright( `` ), chalk.gray( `not yet assigned` ) ); - - return null; - } - - if ( !tokenizedUrl ) - { - Log.error( `play`, chalk.redBright( `[tokenize]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `No URL found in token JSON for channel` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.redBright( `` ), chalk.gray( `${ streamHost }` ), - chalk.redBright( `` ), chalk.gray( `${ streamName }` ), - chalk.redBright( `` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ), - chalk.redBright( `` ), chalk.gray( `${ tokenUrl }` ), - chalk.redBright( `` ), chalk.gray( `missing` ) ); - - return null; - } - - Log.ok( `play`, chalk.yellow( `[tokenize]` ), chalk.white( `✅` ), - chalk.greenBright( `` ), chalk.gray( `Successfully generated token for stream` ), - chalk.greenBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.greenBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.greenBright( `` ), chalk.gray( `${ streamHost }` ), - chalk.greenBright( `` ), chalk.gray( `${ streamName }` ), - chalk.greenBright( `` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ), - chalk.greenBright( `` ), chalk.gray( `${ streamHost }` ), - chalk.greenBright( `` ), chalk.gray( `${ tokenizedUrl }` ), - chalk.greenBright( `` ), chalk.gray( `${ tokenUrl }` ) ); - - return tokenizedUrl; - } - catch ( err ) - { - Log.error( `play`, chalk.redBright( `[tokenize]` ), chalk.white( `❌` ), - chalk.redBright( `` ), chalk.gray( `Fatal error fetching token` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ channelUrl }` ), - chalk.redBright( `` ), chalk.gray( `not defined` ), - chalk.redBright( `` ), chalk.gray( `not defined` ), - chalk.redBright( `` ), chalk.gray( `${ envStreamQuality.toLowerCase() }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Requesting to serve M3U playlist` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ req.url }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `http://${ req.headers.host }/channel?url=XXXX` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `decodedUrl ends with .ts script; (302) redirecting` ), - chalk.yellowBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.yellowBright( `` ), chalk.gray( `302` ), - chalk.yellowBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.yellowBright( `` ), chalk.gray( `${ paramUrl }` ), - chalk.yellowBright( `` ), chalk.gray( `${ decodedUrl }` ), - chalk.yellowBright( `` ), 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( `` ), chalk.gray( `Serving cachedUrl m3u playlist to client` ), - chalk.blueBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.blueBright( `` ), chalk.gray( `200` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ paramUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ decodedUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ cachedUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); - - res.end( rewrittenPlaylist ); - return; - } - - Log.info( `plst`, chalk.yellow( `[response]` ), chalk.white( `ℹ️` ), - chalk.blueBright( `` ), chalk.gray( `Request not cached; generating new tokenizedUrl for m3u playlist to client` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.blueBright( `` ), chalk.gray( `${ paramUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ decodedUrl }` ), - chalk.blueBright( `` ), chalk.gray( `${ cachedUrl }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ decodedUrl }` ), - chalk.redBright( `` ), chalk.gray( `${ tokenizedUrl }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Serving new tokenizedUrl with m3u playlist to client` ), - chalk.greenBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.greenBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.greenBright( `` ), chalk.gray( `200` ), - chalk.greenBright( `` ), chalk.gray( `${ envFileM3U }` ), - chalk.greenBright( `` ), chalk.gray( `${ paramUrl }` ), - chalk.greenBright( `` ), chalk.gray( `${ decodedUrl }` ), - chalk.greenBright( `` ), chalk.gray( `${ tokenizedUrl }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `Failed to serve m3u playlist` ), - chalk.redBright( `` ), chalk.gray( `serveM3UPlaylist` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), 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( `` ), 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( `` ), chalk.gray( `Response` ), - chalk.greenBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.greenBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.greenBright( `` ), chalk.gray( `${ statusCheck.status }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message } response` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.status }` ), - chalk.redBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Rewriting url for keyword` ), - chalk.blueBright( `` ), chalk.gray( `*thetvapp` ), - chalk.blueBright( `` ), chalk.gray( `${ fullUrl }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Rewriting url for keyword` ), - chalk.blueBright( `` ), chalk.gray( `*tvpass` ), - chalk.blueBright( `` ), chalk.gray( `${ fullUrl }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Rewriting url for keyword` ), - chalk.blueBright( `` ), chalk.gray( `*fl2.moveonjoy` ), - chalk.blueBright( `` ), chalk.gray( `${ fullUrl }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Successfully served m3u8 channel playlist data` ), - chalk.greenBright( `` ), chalk.gray( `serveM3U` ), - chalk.greenBright( `` ), chalk.gray( `200` ), - chalk.greenBright( `` ), chalk.gray( `${ host }` ), - chalk.greenBright( `` ), chalk.gray( `${ req.url }` ), - chalk.greenBright( `` ), chalk.gray( `${ FILE_M3U }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveM3U` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ req.url }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Successfully served uncompressed xml / epg guide data` ), - chalk.greenBright( `` ), chalk.gray( `serveXML` ), - chalk.greenBright( `` ), chalk.gray( `200` ), - chalk.greenBright( `` ), chalk.gray( `${ host }` ), - chalk.greenBright( `` ), chalk.gray( `${ req.url }` ), - chalk.greenBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveXML` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ req.url }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_XML }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `Successfully served compressed gzip xml/epg guide data` ), - chalk.greenBright( `` ), chalk.gray( `serveGZP` ), - chalk.greenBright( `` ), chalk.gray( `200` ), - chalk.greenBright( `` ), chalk.gray( `${ host }` ), - chalk.greenBright( `` ), chalk.gray( `${ req.url }` ), - chalk.greenBright( `` ), chalk.gray( `${ FILE_GZP }` ), - chalk.greenBright( `` ), 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( `` ), chalk.gray( `${ statusCheck.message }` ), - chalk.redBright( `` ), chalk.gray( `serveGZP` ), - chalk.redBright( `` ), chalk.gray( `${ statusCheck.code }` ), - chalk.redBright( `` ), chalk.gray( `${ err.message }` ), - chalk.redBright( `` ), chalk.gray( `${ req.url }` ), - chalk.redBright( `` ), chalk.gray( `${ FILE_GZP }` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `New key created` ), - chalk.blueBright( `` ), chalk.gray( `setCache` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), chalk.gray( `${ key }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Key has expired, marked for deletion` ), - chalk.blueBright( `` ), chalk.gray( `getCache` ), - chalk.blueBright( `` ), chalk.gray( `${ clientIp( req ) }` ), - chalk.blueBright( `` ), 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( `` ), chalk.gray( `Specified cron schedule is not valid` ), - chalk.redBright( `` ), 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( `` ), chalk.gray( `TVApp2 will refresh channel and guide data at` ), - chalk.blueBright( `` ), chalk.whiteBright.gray( ` ${ envTaskCronSync } ` ), - chalk.blueBright( `` ), chalk.whiteBright.gray( ` ${ cronNextRun } ` ), - chalk.blueBright( `` ), chalk.whiteBright.gray( ` ${ cronNextRunDt } ` ) ); - } - - Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `ℹ️` ), - chalk.blueBright( `` ), chalk.gray( `Starting TVApp2 container. Assigning bound IP to host network adapter` ), - chalk.blueBright( `` ), chalk.gray( `${ envWebIP }` ), - chalk.blueBright( `` ), chalk.gray( `${ envIpContainer }` ), - chalk.blueBright( `` ), chalk.gray( `${ envWebPort }` ) ); - - /* - Debug > network - */ - - Log.debug( `.net`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `IP_CONTAINER` ), chalk.blueBright( `` ), chalk.gray( `${ envIpContainer }` ) ); - Log.debug( `.net`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `IP_GATEWAY` ), chalk.blueBright( `` ), chalk.gray( `${ envIpGateway }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `RELEASE` ), chalk.blueBright( `` ), 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( `` ), chalk.gray( `${ key }` ), chalk.blueBright( `` ), 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( `` ), chalk.gray( `URL_REPO` ), chalk.blueBright( `` ), chalk.gray( `${ envUrlRepo }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `WEB_IP` ), chalk.blueBright( `` ), chalk.gray( `${ envWebIP }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `WEB_PORT` ), chalk.blueBright( `` ), chalk.gray( `${ envWebPort }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `WEB_FOLDER` ), chalk.blueBright( `` ), chalk.gray( `${ envWebFolder }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `WEB_ENCODING` ), chalk.blueBright( `` ), chalk.gray( `${ envWebEncoding }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `WEB_PROXY_HEADER` ), chalk.blueBright( `` ), chalk.gray( `${ envProxyHeader }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `STREAM_QUALITY` ), chalk.blueBright( `` ), chalk.gray( `${ envStreamQuality }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `API_KEY` ), chalk.blueBright( `` ), chalk.gray( `${ envApiKey }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `FILE_URL` ), chalk.blueBright( `` ), chalk.gray( `${ envFileURL }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `FILE_M3U` ), chalk.blueBright( `` ), chalk.gray( `${ envFileM3U }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `FILE_EPG` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `FILE_GZP` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `HEALTH_TIMER` ), chalk.blueBright( `` ), chalk.gray( `${ envHealthTimer }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `LOG_LEVEL` ), chalk.blueBright( `` ), chalk.gray( `${ LOG_LEVEL }` ) ); - Log.debug( `.env`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `USERAGENT` ), chalk.blueBright( `` ), chalk.gray( `${ USERAGENT }` ) ); - - /* - Debug > vars > external urls - */ - - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `extURL` ), chalk.blueBright( `` ), chalk.gray( `${ extURL }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `extXML` ), chalk.blueBright( `` ), chalk.gray( `${ extXML }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `extM3U` ), chalk.blueBright( `` ), chalk.gray( `${ extM3U }` ) ); - - /* - Debug > vars > subdomain keywords - */ - - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainGZP` ), chalk.blueBright( `` ), chalk.gray( `${ subdomainGZP.join() }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainM3U` ), chalk.blueBright( `` ), chalk.gray( `${ subdomainM3U.join() }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainEPG` ), chalk.blueBright( `` ), chalk.gray( `${ subdomainEPG.join() }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainKey` ), chalk.blueBright( `` ), chalk.gray( `${ subdomainKey.join() }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainChan` ), chalk.blueBright( `` ), chalk.gray( `${ subdomainChan.join() }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainHealth` ), chalk.blueBright( `` ), chalk.gray( `${ subdomainHealth.join() }` ) ); - Log.debug( `.var`, chalk.yellow( `[assigner]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `subdomainRestart` ), chalk.blueBright( `` ), 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( `` ), chalk.gray( `TVApp2 container is ready` ), - chalk.blueBright( `