diff --git a/hdhomerun-src-backup/index.js b/hdhomerun-src-backup/index.js new file mode 100644 index 00000000..3a7b3cc6 --- /dev/null +++ b/hdhomerun-src-backup/index.js @@ -0,0 +1,2759 @@ +#!/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( `