diff --git a/Dockerfile b/Dockerfile index 69f549c4..a4be35a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,7 @@ FROM --platform=linux/${ARCH} ghcr.io/aetherinox/alpine-base:3.21 ARG ARCH=amd64 ARG BUILDDATE ARG VERSION +ARG RELEASE # # # Set Labels @@ -56,6 +57,7 @@ LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.architecture="${ARCH}" LABEL org.opencontainers.image.ref.name="main" LABEL org.opencontainers.image.registry="local" +LABEL org.opencontainers.image.release="${RELEASE}" LABEL org.tvapp2.image.maintainers="Aetherinox, iFlip721, Optx" LABEL org.tvapp2.image.build-version="Version:- ${VERSION} Date:- ${BUILDDATE}" @@ -65,6 +67,7 @@ LABEL org.tvapp2.image.build-version="Version:- ${VERSION} Date:- ${BUILDDATE}" ENV NODE_VERSION=22.8.0 ENV YARN_VERSION=1.22.22 +ENV RELEASE="${RELEASE}" ENV DIR_BUILD=/usr/src/app ENV DIR_RUN=/usr/bin/app ENV URL_REPO="https://git.binaryninja.net/binaryninja/" diff --git a/tvapp2/index.js b/tvapp2/index.js index d9f8181a..79cdf4e8 100755 --- a/tvapp2/index.js +++ b/tvapp2/index.js @@ -74,6 +74,9 @@ 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 envWebEncoding = process.env.WEB_ENCODING || 'deflate, br'; const envHealthTimer = process.env.HEALTH_TIMER || 600000; const LOG_LEVEL = process.env.LOG_LEVEL || 10; @@ -104,6 +107,7 @@ const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 http://127.0.0.1:4124/playlist http://127.0.0.1:4124/key http://127.0.0.1:4124/channel + http://127.0.0.1:4124/health */ const subdomainRestart = [ 'restart', 'sync', 'resync' ]; @@ -111,7 +115,7 @@ const subdomainGZP = [ 'gzip', 'gz' ]; const subdomainM3U = [ 'playlist', 'm3u', 'm3u8' ]; const subdomainEPG = [ 'guide', 'epg', 'xml' ]; const subdomainKey = [ 'key', 'keys' ]; -const subdomainChan = [ 'channels', 'channel' ]; +const subdomainChan = [ 'channels', 'channel', 'chan' ]; const subdomainHealth = [ 'api/status', 'api/health' ]; /* @@ -212,7 +216,7 @@ class Log if ( process.pkg ) { - Log.info( `Processing Package` ); + Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Starting server utilizing process.execPath` ) ); const basePath = path.dirname( process.execPath ); FILE_URL = path.join( basePath, FOLDER_WWW, `${ envFileURL }` ); @@ -223,7 +227,7 @@ if ( process.pkg ) } else { - Log.info( `Processing Locals` ); + Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Starting server utilizing processed locals` ) ); FILE_URL = path.resolve( __dirname, FOLDER_WWW, `${ envFileURL }` ); FILE_M3U = path.resolve( __dirname, FOLDER_WWW, `${ envFileM3U }` ); @@ -284,7 +288,7 @@ const semaphore = new Semaphore( 5 ); async function downloadFile( url, filePath ) { - Log.info( `Fetching`, chalk.white( `→` ), chalk.grey( `Downloading external file` ), chalk.blueBright( `${ url }` ), chalk.grey( `to` ), chalk.blueBright( `${ filePath }` ) ); + Log.info( `netw`, chalk.yellow( `[start]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Downloading external file` ), chalk.blueBright( `` ), chalk.gray( `${ url }` ), chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ); return new Promise( ( resolve, reject ) => { @@ -296,19 +300,19 @@ async function downloadFile( url, filePath ) { if ( response.statusCode !== 200 ) { - Log.error( `Failed to download file: ${ url }`, chalk.white( `→` ), chalk.grey( `Status code: ${ response.statusCode }` ) ); + Log.error( `netw`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Failed to download source file` ), chalk.blueBright( `` ), chalk.gray( `${ url }` ), chalk.blueBright( `` ), chalk.gray( `${ filePath }` ), chalk.blueBright( `` ), chalk.gray( `${ response.statusCode }` ) ); return reject( new Error( `Failed to download file: ${ url }. Status code: ${ response.statusCode }` ) ); } response.pipe( file ); file.on( 'finish', () => { - Log.ok( `Received`, chalk.white( `→` ), chalk.grey( `Successfully wrote data to file` ), chalk.blueBright( `${ filePath }` ) ); + Log.ok( `netw`, chalk.yellow( `[finish]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Successfully downloaded and wrote new file` ), chalk.blueBright( `` ), chalk.gray( `${ url }` ), chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ); file.close( () => resolve( true ) ); }); }) .on( 'error', ( err ) => { - Log.error( `Error downloading file: ${ url }`, chalk.white( `→` ), chalk.grey( `Status code: ${ err.message }` ) ); + Log.error( `netw`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Failed to download source file` ), chalk.blueBright( `` ), chalk.gray( `${ err.message }`, chalk.blueBright( `` ), chalk.gray( `${ url }` ), chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ) ); fs.unlink( filePath, () => reject( err ) ); }); }); @@ -373,7 +377,7 @@ function getFileSizeHuman( filename, si = true, decimal = 1 ) } /* - Func > Ensure File Exists + 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. @@ -395,59 +399,56 @@ async function getFile( url, filePath ) { if ( fs.existsSync( filePath ) ) { - Log.warn( `Using existing local file ${ filePath }, download failed`, chalk.white( `→` ), chalk.grey( `${ url }` ) ); + Log.warn( `netw`, chalk.yellow( `[get]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Download failed - Using existing local file ${ filePath }` ), chalk.blueBright( `` ), chalk.gray( `${ url }` ), chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ); } else { - Log.error( `Failed to download file, and no local file exists; aborting`, chalk.white( `→` ), chalk.grey( `${ url }` ) ); + Log.error( `netw`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Download filed and no local backup exists, aborting` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.gray( `${ url }` ), chalk.blueBright( `` ), chalk.gray( `${ filePath }` ) ); throw err; } } } /* - Func > Package GZip + Func > Create GZip locates the xmltv.xml and packages it into a xmltv.gz archive */ async function createGzip( ) { - Log.debug( `Preparing to gzip`, chalk.white( `→` ), chalk.grey( `${ envFileXML }` ) ); - + Log.info( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Preparing to create compressed XML gz file` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); return new Promise( ( resolve, reject ) => { - Log.debug( `createGzip[promise]`, chalk.white( `→` ), chalk.grey( `${ envFileXML }` ) ); - + Log.debug( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Promise to create compressed gz started` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); fs.readFile( FILE_XML, ( err, buf ) => { - Log.debug( `createGzip[fs.readFile]`, chalk.white( `→` ), chalk.grey( `${ envFileXML }` ) ); - + Log.debug( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Reading source XML file` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); if ( err ) { - Log.error( `Could not read file ${ envFileXML }. Error: `, chalk.white( `→` ), chalk.grey( `${ err }` ) ); + Log.error( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Could not read source XML file` ), chalk.blueBright( `` ), chalk.redBright( `${ err }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); return reject( new Error( `Could not read file ${ envFileXML }. Error: ${ err }` ) ); } zlib.gzip( buf, ( err, buf ) => { - Log.debug( `createGzip[zlib.gzip]`, chalk.white( `→` ), chalk.grey( `${ envFileXML }` ), chalk.white( `→` ), chalk.grey( `${ envFileGZP }` ) ); + Log.debug( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Starting zlib.gzip` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); if ( err ) { - Log.error( `Could not write to archive. Error: `, chalk.white( `→` ), chalk.grey( `${ err }` ) ); + Log.error( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Could not create gz archive` ), chalk.blueBright( `` ), chalk.redBright( `${ err }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); return reject( new Error( `Could not create ${ envFileGZP }. Error: ${ err }` ) ); } - Log.info( `Compressing`, chalk.white( `→` ), `${ envFileXML }`, chalk.white( `→` ), `${ FILE_GZP }` ); + Log.info( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Started creating gz archive from XML source` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); fs.writeFile( `${ FILE_GZP }`, buf, ( err ) => { if ( err ) { - Log.error( `Could not write XML file to archive. Error: `, chalk.white( `→` ), chalk.grey( `${ err }` ) ); + Log.error( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Could not write to and create gz archive` ), chalk.blueBright( `` ), chalk.redBright( `${ err }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); return reject( new Error( `Could not write XML file ${ envFileXML } to ${ envFileGZP }. Error: ${ err }` ) ); } - Log.ok( `Compressed`, chalk.white( `→` ), `${ envFileXML }`, chalk.white( `→` ), `${ FILE_GZP }` ); + Log.ok( `gzip`, chalk.yellow( `[create]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Successfully created compressed gz archive from XML source file` ), chalk.blueBright( `` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `` ), chalk.gray( `${ envFileGZP }` ) ); resolve( true ); }); }); @@ -456,19 +457,12 @@ async function createGzip( ) } /* - Func > Ensure File Exists + Func > Get Gzip - 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 + try; catch to create a .gz compressed file from the .xml guide data */ -async function prepareGzip( ) +async function getGzip( ) { try { @@ -478,11 +472,11 @@ async function prepareGzip( ) { if ( fs.existsSync( FILE_XML ) ) { - Log.warn( `XML file found, but gzip failed to compress XML`, chalk.white( `→` ), chalk.grey( `${ FILE_XML }` ) ); + Log.warn( `gzip`, chalk.yellow( `[compress]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.yellowBright( `Source xml file found, but gzip failed generate a compressed .gz fileL` ), chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ) ); } else { - Log.error( `XML file not found`, chalk.white( `→` ), chalk.grey( `${ FILE_XML }` ) ); + Log.error( `gzip`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Source XML file not found; cannot create compressed gzip` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ) ); throw err; } } @@ -523,7 +517,7 @@ async function fetchRemote( url ) { if ( resp.statusCode !== 200 ) { - Log.error( `Server returned status code other than 200`, chalk.white( `→` ), chalk.grey( `${ url } - ${ resp.statusCode }` ) ); + Log.error( `core`, chalk.yellow( `[fetch]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Server returned status code other than 200` ), chalk.blueBright( `` ), chalk.redBright( `${ resp.statusCode }` ), chalk.blueBright( `` ), chalk.gray( `${ url }` ) ); return reject( new Error( `HTTP ${ resp.statusCode } for ${ url }` ) ); } @@ -576,22 +570,25 @@ async function serveKey( req, res ) const uriParam = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'uri' ); if ( !uriParam ) { - Log.error( `Missing "uri" parameter for key download`, chalk.white( `→` ), chalk.grey( `${ req.url }` ) ); - const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), message: 'Error: Missing "uri" parameter for key download.', + status: 'unhealthy', + ref: req.url, + method: req.method || 'GET', code: 400, timestamp: Date.now() }; - res.writeHead( 400, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); + Log.error( `key`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.gray( `${ req.url }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + return res.end( JSON.stringify( statusCheck ) ); } @@ -604,22 +601,26 @@ async function serveKey( req, res ) } catch ( err ) { - Log.error( `ServeKey Error:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); - const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), - message: 'Error fetching key', + message: `Failed to serve key`, + error: `${ err.message }`, + status: 'unhealthy', + ref: req.url, + method: req.method || 'GET', code: 500, timestamp: Date.now() }; - res.writeHead( 500, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); + Log.error( `key`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.error }` ), chalk.blueBright( `` ), chalk.gray( `${ req.url }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + res.end( JSON.stringify( statusCheck ) ); } } @@ -706,7 +707,7 @@ async function getTokenizedUrl( channelUrl ) const streamNameMatch = html.match( /id="stream_name" name="([^"]+)"/ ); if ( !streamNameMatch ) { - Log.error( `Cannot find "stream_name"`, chalk.white( `→` ), chalk.grey( `${ channelUrl }` ) ); + Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Cannot find "stream_name` ), chalk.blueBright( `` ), chalk.grey( `${ channelUrl }` ) ); return null; } streamName = streamNameMatch[1]; @@ -726,6 +727,8 @@ async function getTokenizedUrl( channelUrl ) const tokenResponse = await fetchPage( tokenUrl ); let finalUrl; + Log.debug( `playlist`, chalk.yellow( `[tokenize]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Generating tokenized final stream URL` ), chalk.blueBright( `` ), chalk.gray( `${ streamName }` ), chalk.blueBright( `` ), chalk.gray( `${ envStreamQuality }` ), chalk.blueBright( `` ), chalk.gray( `${ streamHost }` ) ); + try { const json = JSON.parse( tokenResponse ); @@ -733,23 +736,22 @@ async function getTokenizedUrl( channelUrl ) } catch ( err ) { - Log.error( `Failed to parse token JSON for channel`, chalk.white( `→` ), chalk.grey( `${ channelUrl } - ${ err.message }` ) ); + Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Failed to parse token JSON for channel` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.gray( `${ channelUrl }` ) ); return null; } if ( !finalUrl ) { - Log.error( `No URL found in token JSON for channel`, chalk.white( `→` ), chalk.grey( `${ channelUrl }` ) ); + Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `No URL found in token JSON for channel` ), chalk.blueBright( `` ), chalk.gray( `${ channelUrl }` ) ); return null; } - Log.debug( `Tokenized URL:`, chalk.white( `→` ), chalk.grey( `${ finalUrl }` ) ); - + Log.debug( `playlist`, chalk.yellow( `[tokenize]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `Completed generated tokenized final stream URL` ), chalk.blueBright( `` ), chalk.gray( `${ streamName }` ), chalk.blueBright( `` ), chalk.gray( `${ envStreamQuality }` ), chalk.blueBright( `` ), chalk.gray( `${ streamHost }` ), chalk.blueBright( `` ), chalk.gray( `${ finalUrl }` ) ); return finalUrl; } catch ( err ) { - Log.error( `Fatal error fetching token:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); + Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Fatal error fetching token` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.grey( `${ channelUrl }` ) ); return null; } } @@ -762,27 +764,29 @@ async function serveM3UPlaylist( req, res ) const urlParam = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'url' ); if ( !urlParam ) { - Log.error( `Missing parameter`, chalk.white( `→` ), chalk.grey( `URL` ) ); - const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), - message: 'Missing URL parameter', + message: `Missing ?url= parameter`, + status: `unhealthy`, + ref: req.url, + method: req.method || 'GET', code: 404, timestamp: Date.now() }; - res.writeHead( 400, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); + Log.error( `channel`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.grey( `http://${ req.headers.host }/channel?url=XXXX` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + res.end( JSON.stringify( statusCheck ) ); return; } - const decodedUrl = decodeURIComponent( urlParam ); if ( decodedUrl.endsWith( '.ts' ) ) { @@ -803,31 +807,36 @@ async function serveM3UPlaylist( req, res ) 'Content-Disposition': 'inline; filename="' + envFileM3U }); + Log.debug( `playlist`, chalk.yellow( `[fetch]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `retrieving cached playlist` ), chalk.blueBright( `` ), chalk.gray( `${ cachedUrl }` ), chalk.blueBright( `` ), chalk.gray( `${ urlParam }` ), chalk.blueBright( `` ), chalk.gray( `200` ) ); + res.end( rewrittenPlaylist ); return; } - Log.info( `Fetching stream:`, chalk.white( `→` ), chalk.grey( `${ urlParam }` ) ); + Log.info( `playlist`, chalk.yellow( `[fetch]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `${ urlParam }` ) ); const finalUrl = await getTokenizedUrl( decodedUrl ); if ( !finalUrl ) { - Log.error( `Failed to retrieve tokenized URL` ); - const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), - message: 'Error: Failed to retrieve tokenized URL.', + message: `Failed to retrieve tokenized URL.`, + status: `unhealthy`, + ref: req.url, + method: req.method || 'GET', code: 500, timestamp: Date.now() }; - res.writeHead( 500, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); + Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.gray( `${ urlParam }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + res.end( JSON.stringify( statusCheck ) ); return; @@ -846,8 +855,6 @@ async function serveM3UPlaylist( req, res ) } catch ( err ) { - Log.error( `Error processing request:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); - if ( !res.headersSent ) { const statusCheck = @@ -855,15 +862,21 @@ async function serveM3UPlaylist( req, res ) ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), - message: 'Error: Cannot process request.', + message: `Cannot process request when fetching channel playlist`, + error: `${ err.message }`, + status: 'unhealthy', + ref: req.url, + method: req.method || 'GET', code: 500, timestamp: Date.now() }; - res.writeHead( 500, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); + Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + res.end( JSON.stringify( statusCheck ) ); } } @@ -878,50 +891,59 @@ async function serveHealthCheck( req, res ) await semaphore.acquire(); try { - const urlParam = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'url' ); + const urlParam = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'api' ); if ( !urlParam ) { - Log.debug( `No parameters passed to healthcheck`, chalk.white( `→` ), chalk.grey( `URL` ) ); + Log.debug( `health`, chalk.yellow( `[api]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `No API key passed to health check` ) ); } - const healthcheck = + const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), - message: 'Healthy', + message: `healthy`, + status: `healthy`, + ref: req.url, + method: req.method || 'GET', code: 200, timestamp: Date.now() }; - res.writeHead( 200, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); - res.end( JSON.stringify( healthcheck ) ); + Log.ok( `health`, chalk.yellow( `[api]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `health check returned` ), chalk.greenBright( `${ statusCheck.status }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ), chalk.blueBright( `` ), chalk.gray( `${ process.uptime() }` ) ); + + res.end( JSON.stringify( statusCheck ) ); return; } catch ( err ) { - Log.error( `Error getting healthcheck:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); - if ( !res.headersSent ) { - const healthcheck = + const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), - message: 'Unhealthy', + message: `health check failed`, + error: `${ err.message }`, + status: `unhealthy`, + ref: req.url, + method: req.method || 'GET', code: 503, timestamp: Date.now() }; - res.writeHead( 503, { + res.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); - res.end( JSON.stringify( healthcheck ) ); + Log.error( `health`, chalk.yellow( `[error]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.message }; returned` ), chalk.redBright( `${ statusCheck.status }` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ), chalk.blueBright( `` ), chalk.gray( `${ process.uptime() }` ) ); + + res.end( JSON.stringify( statusCheck ) ); } } finally @@ -963,7 +985,7 @@ async function rewriteM3U( originalUrl, req ) Serves IPTV .m3u playlist */ -async function serveM3U( response, req ) +async function serveM3U( res, req ) { try { @@ -981,22 +1003,36 @@ async function serveM3U( response, req ) return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`; }); - response.writeHead( 200, { - 'Content-Type': 'application/x-mpegURL', - 'Content-Disposition': 'inline; filename="' + envFileM3U - }); + res.writeHead( 200, { + 'Content-Type': 'application/x-mpegURL', + 'Content-Disposition': 'inline; filename="' + envFileM3U + }); - response.end( updatedContent ); + res.end( updatedContent ); } catch ( err ) { - Log.error( `Error in serveM3U:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); + const statusCheck = + { + ip: envIpContainer, + gateway: envIpGateway, + uptime: process.uptime(), + message: `Fatal error serving playlist`, + error: `${ err.message }`, + status: 'unhealthy', + ref: req.url, + method: req.method || 'GET', + code: 500, + timestamp: Date.now() + }; - response.writeHead( 500, { - 'Content-Type': 'text/plain' + Log.error( `playlist`, chalk.yellow( `[serve]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `` ), chalk.gray( `${ req.url }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + + res.writeHead( statusCheck.code, { + 'Content-Type': 'application/json' }); - response.end( `Error serving playlist: ${ err.message }` ); + res.end( JSON.stringify( statusCheck ) ); } } @@ -1022,13 +1058,13 @@ async function serveXML( response, req ) } catch ( err ) { - Log.error( `Error in serveM3U:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); + Log.error( `playlist`, chalk.yellow( `[serve]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Fatal serving xml / epg guide data` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.gray( `${ req.url }` ) ); response.writeHead( 500, { 'Content-Type': 'text/plain' }); - response.end( `Error serving playlist: ${ err.message }` ); + response.end( `Error serving xml/epg guide data: ${ err.message }` ); } }; @@ -1054,7 +1090,7 @@ async function serveGZP( response, req ) } catch ( err ) { - Log.error( `Error in serveGZP:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); + Log.error( `playlist`, chalk.yellow( `[serve]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Fatal serving compressed gzip file` ), chalk.blueBright( `` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `` ), chalk.gray( `${ req.url }` ) ); response.writeHead( 500, { 'Content-Type': 'text/plain' @@ -1072,7 +1108,7 @@ function setCache( key, value, ttl ) expiry }); - Log.debug( `Cache set for key ${ key } which expires in`, chalk.white( `→` ), chalk.grey( `${ ttl / 1000 } seconds` ) ); + Log.debug( `cache`, chalk.yellow( `[set]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `new key created` ), chalk.blueBright( `` ), chalk.gray( `${ key }` ), chalk.blueBright( `` ), chalk.gray( `${ ttl / 1000 } seconds` ) ); } function getCache( key ) @@ -1085,7 +1121,7 @@ function getCache( key ) else { if ( cached ) - Log.debug( `Cache expired for key`, chalk.white( `→` ), chalk.grey( `${ key }` ) ); + Log.debug( `cache`, chalk.yellow( `[get]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `key has expired, marked for deletion` ), chalk.blueBright( `` ), chalk.gray( `${ key }` ) ); cache.delete( key ); return null; @@ -1102,12 +1138,18 @@ async function initialize() { try { - Log.info( `Initialization Started` ); + const start = performance.now(); + Log.info( `core`, chalk.yellow( `[init]` ), 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 }` ) ); + + Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `FILE_URL` ), chalk.blueBright( `` ), chalk.gray( `${ FILE_URL }` ) ); + Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `FILE_M3U` ), chalk.blueBright( `` ), chalk.gray( `${ FILE_M3U }` ) ); + Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `FILE_XML` ), chalk.blueBright( `` ), chalk.gray( `${ FILE_XML }` ) ); + Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `FILE_GZP` ), chalk.blueBright( `` ), chalk.gray( `${ FILE_GZP }` ) ); await getFile( extURL, FILE_URL ); await getFile( extXML, FILE_XML ); await getFile( extM3U, FILE_M3U ); - await prepareGzip(); + await getGzip(); urls = fs.readFileSync( FILE_URL, 'utf-8' ).split( '\n' ).filter( Boolean ); if ( urls.length === 0 ) @@ -1125,11 +1167,12 @@ async function initialize() FILE_XML_MODIFIED = getFileModified( FILE_XML ); FILE_GZP_MODIFIED = getFileModified( FILE_GZP ); - Log.ok( `Initialization Complete` ); + const end = performance.now(); + Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `TVApp2 container is ready` ), chalk.blueBright( `took ${ end - start }ms` ), chalk.blueBright( `` ), chalk.gray( `TVApp2 container is ready; took ${ end - start }ms` ), chalk.blueBright( `` ), chalk.gray( `${ envIpContainer }` ), chalk.blueBright( `` ), chalk.gray( `${ envIpGateway }` ), chalk.blueBright( `` ), chalk.gray( `${ envWebPort }` ) ); } catch ( err ) { - Log.error( `Initialization error:`, chalk.white( `→` ), chalk.grey( `${ err.message }` ) ); + Log.error( `core`, chalk.yellow( `[init]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.redBright( `Could not start up TVApp2 container due to error` ), chalk.blueBright( `` ), chalk.redBright( `${ err }` ), chalk.blueBright( `` ), chalk.gray( `${ envIpContainer }` ), chalk.blueBright( `` ), chalk.gray( `${ envIpGateway }` ), chalk.blueBright( `` ), chalk.gray( `${ envWebPort }` ) ); } } @@ -1171,8 +1214,6 @@ const server = http.createServer( ( request, response ) => const loadFile = reqUrl.replace( /^\/+/, '' ); - Log.debug( `www`, chalk.blueBright( `[REQUEST]` ), chalk.white( `→` ), chalk.grey( `asset>` ), chalk.greenBright( `${ loadFile }` ), chalk.grey( `` ), chalk.greenBright( `${ method }` ) ); - const handleRequest = async() => { /* @@ -1185,27 +1226,35 @@ const server = http.createServer( ( request, response ) => if ( subdomainRestart.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) ) { - Log.info( `Toggled restart`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); - - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_URL` ), chalk.blueBright( `${ FILE_URL }` ) ); - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_M3U` ), chalk.blueBright( `${ FILE_M3U }` ) ); - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_XML` ), chalk.blueBright( `${ FILE_XML }` ) ); - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_GZP` ), chalk.blueBright( `${ FILE_GZP }` ) ); - await initialize(); - response.writeHead( 200, { + const statusCheck = + { + ip: envIpContainer, + gateway: envIpGateway, + uptime: process.uptime(), + message: 'Restart command received', + status: 'ok', + ref: request.url, + method: method || 'GET', + code: 200, + timestamp: Date.now() + }; + + response.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); - response.end( `{ "status": "ok" }` ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `api/restart` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); + + response.end( JSON.stringify( statusCheck ) ); return; } if ( subdomainM3U.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) { - Log.info( `Received request for m3u playlist data`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `m3u playlist` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); await serveM3U( response, request ); return; @@ -1213,7 +1262,7 @@ const server = http.createServer( ( request, response ) => if ( subdomainChan.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) { - Log.info( `Received request for channel data`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `channel` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); await serveM3UPlaylist( request, response ); return; @@ -1221,7 +1270,7 @@ const server = http.createServer( ( request, response ) => if ( subdomainKey.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) { - Log.info( `Received request for key data`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `key` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); await serveKey( request, response ); return; @@ -1229,7 +1278,7 @@ const server = http.createServer( ( request, response ) => if ( subdomainEPG.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) { - Log.info( `Received request for raw uncompressed EPG data`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `epg-uncompressed` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); await serveXML( response, request ); return; @@ -1237,7 +1286,7 @@ const server = http.createServer( ( request, response ) => if ( subdomainGZP.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) { - Log.info( `Received request for compressed EPG data`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `epg-compressed` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); await serveGZP( response, request ); return; @@ -1245,7 +1294,7 @@ const server = http.createServer( ( request, response ) => if ( subdomainHealth.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) { - Log.info( `Received healthcheck`, chalk.white( `→` ), chalk.grey( `${ loadFile }` ) ); + Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `api` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ method }` ) ); await serveHealthCheck( request, response ); return; @@ -1297,6 +1346,7 @@ const server = http.createServer( ( request, response ) => '.png' : 'image/png', '.gif' : 'image/gif', '.css' : 'text/css', + '.scss' : 'text/x-sass', '.gz' : 'application/gzip', '.js' : 'text/javascript', '.txt' : 'text/plain', @@ -1315,45 +1365,46 @@ const server = http.createServer( ( request, response ) => response.setHeader( 'Content-type', fileMime ); response.end( data ); - Log.ok( `www`, chalk.greenBright( ` [LOAD] ` ), chalk.white( `→` ), chalk.grey( `` ), chalk.greenBright( `${ loadFile }` ), chalk.grey( `` ), chalk.greenBright( `${ fileMime }` ) ); + Log.ok( `www`, chalk.yellow( `[load]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ fileMime }` ) ); } else { if ( loadFile === 'discovery.json' ) { - Log.notice( `www`, chalk.yellowBright( ` [NOTICE] ` ), chalk.white( `→` ), chalk.grey( `If you are attempting to load TVApp2 using an HDHomeRun tuner, please switch to the` ), chalk.yellowBright( `M3U Tuner` ) ); + Log.notice( `www`, chalk.yellowBright( `[notice]` ), chalk.white( `→` ), chalk.grey( `If you are attempting to load TVApp2 using an HDHomeRun tuner, please switch to the` ), chalk.yellowBright( `M3U Tuner` ) ); } - Log.error( `www`, chalk.redBright( ` [ERROR] ` ), chalk.white( `→` ), chalk.grey( `File not found:` ), chalk.redBright( `${ loadFile }` ) ); - const statusCheck = { ip: envIpContainer, gateway: envIpGateway, uptime: process.uptime(), message: 'Page not found', + status: 'healthy', ref: request.url, - method: method, + method: method || 'GET', code: 404, timestamp: Date.now() }; - response.writeHead( 404, { + response.writeHead( statusCheck.code, { 'Content-Type': 'application/json' }); + Log.error( `www`, chalk.redBright( `[error]` ), chalk.white( `→` ), chalk.grey( `${ statusCheck.message }` ), chalk.redBright( `${ loadFile }` ), chalk.blueBright( `` ), chalk.gray( `${ statusCheck.code }` ) ); + response.end( JSON.stringify( statusCheck ) ); } }); }; handleRequest().catch( ( err ) => { - Log.error( `Error handling request:`, chalk.white( `→` ), chalk.grey( `${ err }` ) ); - response.writeHead( 500, { 'Content-Type': 'text/plain' }); + Log.error( `Error handling request:`, chalk.white( `→` ), chalk.grey( `${ err }` ) ); + response.end( 'Internal Server Error' ); }); }); @@ -1364,17 +1415,15 @@ const server = http.createServer( ( request, response ) => ( async() => { - const envWebIP = process.env.WEB_IP || '0.0.0.0'; - const envWebPort = process.env.WEB_PORT || `4124`; - - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_URL` ), chalk.blueBright( `${ FILE_URL }` ) ); - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_M3U` ), chalk.blueBright( `${ FILE_M3U }` ) ); - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_XML` ), chalk.blueBright( `${ FILE_XML }` ) ); - Log.debug( `env`, chalk.blueBright( `[SET]` ), chalk.white( `→` ), chalk.grey( `FILE_GZP` ), chalk.blueBright( `${ FILE_GZP }` ) ); + if ( !envApiKey ) + { + Log.warn( `core`, chalk.yellow( `[api]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `API_KEY environment variable not defined for api, leaving blank` ) ); + } await initialize(); + server.listen( envWebPort, envWebIP, () => { - Log.info( `Server now running on`, chalk.white( `→` ), chalk.whiteBright.bgBlack( ` ${ envWebIP }:${ envWebPort } ` ) ); + Log.warn( `core`, chalk.yellow( `[init]` ), chalk.white( `→` ), chalk.blueBright( `` ), chalk.gray( `server is now running on` ), chalk.whiteBright.bgBlack( ` ${ envWebIP }:${ envWebPort } ` ) ); }); })(); diff --git a/tvapp2/node_modules/.package-lock.json b/tvapp2/node_modules/.package-lock.json index e896024e..2cee98c5 100755 --- a/tvapp2/node_modules/.package-lock.json +++ b/tvapp2/node_modules/.package-lock.json @@ -1,6 +1,6 @@ { "name": "tvapp2", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": {