#!/usr/bin/env node /* Import Packages */ import fs from 'fs'; import path from 'path'; import http from 'http'; import https from 'https'; import os from 'node:os'; import osName from 'os-name'; import getos from 'getos'; import zlib from 'zlib'; import chalk from 'chalk'; import ejs from 'ejs'; import moment from 'moment'; import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en'; import nconf from 'nconf'; import crypto from 'node:crypto'; import Log from './classes/Log.js'; import Storage from './classes/Storage.js'; import Utils from './classes/Utils.js'; import CLib from './classes/CLib.js'; import Semaphore from './classes/Semaphore.js'; import Tuner from './classes/Tuner.js'; import cron, { schedule } from 'node-cron'; import * as child from 'child_process'; import * as crons from 'cron'; /* Old CJS variables converted to ESM */ import { fileURLToPath } from 'url'; const cache = new Map(); const clib = new CLib(); const encoded = clib.encodeToHexBase64( 'hello' ); const decoded = clib.decodeFromHexBase64( `${ encoded }` ); /* 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; /* */ TimeAgo.addDefaultLocale( en ); const timeAgo = new TimeAgo( ); /* Define > General @note if you change `envWebFolder`; ensure you re-name the folder where the website assets are stored. */ let FILE_CFG; 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; /* Server */ let serverOs = 'Unknown'; let serverStartup = 0; /* 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`; /* Hosts */ const hosts = [ { name: 'TVPass.org', url: 'https://tvpass.org' }, { name: 'TheTVApp.to', url: 'https://thetvapp.to' }, { name: 'MoveOnJoy.com', url: 'http://moveonjoy.com' }, { name: 'Daddylive.dad', url: 'https://daddylivestream.com' }, { name: 'git.binaryninja.com', url: envUrlRepo } ]; /* Get Server OS attempts to get the OS of a server a few different ways; and not just show "Linux". Windows machines will show Windows 11 Linux machines will show Linux Alpine (3.22.0) */ getos( ( e, json ) => { if ( e ) return osName( os.platform(), os.release() ); if ( json.os === 'win32' ) serverOs = osName( os.platform(), os.release() ); if ( json.os === 'linux' ) { if ( json.dist ) serverOs = json.dist; if ( json.release ) serverOs = serverOs.concat( ' ', '(' + json.release + ')' ); } return serverOs; }); /* 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_CFG = path.join( basePath, envWebFolder, `config.json` ); 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_CFG = path.resolve( __dirname, envWebFolder, `config.json` ); 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 }` ); } /* helper > sleep */ function sleep( ms ) { return new Promise( ( resolve ) => { setTimeout( resolve, ms ); }); } /* Semaphore > Initialize @arg int threads_max */ const semaphore = new Semaphore( 5 ); /* 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 ); /* 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 hostCheck( service, uri ) { /* try 1 */ try { const resp = await fetch( uri ); /* try 1 > domain down */ if ( resp.status !== 200 ) { Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Try: Service Offline; failed to communicate with service, possibly down` ), chalk.redBright( `` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uri }` ) ); return false; } /* try 1 > domain up */ Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `` ), chalk.gray( `Domain Online` ), chalk.greenBright( `` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `` ), chalk.gray( `${ service }` ), chalk.greenBright( `
` ), chalk.gray( `${ uri }` ) ); return true; } catch ( err ) { /* try 2 > https */ if ( /^https:\/\//i.test( uri ) ) { const uriRetry = uri.replace( /^https:\/\//ig, 'http://' ); Log.info( `ping`, chalk.yellow( `[response]` ), chalk.white( `⚠️` ), chalk.yellowBright( `` ), chalk.gray( `Try: Failed via HTTPS; 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 resp = await fetch( uriRetry ); /* try 2 > https > domain down */ if ( resp.status !== 200 ) { Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Try: Domain Offline; failed to communicate with domain, possibly down` ), chalk.redBright( `` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uriRetry }` ) ); return false; } /* try 2 > https > domain up */ Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `` ), chalk.gray( `Domain Online` ), chalk.greenBright( `` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `` ), chalk.gray( `${ service }` ), chalk.greenBright( `
` ), chalk.gray( `${ uriRetry }` ) ); return true; } catch ( err ) { /* try 2 > https > domain not exist */ Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Try: Domain Offline; failed to communicate with domain, address does not exist` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uri }` ), chalk.redBright( `` ), chalk.gray( `${ err }` ) ); return false; } } /* try 2 > http */ 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( `Try: Failed via HTTP; 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 resp = await fetch( uriRetry ); /* try 2 > http > domain down */ if ( resp.status !== 200 ) { Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Domain Offline; failed to communicate with domain, possibly down` ), chalk.redBright( `` ), chalk.gray( `${ resp.status }` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uriRetry }` ) ); return false; } /* try 2 > http > domain up */ Log.ok( `ping`, chalk.yellow( `[response]` ), chalk.white( `✅` ), chalk.greenBright( `` ), chalk.gray( `Domain Online` ), chalk.greenBright( `` ), chalk.gray( `${ resp.status }` ), chalk.greenBright( `` ), chalk.gray( `${ service }` ), chalk.greenBright( `
` ), chalk.gray( `${ uriRetry }` ) ); return true; } catch ( err ) { /* try 2 > http > domain not exist */ Log.error( `ping`, chalk.redBright( `[response]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Domain Offline; failed to communicate with domain, address does not exist` ), chalk.redBright( `` ), chalk.gray( `${ service }` ), chalk.redBright( `
` ), chalk.gray( `${ uri }` ), chalk.redBright( `` ), chalk.gray( `${ err }` ) ); return false; } } } } /* 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 }` ) ); const ok = await hostCheck( 'git.binaryninja.com', `${ envUrlRepo }` ); if ( ok ) { try { await downloadFile( url, filePath ); return true; } catch ( err ) { Log.error( `file`, chalk.redBright( `[download]` ), chalk.white( `❌` ), chalk.redBright( `` ), chalk.gray( `Download attempt failed after service check succeeded` ), chalk.redBright( `` ), chalk.gray( `${ err.message }` ), chalk.redBright( `` ), chalk.gray( `${ url }` ), chalk.redBright( `` ), chalk.gray( `${ filePath }` ) ); return false; } } else { Log.info( `file`, chalk.yellow( `[download]` ), chalk.white( `ℹ️` ), chalk.yellowBright( `` ), chalk.gray( `Skipping download because service is offline; using existing local file` ), chalk.yellowBright( `` ), chalk.gray( `${ url }` ), chalk.yellowBright( `` ), chalk.gray( `${ filePath }` ) ); return false; } } 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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( `live`, 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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( req, decodedUrl ); 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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( req, decodedUrl, tokenizedUrl, 4 * 60 * 60 * 1000 ); 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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' ); const paramSilent = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'silent' ); if ( !paramUrl ) { if ( Utils.str2bool( paramSilent ) !== true ) { 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), timestamp: Date.now() }; res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); if ( Utils.str2bool( paramSilent ) !== true ) { 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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?:\/\/fl\d+\.moveonjoy\.com[^\s]*)/g, ( fullUrl ) => { const urlRewrite = fullUrl.replace( /fl\d+\.moveonjoy\.com/, 'fl25.moveonjoy.com' ); Log.debug( `.m3u`, chalk.yellow( `[rewriter]` ), chalk.white( `⚙️` ), chalk.blueBright( `` ), chalk.gray( `Rewriting url for keyword` ), chalk.blueBright( `` ), chalk.gray( `*fl1.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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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() ), uptimeShort: timeAgo.format( Date.now() - process.uptime() * 1000, 'twitter' ), uptimeLong: timeAgo.format( Date.now() - process.uptime() * 1000, 'round' ), 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( req, key, value, ttl ) { 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( req, key ) { 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(); serverStartup = `${ end - start }`; Log.info( `core`, chalk.yellow( `[initiate]` ), chalk.white( `ℹ️` ), chalk.blueBright( `` ), chalk.gray( `TVApp2 container is ready` ), chalk.blueBright( `