Compare commits

...

14 Commits
1.3.0 ... 1.4.0

10 changed files with 605 additions and 236 deletions

View File

@@ -452,6 +452,7 @@ jobs:
sbom: false sbom: false
build-args: |- build-args: |-
ARCH=amd64 ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }} BUILDDATE=${{ env.NOW_DOCKER_LABEL }}
@@ -503,6 +504,7 @@ jobs:
sbom: false sbom: false
build-args: |- build-args: |-
ARCH=arm64 ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }} BUILDDATE=${{ env.NOW_DOCKER_LABEL }}

View File

@@ -576,6 +576,7 @@ jobs:
sbom: false sbom: false
build-args: |- build-args: |-
ARCH=amd64 ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }} BUILDDATE=${{ env.NOW_DOCKER_LABEL }}
@@ -627,6 +628,7 @@ jobs:
sbom: false sbom: false
build-args: |- build-args: |-
ARCH=arm64 ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }} BUILDDATE=${{ env.NOW_DOCKER_LABEL }}

View File

@@ -455,6 +455,7 @@ jobs:
sbom: false sbom: false
build-args: |- build-args: |-
ARCH=amd64 ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }} BUILDDATE=${{ env.NOW_DOCKER_LABEL }}
@@ -511,6 +512,7 @@ jobs:
sbom: false sbom: false
build-args: |- build-args: |-
ARCH=arm64 ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }} VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }} BUILDDATE=${{ env.NOW_DOCKER_LABEL }}

View File

@@ -37,6 +37,7 @@ FROM --platform=linux/${ARCH} ghcr.io/aetherinox/alpine-base:3.21
ARG ARCH=amd64 ARG ARCH=amd64
ARG BUILDDATE ARG BUILDDATE
ARG VERSION ARG VERSION
ARG RELEASE
# # # #
# Set Labels # Set Labels
@@ -56,6 +57,7 @@ LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.architecture="${ARCH}" LABEL org.opencontainers.image.architecture="${ARCH}"
LABEL org.opencontainers.image.ref.name="main" LABEL org.opencontainers.image.ref.name="main"
LABEL org.opencontainers.image.registry="local" LABEL org.opencontainers.image.registry="local"
LABEL org.opencontainers.image.release="${RELEASE}"
LABEL org.tvapp2.image.maintainers="Aetherinox, iFlip721, Optx" LABEL org.tvapp2.image.maintainers="Aetherinox, iFlip721, Optx"
LABEL org.tvapp2.image.build-version="Version:- ${VERSION} Date:- ${BUILDDATE}" 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 NODE_VERSION=22.8.0
ENV YARN_VERSION=1.22.22 ENV YARN_VERSION=1.22.22
ENV RELEASE="${RELEASE}"
ENV DIR_BUILD=/usr/src/app ENV DIR_BUILD=/usr/src/app
ENV DIR_RUN=/usr/bin/app ENV DIR_RUN=/usr/bin/app
ENV URL_REPO="https://git.binaryninja.net/binaryninja/" ENV URL_REPO="https://git.binaryninja.net/binaryninja/"
@@ -76,6 +79,7 @@ ENV FILE_URL="urls.txt"
ENV FILE_M3U="playlist.m3u8" ENV FILE_M3U="playlist.m3u8"
ENV FILE_EPG="xmltv.xml" ENV FILE_EPG="xmltv.xml"
ENV FILE_TAR="xmltv.xml.gz" ENV FILE_TAR="xmltv.xml.gz"
ENV HEALTH_TIMER=600000
ENV LOG_LEVEL=4 ENV LOG_LEVEL=4
ENV TZ="Etc/UTC" ENV TZ="Etc/UTC"

View File

@@ -67,13 +67,18 @@ const FOLDER_WWW = 'www';
Define > Environment Variables || Defaults Define > Environment Variables || Defaults
*/ */
const envAppRelease = process.env.RELEASE || 'stable';
const envUrlRepo = process.env.URL_REPO || 'https://git.binaryninja.net/binaryninja'; const envUrlRepo = process.env.URL_REPO || 'https://git.binaryninja.net/binaryninja';
const envStreamQuality = process.env.STREAM_QUALITY || 'hd'; const envStreamQuality = process.env.STREAM_QUALITY || 'hd';
const envFileURL = process.env.FILE_URL || 'urls.txt'; const envFileURL = process.env.FILE_URL || 'urls.txt';
const envFileM3U = process.env.FILE_M3U || 'playlist.m3u8'; const envFileM3U = process.env.FILE_M3U || 'playlist.m3u8';
const envFileXML = process.env.FILE_EPG || 'xmltv.xml'; const envFileXML = process.env.FILE_EPG || 'xmltv.xml';
const envFileGZP = process.env.FILE_GZP || 'xmltv.xml.gz'; 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 envWebEncoding = process.env.WEB_ENCODING || 'deflate, br';
const envHealthTimer = process.env.HEALTH_TIMER || 600000;
const LOG_LEVEL = process.env.LOG_LEVEL || 10; const LOG_LEVEL = process.env.LOG_LEVEL || 10;
/* /*
@@ -102,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/playlist
http://127.0.0.1:4124/key http://127.0.0.1:4124/key
http://127.0.0.1:4124/channel http://127.0.0.1:4124/channel
http://127.0.0.1:4124/health
*/ */
const subdomainRestart = [ 'restart', 'sync', 'resync' ]; const subdomainRestart = [ 'restart', 'sync', 'resync' ];
@@ -109,7 +115,7 @@ const subdomainGZP = [ 'gzip', 'gz' ];
const subdomainM3U = [ 'playlist', 'm3u', 'm3u8' ]; const subdomainM3U = [ 'playlist', 'm3u', 'm3u8' ];
const subdomainEPG = [ 'guide', 'epg', 'xml' ]; const subdomainEPG = [ 'guide', 'epg', 'xml' ];
const subdomainKey = [ 'key', 'keys' ]; const subdomainKey = [ 'key', 'keys' ];
const subdomainChan = [ 'channels', 'channel' ]; const subdomainChan = [ 'channels', 'channel', 'chan' ];
const subdomainHealth = [ 'api/status', 'api/health' ]; const subdomainHealth = [ 'api/status', 'api/health' ];
/* /*
@@ -210,7 +216,7 @@ class Log
if ( process.pkg ) if ( process.pkg )
{ {
Log.info( `Processing Package` ); Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Starting server utilizing process.execPath` ) );
const basePath = path.dirname( process.execPath ); const basePath = path.dirname( process.execPath );
FILE_URL = path.join( basePath, FOLDER_WWW, `${ envFileURL }` ); FILE_URL = path.join( basePath, FOLDER_WWW, `${ envFileURL }` );
@@ -221,7 +227,7 @@ if ( process.pkg )
} }
else else
{ {
Log.info( `Processing Locals` ); Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Starting server utilizing processed locals` ) );
FILE_URL = path.resolve( __dirname, FOLDER_WWW, `${ envFileURL }` ); FILE_URL = path.resolve( __dirname, FOLDER_WWW, `${ envFileURL }` );
FILE_M3U = path.resolve( __dirname, FOLDER_WWW, `${ envFileM3U }` ); FILE_M3U = path.resolve( __dirname, FOLDER_WWW, `${ envFileM3U }` );
@@ -282,7 +288,7 @@ const semaphore = new Semaphore( 5 );
async function downloadFile( url, filePath ) 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( `<message>` ), chalk.gray( `Downloading external file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ) );
return new Promise( ( resolve, reject ) => return new Promise( ( resolve, reject ) =>
{ {
@@ -294,19 +300,19 @@ async function downloadFile( url, filePath )
{ {
if ( response.statusCode !== 200 ) 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( `<message>` ), chalk.gray( `Failed to download source file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ response.statusCode }` ) );
return reject( new Error( `Failed to download file: ${ url }. Status code: ${ response.statusCode }` ) ); return reject( new Error( `Failed to download file: ${ url }. Status code: ${ response.statusCode }` ) );
} }
response.pipe( file ); response.pipe( file );
file.on( 'finish', () => 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( `<message>` ), chalk.gray( `Successfully downloaded and wrote new file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ) );
file.close( () => resolve( true ) ); file.close( () => resolve( true ) );
}); });
}) })
.on( 'error', ( err ) => .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( `<message>` ), chalk.gray( `Failed to download source file` ), chalk.blueBright( `<error>` ), chalk.gray( `${ err.message }`, chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ) ) );
fs.unlink( filePath, () => reject( err ) ); fs.unlink( filePath, () => reject( err ) );
}); });
}); });
@@ -371,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 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. throw error to user that file does not exist via the URL.
@@ -393,59 +399,56 @@ async function getFile( url, filePath )
{ {
if ( fs.existsSync( 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( `<message>` ), chalk.gray( `Download failed - Using existing local file ${ filePath }` ), chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ) );
} }
else 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( `<message>` ), chalk.gray( `Download filed and no local backup exists, aborting` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ) );
throw err; throw err;
} }
} }
} }
/* /*
Func > Package GZip Func > Create GZip
locates the xmltv.xml and packages it into a xmltv.gz archive locates the xmltv.xml and packages it into a xmltv.gz archive
*/ */
async function createGzip( ) async function createGzip( )
{ {
Log.debug( `Preparing to gzip`, chalk.white( `` ), chalk.grey( `${ envFileXML }` ) ); Log.info( `gzip`, chalk.yellow( `[create]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Preparing to create compressed XML gz file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
return new Promise( ( resolve, reject ) => return new Promise( ( resolve, reject ) =>
{ {
Log.debug( `createGzip[promise]`, chalk.white( `` ), chalk.grey( `${ envFileXML }` ) ); Log.debug( `gzip`, chalk.yellow( `[create]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Promise to create compressed gz started` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
fs.readFile( FILE_XML, ( err, buf ) => 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( `<message>` ), chalk.gray( `Reading source XML file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
if ( err ) 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( `<message>` ), chalk.gray( `Could not read source XML file` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err }` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
return reject( new Error( `Could not read file ${ envFileXML }. Error: ${ err }` ) ); return reject( new Error( `Could not read file ${ envFileXML }. Error: ${ err }` ) );
} }
zlib.gzip( buf, ( err, buf ) => 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( `<message>` ), chalk.gray( `Starting zlib.gzip` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
if ( err ) 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( `<message>` ), chalk.gray( `Could not create gz archive` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err }` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
return reject( new Error( `Could not create ${ envFileGZP }. Error: ${ err }` ) ); 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( `<message>` ), chalk.gray( `Started creating gz archive from XML source` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
fs.writeFile( `${ FILE_GZP }`, buf, ( err ) => fs.writeFile( `${ FILE_GZP }`, buf, ( err ) =>
{ {
if ( 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( `<message>` ), chalk.gray( `Could not write to and create gz archive` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err }` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
return reject( new Error( `Could not write XML file ${ envFileXML } to ${ envFileGZP }. Error: ${ err }` ) ); 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( `<message>` ), chalk.gray( `Successfully created compressed gz archive from XML source file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ envFileXML }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ envFileGZP }` ) );
resolve( true ); resolve( true );
}); });
}); });
@@ -454,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 try; catch to create a .gz compressed file from the .xml guide data
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 prepareGzip( ) async function getGzip( )
{ {
try try
{ {
@@ -476,11 +472,11 @@ async function prepareGzip( )
{ {
if ( fs.existsSync( FILE_XML ) ) 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( `<message>` ), chalk.yellowBright( `Source xml file found, but gzip failed generate a compressed .gz fileL` ), chalk.blueBright( `<source>` ), chalk.gray( `${ FILE_XML }` ) );
} }
else else
{ {
Log.error( `XML file not found`, chalk.white( `` ), chalk.grey( `${ FILE_XML }` ) ); Log.error( `gzip`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `Source XML file not found; cannot create compressed gzip` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<source>` ), chalk.gray( `${ FILE_XML }` ) );
throw err; throw err;
} }
} }
@@ -521,7 +517,7 @@ async function fetchRemote( url )
{ {
if ( resp.statusCode !== 200 ) 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( `<message>` ), chalk.redBright( `Server returned status code other than 200` ), chalk.blueBright( `<statusCode>` ), chalk.redBright( `${ resp.statusCode }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ url }` ) );
return reject( new Error( `HTTP ${ resp.statusCode } for ${ url }` ) ); return reject( new Error( `HTTP ${ resp.statusCode } for ${ url }` ) );
} }
@@ -574,22 +570,25 @@ async function serveKey( req, res )
const uriParam = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'uri' ); const uriParam = new URL( req.url, `http://${ req.headers.host }` ).searchParams.get( 'uri' );
if ( !uriParam ) if ( !uriParam )
{ {
Log.error( `Missing "uri" parameter for key download`, chalk.white( `` ), chalk.grey( `${ req.url }` ) );
const statusCheck = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), uptime: process.uptime(),
message: 'Error: Missing "uri" parameter for key download.', message: 'Error: Missing "uri" parameter for key download.',
status: 'unhealthy',
ref: req.url,
method: req.method || 'GET',
code: 400, code: 400,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 400, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
Log.error( `key`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
return res.end( JSON.stringify( statusCheck ) ); return res.end( JSON.stringify( statusCheck ) );
} }
@@ -602,22 +601,26 @@ async function serveKey( req, res )
} }
catch ( err ) catch ( err )
{ {
Log.error( `ServeKey Error:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) );
const statusCheck = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), 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, code: 500,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 500, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
Log.error( `key`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ statusCheck.error }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) ); res.end( JSON.stringify( statusCheck ) );
} }
} }
@@ -704,7 +707,7 @@ async function getTokenizedUrl( channelUrl )
const streamNameMatch = html.match( /id="stream_name" name="([^"]+)"/ ); const streamNameMatch = html.match( /id="stream_name" name="([^"]+)"/ );
if ( !streamNameMatch ) if ( !streamNameMatch )
{ {
Log.error( `Cannot find "stream_name"`, chalk.white( `` ), chalk.grey( `${ channelUrl }` ) ); Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `Cannot find "stream_name` ), chalk.blueBright( `<url>` ), chalk.grey( `${ channelUrl }` ) );
return null; return null;
} }
streamName = streamNameMatch[1]; streamName = streamNameMatch[1];
@@ -724,6 +727,8 @@ async function getTokenizedUrl( channelUrl )
const tokenResponse = await fetchPage( tokenUrl ); const tokenResponse = await fetchPage( tokenUrl );
let finalUrl; let finalUrl;
Log.debug( `playlist`, chalk.yellow( `[tokenize]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Generating tokenized final stream URL` ), chalk.blueBright( `<streamName>` ), chalk.gray( `${ streamName }` ), chalk.blueBright( `<quality>` ), chalk.gray( `${ envStreamQuality }` ), chalk.blueBright( `<host>` ), chalk.gray( `${ streamHost }` ) );
try try
{ {
const json = JSON.parse( tokenResponse ); const json = JSON.parse( tokenResponse );
@@ -731,23 +736,22 @@ async function getTokenizedUrl( channelUrl )
} }
catch ( err ) 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( `<message>` ), chalk.redBright( `Failed to parse token JSON for channel` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ channelUrl }` ) );
return null; return null;
} }
if ( !finalUrl ) 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( `<message>` ), chalk.redBright( `No URL found in token JSON for channel` ), chalk.blueBright( `<url>` ), chalk.gray( `${ channelUrl }` ) );
return null; return null;
} }
Log.debug( `Tokenized URL:`, chalk.white( `` ), chalk.grey( `${ finalUrl }` ) ); Log.debug( `playlist`, chalk.yellow( `[tokenize]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Completed generated tokenized final stream URL` ), chalk.blueBright( `<streamName>` ), chalk.gray( `${ streamName }` ), chalk.blueBright( `<quality>` ), chalk.gray( `${ envStreamQuality }` ), chalk.blueBright( `<host>` ), chalk.gray( `${ streamHost }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ finalUrl }` ) );
return finalUrl; return finalUrl;
} }
catch ( err ) catch ( err )
{ {
Log.error( `Fatal error fetching token:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) ); Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `Fatal error fetching token` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<url>` ), chalk.grey( `${ channelUrl }` ) );
return null; return null;
} }
} }
@@ -760,27 +764,29 @@ async function serveM3UPlaylist( req, res )
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( 'url' );
if ( !urlParam ) if ( !urlParam )
{ {
Log.error( `Missing parameter`, chalk.white( `` ), chalk.grey( `URL` ) );
const statusCheck = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), uptime: process.uptime(),
message: 'Missing URL parameter', message: `Missing ?url= parameter`,
status: `unhealthy`,
ref: req.url,
method: req.method || 'GET',
code: 404, code: 404,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 400, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
Log.error( `channel`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<expected>` ), chalk.grey( `http://${ req.headers.host }/channel?url=XXXX` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) ); res.end( JSON.stringify( statusCheck ) );
return; return;
} }
const decodedUrl = decodeURIComponent( urlParam ); const decodedUrl = decodeURIComponent( urlParam );
if ( decodedUrl.endsWith( '.ts' ) ) if ( decodedUrl.endsWith( '.ts' ) )
{ {
@@ -801,31 +807,36 @@ async function serveM3UPlaylist( req, res )
'Content-Disposition': 'inline; filename="' + envFileM3U 'Content-Disposition': 'inline; filename="' + envFileM3U
}); });
Log.debug( `playlist`, chalk.yellow( `[fetch]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `retrieving cached playlist` ), chalk.blueBright( `<cachedUrl>` ), chalk.gray( `${ cachedUrl }` ), chalk.blueBright( `<stream>` ), chalk.gray( `${ urlParam }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `200` ) );
res.end( rewrittenPlaylist ); res.end( rewrittenPlaylist );
return; return;
} }
Log.info( `Fetching stream:`, chalk.white( `` ), chalk.grey( `${ urlParam }` ) ); Log.info( `playlist`, chalk.yellow( `[fetch]` ), chalk.white( `` ), chalk.blueBright( `<stream>` ), chalk.gray( `${ urlParam }` ) );
const finalUrl = await getTokenizedUrl( decodedUrl ); const finalUrl = await getTokenizedUrl( decodedUrl );
if ( !finalUrl ) if ( !finalUrl )
{ {
Log.error( `Failed to retrieve tokenized URL` );
const statusCheck = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), 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, code: 500,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 500, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<stream>` ), chalk.gray( `${ urlParam }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) ); res.end( JSON.stringify( statusCheck ) );
return; return;
@@ -844,8 +855,6 @@ async function serveM3UPlaylist( req, res )
} }
catch ( err ) catch ( err )
{ {
Log.error( `Error processing request:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) );
if ( !res.headersSent ) if ( !res.headersSent )
{ {
const statusCheck = const statusCheck =
@@ -853,15 +862,21 @@ async function serveM3UPlaylist( req, res )
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), 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, code: 500,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 500, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
Log.error( `playlist`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) ); res.end( JSON.stringify( statusCheck ) );
} }
} }
@@ -876,50 +891,59 @@ async function serveHealthCheck( req, res )
await semaphore.acquire(); await semaphore.acquire();
try 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 ) if ( !urlParam )
{ {
Log.debug( `No parameters passed to healthcheck`, chalk.white( `` ), chalk.grey( `URL` ) ); Log.debug( `health`, chalk.yellow( `[api]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `No API key passed to health check` ) );
} }
const healthcheck = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), uptime: process.uptime(),
message: 'Healthy', message: `healthy`,
status: `healthy`,
ref: req.url,
method: req.method || 'GET',
code: 200, code: 200,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 200, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
res.end( JSON.stringify( healthcheck ) ); Log.ok( `health`, chalk.yellow( `[api]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `health check returned` ), chalk.greenBright( `${ statusCheck.status }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ), chalk.blueBright( `<uptime>` ), chalk.gray( `${ process.uptime() }` ) );
res.end( JSON.stringify( statusCheck ) );
return; return;
} }
catch ( err ) catch ( err )
{ {
Log.error( `Error getting healthcheck:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) );
if ( !res.headersSent ) if ( !res.headersSent )
{ {
const healthcheck = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), uptime: process.uptime(),
message: 'Unhealthy', message: `health check failed`,
error: `${ err.message }`,
status: `unhealthy`,
ref: req.url,
method: req.method || 'GET',
code: 503, code: 503,
timestamp: Date.now() timestamp: Date.now()
}; };
res.writeHead( 503, { res.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
res.end( JSON.stringify( healthcheck ) ); Log.error( `health`, chalk.yellow( `[error]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `${ statusCheck.message }; returned` ), chalk.redBright( `${ statusCheck.status }` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ), chalk.blueBright( `<uptime>` ), chalk.gray( `${ process.uptime() }` ) );
res.end( JSON.stringify( statusCheck ) );
} }
} }
finally finally
@@ -961,7 +985,7 @@ async function rewriteM3U( originalUrl, req )
Serves IPTV .m3u playlist Serves IPTV .m3u playlist
*/ */
async function serveM3U( response, req ) async function serveM3U( res, req )
{ {
try try
{ {
@@ -979,22 +1003,36 @@ async function serveM3U( response, req )
return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`; return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`;
}); });
response.writeHead( 200, { res.writeHead( 200, {
'Content-Type': 'application/x-mpegURL', 'Content-Type': 'application/x-mpegURL',
'Content-Disposition': 'inline; filename="' + envFileM3U 'Content-Disposition': 'inline; filename="' + envFileM3U
}); });
response.end( updatedContent ); res.end( updatedContent );
} }
catch ( err ) 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, { Log.error( `playlist`, chalk.yellow( `[serve]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
'Content-Type': 'text/plain'
res.writeHead( statusCheck.code, {
'Content-Type': 'application/json'
}); });
response.end( `Error serving playlist: ${ err.message }` ); res.end( JSON.stringify( statusCheck ) );
} }
} }
@@ -1020,13 +1058,13 @@ async function serveXML( response, req )
} }
catch ( err ) catch ( err )
{ {
Log.error( `Error in serveM3U:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) ); Log.error( `playlist`, chalk.yellow( `[serve]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `Fatal serving xml / epg guide data` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ) );
response.writeHead( 500, { response.writeHead( 500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
response.end( `Error serving playlist: ${ err.message }` ); response.end( `Error serving xml/epg guide data: ${ err.message }` );
} }
}; };
@@ -1052,7 +1090,7 @@ async function serveGZP( response, req )
} }
catch ( err ) catch ( err )
{ {
Log.error( `Error in serveGZP:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) ); Log.error( `playlist`, chalk.yellow( `[serve]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `Fatal serving compressed gzip file` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err.message }` ), chalk.blueBright( `<url>` ), chalk.gray( `${ req.url }` ) );
response.writeHead( 500, { response.writeHead( 500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
@@ -1070,7 +1108,7 @@ function setCache( key, value, ttl )
expiry 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( `<message>` ), chalk.gray( `new key created` ), chalk.blueBright( `<key>` ), chalk.gray( `${ key }` ), chalk.blueBright( `<expire>` ), chalk.gray( `${ ttl / 1000 } seconds` ) );
} }
function getCache( key ) function getCache( key )
@@ -1083,7 +1121,7 @@ function getCache( key )
else else
{ {
if ( cached ) if ( cached )
Log.debug( `Cache expired for key`, chalk.white( `` ), chalk.grey( `${ key }` ) ); Log.debug( `cache`, chalk.yellow( `[get]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `key has expired, marked for deletion` ), chalk.blueBright( `<key>` ), chalk.gray( `${ key }` ) );
cache.delete( key ); cache.delete( key );
return null; return null;
@@ -1100,12 +1138,18 @@ async function initialize()
{ {
try try
{ {
Log.info( `Initialization Started` ); const start = performance.now();
Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `Starting TVApp2 container. Assigning bound IP to host network adapter` ), chalk.blueBright( `<hostIp>` ), chalk.gray( `${ envWebIP }` ), chalk.blueBright( `<containerIp>` ), chalk.gray( `${ envIpContainer }` ), chalk.blueBright( `<port>` ), chalk.gray( `${ envWebPort }` ) );
Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `` ), chalk.blueBright( `<variable>` ), chalk.gray( `FILE_URL` ), chalk.blueBright( `<value>` ), chalk.gray( `${ FILE_URL }` ) );
Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `` ), chalk.blueBright( `<variable>` ), chalk.gray( `FILE_M3U` ), chalk.blueBright( `<value>` ), chalk.gray( `${ FILE_M3U }` ) );
Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `` ), chalk.blueBright( `<variable>` ), chalk.gray( `FILE_XML` ), chalk.blueBright( `<value>` ), chalk.gray( `${ FILE_XML }` ) );
Log.debug( `.env`, chalk.yellow( `[set]` ), chalk.white( `` ), chalk.blueBright( `<variable>` ), chalk.gray( `FILE_GZP` ), chalk.blueBright( `<value>` ), chalk.gray( `${ FILE_GZP }` ) );
await getFile( extURL, FILE_URL ); await getFile( extURL, FILE_URL );
await getFile( extXML, FILE_XML ); await getFile( extXML, FILE_XML );
await getFile( extM3U, FILE_M3U ); await getFile( extM3U, FILE_M3U );
await prepareGzip(); await getGzip();
urls = fs.readFileSync( FILE_URL, 'utf-8' ).split( '\n' ).filter( Boolean ); urls = fs.readFileSync( FILE_URL, 'utf-8' ).split( '\n' ).filter( Boolean );
if ( urls.length === 0 ) if ( urls.length === 0 )
@@ -1123,11 +1167,12 @@ async function initialize()
FILE_XML_MODIFIED = getFileModified( FILE_XML ); FILE_XML_MODIFIED = getFileModified( FILE_XML );
FILE_GZP_MODIFIED = getFileModified( FILE_GZP ); FILE_GZP_MODIFIED = getFileModified( FILE_GZP );
Log.ok( `Initialization Complete` ); const end = performance.now();
Log.info( `core`, chalk.yellow( `[init]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `TVApp2 container is ready` ), chalk.blueBright( `took ${ end - start }ms` ), chalk.blueBright( `<message>` ), chalk.gray( `TVApp2 container is ready; took ${ end - start }ms` ), chalk.blueBright( `<ip>` ), chalk.gray( `${ envIpContainer }` ), chalk.blueBright( `<gateway>` ), chalk.gray( `${ envIpGateway }` ), chalk.blueBright( `<port>` ), chalk.gray( `${ envWebPort }` ) );
} }
catch ( err ) catch ( err )
{ {
Log.error( `Initialization error:`, chalk.white( `` ), chalk.grey( `${ err.message }` ) ); Log.error( `core`, chalk.yellow( `[init]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.redBright( `Could not start up TVApp2 container due to error` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ err }` ), chalk.blueBright( `<ip>` ), chalk.gray( `${ envIpContainer }` ), chalk.blueBright( `<gateway>` ), chalk.gray( `${ envIpGateway }` ), chalk.blueBright( `<port>` ), chalk.gray( `${ envWebPort }` ) );
} }
} }
@@ -1169,8 +1214,6 @@ const server = http.createServer( ( request, response ) =>
const loadFile = reqUrl.replace( /^\/+/, '' ); const loadFile = reqUrl.replace( /^\/+/, '' );
Log.debug( `www`, chalk.blueBright( `[REQUEST]` ), chalk.white( `` ), chalk.grey( `asset>` ), chalk.greenBright( `${ loadFile }` ), chalk.grey( `<method>` ), chalk.greenBright( `${ method }` ) );
const handleRequest = async() => const handleRequest = async() =>
{ {
/* /*
@@ -1183,27 +1226,35 @@ const server = http.createServer( ( request, response ) =>
if ( subdomainRestart.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) ) 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(); 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' 'Content-Type': 'application/json'
}); });
response.end( `{ "status": "ok" }` ); Log.info( `www`, chalk.yellow( `[req]` ), chalk.white( `` ), chalk.blueBright( `<type>` ), chalk.gray( `api/restart` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
response.end( JSON.stringify( statusCheck ) );
return; return;
} }
if ( subdomainM3U.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) 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( `<type>` ), chalk.gray( `m3u playlist` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveM3U( response, request ); await serveM3U( response, request );
return; return;
@@ -1211,7 +1262,7 @@ const server = http.createServer( ( request, response ) =>
if ( subdomainChan.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) 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( `<type>` ), chalk.gray( `channel` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveM3UPlaylist( request, response ); await serveM3UPlaylist( request, response );
return; return;
@@ -1219,7 +1270,7 @@ const server = http.createServer( ( request, response ) =>
if ( subdomainKey.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) 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( `<type>` ), chalk.gray( `key` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveKey( request, response ); await serveKey( request, response );
return; return;
@@ -1227,7 +1278,7 @@ const server = http.createServer( ( request, response ) =>
if ( subdomainEPG.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) 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( `<type>` ), chalk.gray( `epg-uncompressed` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveXML( response, request ); await serveXML( response, request );
return; return;
@@ -1235,7 +1286,7 @@ const server = http.createServer( ( request, response ) =>
if ( subdomainGZP.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) 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( `<type>` ), chalk.gray( `epg-compressed` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveGZP( response, request ); await serveGZP( response, request );
return; return;
@@ -1243,7 +1294,7 @@ const server = http.createServer( ( request, response ) =>
if ( subdomainHealth.some( ( urlKeyword ) => loadFile.startsWith( urlKeyword ) ) && method === 'GET' ) 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( `<type>` ), chalk.gray( `api` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveHealthCheck( request, response ); await serveHealthCheck( request, response );
return; return;
@@ -1268,6 +1319,8 @@ const server = http.createServer( ( request, response ) =>
sizeGZP: FILE_GZP_SIZE, sizeGZP: FILE_GZP_SIZE,
dateGZP: FILE_GZP_MODIFIED, dateGZP: FILE_GZP_MODIFIED,
healthTimer: envHealthTimer,
appRelease: envAppRelease,
appName: name, appName: name,
appVersion: version, appVersion: version,
appUrlGithub: repository.url, appUrlGithub: repository.url,
@@ -1293,6 +1346,7 @@ const server = http.createServer( ( request, response ) =>
'.png' : 'image/png', '.png' : 'image/png',
'.gif' : 'image/gif', '.gif' : 'image/gif',
'.css' : 'text/css', '.css' : 'text/css',
'.scss' : 'text/x-sass',
'.gz' : 'application/gzip', '.gz' : 'application/gzip',
'.js' : 'text/javascript', '.js' : 'text/javascript',
'.txt' : 'text/plain', '.txt' : 'text/plain',
@@ -1311,45 +1365,46 @@ const server = http.createServer( ( request, response ) =>
response.setHeader( 'Content-type', fileMime ); response.setHeader( 'Content-type', fileMime );
response.end( data ); response.end( data );
Log.ok( `www`, chalk.greenBright( ` [LOAD] ` ), chalk.white( `` ), chalk.grey( `<asset>` ), chalk.greenBright( `${ loadFile }` ), chalk.grey( `<mime>` ), chalk.greenBright( `${ fileMime }` ) ); Log.ok( `www`, chalk.yellow( `[load]` ), chalk.white( `` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<mime>` ), chalk.gray( `${ fileMime }` ) );
} }
else else
{ {
if ( loadFile === 'discovery.json' ) 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 = const statusCheck =
{ {
ip: envIpContainer, ip: envIpContainer,
gateway: envIpGateway, gateway: envIpGateway,
uptime: process.uptime(), uptime: process.uptime(),
message: 'Page not found', message: 'Page not found',
status: 'healthy',
ref: request.url, ref: request.url,
method: method, method: method || 'GET',
code: 404, code: 404,
timestamp: Date.now() timestamp: Date.now()
}; };
response.writeHead( 404, { response.writeHead( statusCheck.code, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
Log.error( `www`, chalk.redBright( `[error]` ), chalk.white( `` ), chalk.grey( `${ statusCheck.message }` ), chalk.redBright( `${ loadFile }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
response.end( JSON.stringify( statusCheck ) ); response.end( JSON.stringify( statusCheck ) );
} }
}); });
}; };
handleRequest().catch( ( err ) => handleRequest().catch( ( err ) =>
{ {
Log.error( `Error handling request:`, chalk.white( `` ), chalk.grey( `${ err }` ) );
response.writeHead( 500, { response.writeHead( 500, {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
}); });
Log.error( `Error handling request:`, chalk.white( `` ), chalk.grey( `${ err }` ) );
response.end( 'Internal Server Error' ); response.end( 'Internal Server Error' );
}); });
}); });
@@ -1360,17 +1415,15 @@ const server = http.createServer( ( request, response ) =>
( async() => ( async() =>
{ {
const envWebIP = process.env.WEB_IP || '0.0.0.0'; if ( !envApiKey )
const envWebPort = process.env.WEB_PORT || `4124`; {
Log.warn( `core`, chalk.yellow( `[api]` ), chalk.white( `` ), chalk.blueBright( `<message>` ), chalk.gray( `API_KEY environment variable not defined for api, leaving blank` ) );
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(); await initialize();
server.listen( envWebPort, envWebIP, () => 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( `<message>` ), chalk.gray( `server is now running on` ), chalk.whiteBright.bgBlack( ` ${ envWebIP }:${ envWebPort } ` ) );
}); });
})(); })();

View File

@@ -1,6 +1,6 @@
{ {
"name": "tvapp2", "name": "tvapp2",
"version": "1.3.0", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@@ -1,12 +1,12 @@
{ {
"name": "tvapp2", "name": "tvapp2",
"version": "1.3.0", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tvapp2", "name": "tvapp2",
"version": "1.3.0", "version": "1.4.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^5.3.0", "chalk": "^5.3.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "tvapp2", "name": "tvapp2",
"version": "1.3.0", "version": "1.4.0",
"description": "This package allows you to generate M3U playlists and EPG guides from various online IPTV services.", "description": "This package allows you to generate M3U playlists and EPG guides from various online IPTV services.",
"author": "BinaryNinja", "author": "BinaryNinja",
"license": "MIT", "license": "MIT",

View File

@@ -1,24 +1,55 @@
/*
Boostrap 5 > Table
*/
.table .table
{ {
--bs-table-bg: #1b1b1b; --bs-table-bg: #1b1b1b;
--bs-table-border-color: #313131; --bs-table-border-color: #313131;
} }
.table-striped > tbody > tr:nth-of-type(2n+1) > * .table-striped > tbody > tr:nth-of-type(2n+1) > *
{ {
--bs-table-bg-type: #242424; --bs-table-bg-type: #242424;
} }
.table > :not(caption) > * > * /*
{ Boostrap 5 > Tooltips
*/
.tooltip-inner
{
background-color: #3061a1;
box-shadow: 0px 0px 4px black;
opacity: 1;
color: #FFF;
}
.tooltip.bs-tooltip-right .tooltip-arrow::before
{
border-right-color: #3061a1 !important;
}
.tooltip.bs-tooltip-left .tooltip-arrow::before
{
border-left-color: #3061a1 !important;
}
.tooltip.bs-tooltip-bottom .tooltip-arrow::before
{
border-bottom-color: #3061a1 !important;
}
.tooltip.bs-tooltip-top .tooltip-arrow::before
{
border-top-color: #3061a1 !important;
} }
body body
{ {
background-color: #f8f9fa; background-color: #f8f9fa;
padding-bottom: 20px; padding-bottom: 20px;
overflow: auto; overflow: auto;
} }
@media (prefers-color-scheme: dark) @media (prefers-color-scheme: dark)
@@ -73,20 +104,20 @@ body
@keyframes spin-scale @keyframes spin-scale
{ {
to { to {
transform: rotate(0deg) scale(1, 1); transform: rotate(0deg) scale(1, 1);
} }
from { from {
transform: rotate(360deg) scale(1.5, 1.5); transform: rotate(360deg) scale(1.5, 1.5);
} }
} }
@keyframes spin @keyframes spin
{ {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@@ -208,6 +239,12 @@ p
padding-left: 7px; padding-left: 7px;
} }
.header .health
{
color: #ff7575;
text-decoration: none;
}
#breadcrumbs::before #breadcrumbs::before
{ {
margin-top: 4px; margin-top: 4px;
@@ -310,8 +347,8 @@ p
.navbar-social svg .navbar-social svg
{ {
font-size: clamp(0.7em, 2vw, 1.1em); font-size: clamp(0.7em, 2vw, 1.0em);
margin-left: 10px; margin-left: 10px;
} }
.logo .logo
@@ -364,12 +401,12 @@ p
.spin .spin
{ {
transition-property: transform; transition-property: transform;
transition-duration: 0.5s; transition-duration: 0.5s;
animation-name: spin; animation-name: spin;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;
} }
.table thead th a .table thead th a
@@ -430,6 +467,7 @@ p
font-weight: 100; font-weight: 100;
border: 1px dashed #9e973a; border: 1px dashed #9e973a;
display: none; display: none;
z-index: 5000 !important;
} }
#ntfy-firewall #ntfy-firewall
@@ -542,7 +580,7 @@ code
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
z-index: 999; z-index: 200;
} }
.fixed-bottom .fixed-bottom
@@ -551,7 +589,7 @@ code
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 999; z-index: 200;
} }
.sticky-top .sticky-top
@@ -560,17 +598,25 @@ code
{ {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 999; z-index: 99999;
} }
} }
.sticky-bottom
{
position: -webkit-sticky;
position: relative !important;
bottom: 0;
z-index: 500 !important;
}
.sticky-bottom .sticky-bottom
{ {
@supports (position: sticky) @supports (position: sticky)
{ {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
z-index: 999; z-index: 200;
} }
} }
@@ -580,6 +626,40 @@ code
position: relative; position: relative;
} }
@keyframes heartbeat
{
0%
{
transform: scale(1.2);
}
20%
{
transform: scale(1.5);
}
40%
{
transform: scale(1.2);
}
60%
{
transform: scale(1.5);
}
80%
{
transform: scale(1.2);
}
100%
{
transform: scale(1.2);
}
}
.heart
{
position: relative;
animation: heartbeat 1s infinite;
}
@-webkit-keyframes fadeOut { @-webkit-keyframes fadeOut {
0% { 0% {
opacity: 1; opacity: 1;
@@ -620,7 +700,7 @@ code
#dimmer #dimmer
{ {
z-index: 300; z-index: 200;
position: fixed; position: fixed;
background-color: #000000; background-color: #000000;
width: 100%; width: 100%;

View File

@@ -1,27 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
<head> <head>
<title><%= appName %> - v<%= appVersion %></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title><%= appName %> - v<%= appVersion %></title>
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
<link rel="stylesheet" href="css/tvapp2.min.css"> <link rel="stylesheet" href="css/tvapp2.min.css">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<script src='https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js' integrity='sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq' crossorigin='anonymous'></script>
<script src=' https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js '></script>
<script src='js/tvapp2.min.js'></script>
</head> </head>
<body> <body>
<!-- Header --> <!-- Header -->
<div class="header"> <div class="header">
<nav class="navbar sticky-top container"> <nav class="navbar sticky-top container">
<div class="navbar-brand"> <div class="navbar-brand">
<i class="logo fa-sharp-duotone fa-regular fa-tv" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i> <i data-bs-toggle="tooltip" title="v<%= appVersion %>" class="logo fa-sharp-duotone fa-regular fa-tv" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i>
<a class="header-name" href="<%= appUrlGithub %>">TVApp2 for Docker</a> <a class="header-name" href="<%= appUrlGithub %>">TVApp2 for Docker</a>
</div> </div>
<div class="navbar-social"> <div class="navbar-social">
<a href="javascript:toggleRestart();"><i class="restart fa-solid fa-rotate" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a> <a href=""><i id="action-health" data-bs-toggle="tooltip" title="Health" class="heart logo health fa-duotone fa-solid fa-heart" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
<a href="<%= appUrlDocs %>"><i class="logo fa-duotone fa-solid fa-book-open-cover" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a> <a href="javascript:runResync();"><i id="action-resync" data-bs-toggle="tooltip" title="Resync" class="restart fa-solid fa-rotate" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
<a href="<%= appUrlGithub %>"><i class="logo fa-logos fa-github" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a> <a href="<%= appUrlDocs %>"><i data-bs-toggle="tooltip" title="Documentation" class="logo fa-duotone fa-solid fa-book-open-cover" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
<a href="<%= appUrlDiscord %>"><i class="logo fa-logos fa-discord" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a> <a href="<%= appUrlGithub %>"><i data-bs-toggle="tooltip" title="Github" class="logo fa-logos fa-github" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
<a href="<%= appUrlDiscord %>"><i data-bs-toggle="tooltip" title="Discord" class="logo fa-logos fa-discord" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i></a>
</div> </div>
</nav> </nav>
</div> </div>
@@ -36,7 +40,7 @@
</div> </div>
</div> </div>
<!-- Header Fontawesome Icons --> <!-- Header Fontawesome Icons -->
<div class="container main-container"> <div class="container main-container">
<table id="list" class="table table-dark table-striped"> <table id="list" class="table table-dark table-striped">
<thead> <thead>
@@ -68,7 +72,7 @@
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> --> <!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td> </td>
<td class="file cell-file"> <td class="file cell-file">
<a id="m3u-name" target="_blank"></a> <a id="m3u-name" target="_blank" data-bs-toggle="tooltip" title="IPTV channel list"></a>
</td> </td>
<td class="link cell-link"><a id="m3u-link" target="_blank"></a></td> <td class="link cell-link"><a id="m3u-link" target="_blank"></a></td>
<td class="size cell-size"><span id="m3u-size"></span></td> <td class="size cell-size"><span id="m3u-size"></span></td>
@@ -83,7 +87,7 @@
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> --> <!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td> </td>
<td class="file cell-file"> <td class="file cell-file">
<a id="xml-name" target="_blank"></a> <a id="xml-name" target="_blank" data-bs-toggle="tooltip" title="Uncompressed TV guide data"></a>
</td> </td>
<td class="link cell-link"><a id="xml-link" target="_blank"></a></td> <td class="link cell-link"><a id="xml-link" target="_blank"></a></td>
<td class="size cell-size"><span id="xml-size"></span></td> <td class="size cell-size"><span id="xml-size"></span></td>
@@ -98,7 +102,7 @@
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> --> <!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td> </td>
<td class="file cell-gzp"> <td class="file cell-gzp">
<a id="gzp-name" target="_blank"></a> <a id="gzp-name" target="_blank" data-bs-toggle="tooltip" title="Compressed TV guide data"></a>
</td> </td>
<td class="link cell-link"><a id="gzp-link" target="_blank"></a></td> <td class="link cell-link"><a id="gzp-link" target="_blank"></a></td>
<td class="size cell-size"><span id="gzp-size"></span></td> <td class="size cell-size"><span id="gzp-size"></span></td>
@@ -120,71 +124,130 @@
<div class="footer-inner"> <div class="footer-inner">
<div class="container"> <div class="container">
<div class="col text-center text-muted text-small text-nowrap"> <div class="col text-center text-muted text-small text-nowrap">
<small>Developed by BinaryNinja - <a href="<%= appUrlGithub %>"><%= appName %></a> - v<%= appVersion %></small><br /> <small>Developed by BinaryNinja - <a data-bs-toggle="tooltip" title="<%= appRelease %> build" href="<%= appUrlGithub %>"><%= appName %> (<%= appRelease %>)</a> - v<%= appVersion %></small><br />
<small>This utility is for educational purposes only</small> <small>This utility is for educational purposes only</small>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>
<!-- Toast Notifications -->
<!-- <button type="button" class="btn btn-primary" id="btnTestToasts">Show toast</button> -->
<div style="z-index: 9999;" class="toast position-fixed bottom-0 end-0 p-8 m-3" id="tvapp2Toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="4000">
<div class="toast-body">
<div class="d-flex gap-4">
<span><i class="fa-solid fa-circle-check fa-lg icon-success"></i></span>
<div class="d-flex flex-column flex-grow-1 gap-2">
<div class="d-flex align-items-center">
<span id="toast-title" class="fw-semibold">Toast Title</span>
<button type="button" class="btn-close btn-close-sm ms-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<span id="toast-message">Dismiss in 6 seconds</span>
</div>
</div>
</div>
</div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="modalTvapp2" tabindex="-1" data-bs-backdrop="static" aria-labelledby="modalTvapp2Label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">TVApp2</h5> <h5 class="modal-title" id="modalTvapp2Label">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Do you ever feel like a plastic bag.... drifting through the wind? ...
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Welcome to costco</button> <button type="button" id="btn-secondary" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">I'll be back</button> <button type="button" id="btn-primary" class="btn btn-primary">Save changes</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
/*
this is test code. enable the "Show Toast" button and then uncomment this code.
document.getElementById("btnTestToasts").onclick = function()
{
var toastElList = [].slice.call(document.querySelectorAll('.toast'))
var toastList = toastElList.map(function(toastEl)
{
return new bootstrap.Toast(toastEl)
});
toastList.forEach(toast => toast.show());
console.log(toastList);
};
*/
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
const urlBase = window.location.origin; const urlBase = window.location.origin;
const urlM3U = urlBase + "/playlist"; const urlM3U = urlBase + '/playlist';
const urlXML = urlBase + "/epg"; const urlXML = urlBase + '/epg';
const urlGZP = urlBase + "/gzip"; const urlGZP = urlBase + '/gzip';
document.getElementById("m3u-name").textContent = "<%= fileM3U %>"; document.getElementById('m3u-name').textContent = '<%= fileM3U %>';
document.getElementById("m3u-name").href = urlM3U; document.getElementById('m3u-name').href = urlM3U;
document.getElementById("m3u-link").textContent = urlM3U; document.getElementById('m3u-link').textContent = urlM3U;
document.getElementById("m3u-link").href = urlM3U; document.getElementById('m3u-link').href = urlM3U;
document.getElementById("m3u-size").textContent = "<%= sizeM3U %>"; document.getElementById('m3u-size').textContent = '<%= sizeM3U %>';
document.getElementById("m3u-date").textContent = "<%= dateM3U %>"; document.getElementById('m3u-date').textContent = '<%= dateM3U %>';
document.getElementById("xml-name").textContent = "<%= fileXML %>"; document.getElementById('xml-name').textContent = '<%= fileXML %>';
document.getElementById("xml-name").href = urlXML; document.getElementById('xml-name').href = urlXML;
document.getElementById("xml-link").textContent = urlXML; document.getElementById('xml-link').textContent = urlXML;
document.getElementById("xml-link").href = urlXML; document.getElementById('xml-link').href = urlXML;
document.getElementById("xml-size").textContent = "<%= sizeXML %>"; document.getElementById('xml-size').textContent = '<%= sizeXML %>';
document.getElementById("xml-date").textContent = "<%= dateXML %>"; document.getElementById('xml-date').textContent = '<%= dateXML %>';
document.getElementById("gzp-name").textContent = "<%= fileGZP %>"; document.getElementById('gzp-name').textContent = '<%= fileGZP %>';
document.getElementById("gzp-name").href = urlGZP; document.getElementById('gzp-name').href = urlGZP;
document.getElementById("gzp-link").textContent = urlGZP; document.getElementById('gzp-link').textContent = urlGZP;
document.getElementById("gzp-link").href = urlGZP; document.getElementById('gzp-link').href = urlGZP;
document.getElementById("gzp-size").textContent = "<%= sizeGZP %>"; document.getElementById('gzp-size').textContent = '<%= sizeGZP %>';
document.getElementById("gzp-date").textContent = "<%= dateGZP %>"; document.getElementById('gzp-date').textContent = '<%= dateGZP %>';
</script> </script>
<script> <script>
/*
Document Ready
*/
$(function(){
$("[data-bs-toggle=tooltip]").tooltip({ placement: 'bottom'});
});
/*
Action > DOM Status
*/
document.addEventListener("DOMContentReady", function() {
$("#tvapp2Toast").toast();
});
/*
document.addEventListener("DOMContentLoaded", function() {
$('#tvapp2Toast').toast("show");
});
*/
/* /*
Notify > Localhost Notify > Localhost
*/ */
document.addEventListener("DOMContentLoaded", function() document.addEventListener('DOMContentLoaded', function() {
{
const host = window.location.hostname; const host = window.location.hostname;
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80"); const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
if (host === "localhost" || host === "127.0.0.1") if (host === 'localhost' || host === '127.0.0.1')
{ {
const msg = "<p><span class='warning'>Warning</span> If you are accessing this page via 127.0.0.1 or localhost, proxying will not work on other devices.Please load \ const msg = "<p><span class='warning'>Warning</span> If you are accessing this page via 127.0.0.1 or localhost, proxying will not work on other devices.Please load \
this page using your computer's IP address (e.g., 192.168.x.x) and port in order to access the playlist from other devices on your network.</p> \ this page using your computer's IP address (e.g., 192.168.x.x) and port in order to access the playlist from other devices on your network.</p> \
@@ -192,10 +255,10 @@
<p> Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> or \ <p> Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> or \
<a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.</p>"; <a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.</p>";
document.getElementById("ntfy-localhost").innerHTML = msg; document.getElementById('ntfy-localhost').innerHTML = msg;
document.getElementById("ntfy-localhost").style.display = "block"; document.getElementById('ntfy-localhost').style.display = 'block';
} else { } else {
document.getElementById("ntfy-localhost").style.display = "none"; document.getElementById('ntfy-localhost').style.display = 'none';
} }
}); });
@@ -203,79 +266,242 @@
Notify > Firewall Notify > Firewall
*/ */
document.addEventListener("DOMContentLoaded", function() document.addEventListener('DOMContentLoaded', function() {
{ const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
const msg = "<p><span class='notice'>Notice</span> Port <strong> " + port + " </strong> must be open and allowed through your <a href='https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18 target='_blank'>Windows</a> \ const msg = "<p><span class='notice'>Notice</span> Port <strong> " + port + " </strong> must be open and allowed through your <a href='https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18 target='_blank'>Windows</a> \
or <a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a> OS firewall settings \ or <a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a> OS firewall settings \
This action enables devices such as Firestick or Android to connect to the server and request the playlist through the proxy.</p>"; This action enables devices such as Firestick or Android to connect to the server and request the playlist through the proxy.</p>";
document.getElementById("ntfy-firewall").innerHTML = msg; document.getElementById('ntfy-firewall').innerHTML = msg;
document.getElementById("ntfy-firewall").style.display = "block"; document.getElementById('ntfy-firewall').style.display = 'block';
}); });
/* /*
Notify > Restart / Resync Notify > Restart / Resync
*/ */
document.addEventListener("DOMContentLoaded", function() document.addEventListener('DOMContentLoaded', function() {
{ const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
const msg = "<p><span class='success'>Success</span> Your IPTV m3u channels and xml guide data has been successfully re-synced. \ const msg = "<p><span class='success'>Success</span> Your IPTV m3u channels and xml guide data has been successfully re-synced. \
Please refresh this window to see new data</p>"; Please refresh this window to see new data</p>";
document.getElementById("ntfy-restart").innerHTML = msg; document.getElementById('ntfy-restart').innerHTML = msg;
document.getElementById("ntfy-restart").style.display = "none"; document.getElementById('ntfy-restart').style.display = 'none';
}); });
/* /*
Activate Resync Set initial health check sync time
first health check runs after 10 seconds
all future health checks run after <%= healthTimer %>
*/ */
function toggleRestart() let timerDelayMS = 10000;
let timerStartMS = Date.now(); // returns milliseconds
const timerHealthRun = '<%= healthTimer %>'; // time in milliseconds until health check ran AFTER initial run
/*
Action > Healthcheck
*/
function runHealthCheck()
{
const toastTypeClass = [];
toastTypeClass[ 'DEFAULT' ] = 'text-bg-primary';
toastTypeClass[ 'UNHEALTHY' ] = 'text-bg-warning';
toastTypeClass[ 'HEALTHY' ] = 'text-bg-success';
toastTypeClass[ 'ERROR' ] = 'text-bg-danger';
$.ajax(
{
url: 'api/health',
type: 'GET',
data: {
internal: 1
},
beforeSend: function( data )
{
console.log('Sending health check ...')
},
success: function( data )
{
const status = data.message;
const code = data.code;
if ( status )
{
const toastClass = toastTypeClass[status.toUpperCase()];
const toastElm = document.getElementById('tvapp2Toast');
toastElm.classList.add(toastClass);
$('.toast #toast-title').html(`<%= appName %> is ${ status }`);
$('.toast #toast-message').html(`Health check returned ${ status } (${ code })`);
$('#tvapp2Toast').toast('show');
}
},
error: function( data )
{
const toastClass = toastTypeClass['ERROR'];
const toastElm = document.getElementById('tvapp2Toast');
toastElm.classList.add(toastClass);
$('.toast #toast-title').html(`Could not connect to health check api`);
$('.toast #toast-message').html(`Failed to communicate with health check api. Try restarting the docker container to restore connection.`);
$('#tvapp2Toast').toast('show');
}
}).always(function()
{
timerDelayMS = parseInt(timerHealthRun);
timerStartMS = Date.now();
setTimeout(function()
{
runHealthCheck();
}, parseInt(timerHealthRun));
}).responseText;
}
/*
Action > Do Resync
*/
function runResync()
{ {
$.ajax( $.ajax(
{ {
url: 'restart', url: 'restart',
type: 'POST', type: 'POST',
data: { data: {
x: 1 internal: 1
}, },
beforeSend: function( data ) beforeSend: function( data )
{ {
const dimmer = document.createElement('div'); const dimmer = document.createElement('div');
dimmer.setAttribute("id", "dimmer"); dimmer.setAttribute('id', 'dimmer');
dimmer.style.visibility = "visible"; dimmer.style.visibility = 'visible';
dimmer.classList.add("dimmer-in"); dimmer.classList.add('dimmer-in');
document.getElementsByTagName('body')[0].appendChild(dimmer); document.getElementsByTagName('body')[0].appendChild(dimmer);
document.getElementById("ntfy-firewall").style.display = "none"; document.getElementById('ntfy-firewall').style.display = 'none';
document.getElementById("ntfy-localhost").style.display = "none"; document.getElementById('ntfy-localhost').style.display = 'none';
document.getElementById("ntfy-restart").style.display = "none"; document.getElementById('ntfy-restart').style.display = 'none';
const iconResync = document.getElementsByClassName('fa-rotate'); const iconResync = document.getElementsByClassName('fa-rotate');
iconResync[0].classList.remove("restart"); iconResync[0].classList.remove('restart');
iconResync[0].classList.add("spin"); iconResync[0].classList.add('spin');
$('.modal-content .modal-body').html('<small>The M3U and EPG data will now be re-downloaded and synced with your TVApp2 container. Afterward, this page will be refreshed automatically.</small><br /><br /><small>Please wait...</small>')
$('.modal-content .modal-title').html('Resyncing Data')
$('#modalTvapp2').modal('show');
const modalBtnPrimary = document.querySelector('#btn-primary');
modalBtnPrimary.style.display = 'none';
modalBtnPrimary.style.visibility= 'hidden';
}, },
success: function( data ) success: function( data )
{ {
setTimeout(() =>
/*
On successful restart, wait 1 second, remove dimmer, reload page in 5 seconds
*/
setTimeout( () =>
{ {
document.getElementById("ntfy-restart").style.display = "block" document.getElementById('ntfy-restart').style.display = 'block'
const dimmer = document.getElementById("dimmer"); const dimmer = document.getElementById('dimmer');
dimmer.classList.remove("dimmer-in"); dimmer.classList.remove('dimmer-in');
dimmer.classList.add("dimmer-out"); dimmer.classList.add('dimmer-out');
dimmer.remove(); dimmer.remove();
const iconResync = document.getElementsByClassName('fa-rotate');
iconResync[0].classList.remove("spin"); setTimeout( function()
iconResync[0].classList.add("restart"); {
setTimeout(location.reload.bind(location), 1000); const iconResync = document.getElementsByClassName('fa-rotate'); // resync favicon
}, 1000); iconResync[0].classList.remove('spin'); // stop spinning
iconResync[0].classList.add('restart'); // normal spinner class
document.location.reload() // reload page
}, 5000 ); // how long until refresh page
}, 1000 ); // how long until dimmer is removed / reload page activated (also on delay)
} }
}); });
} }
/*
Health check > Show time remaining as tooltip
*/
function runTooltipCountdown( )
{
let timerHours, timerMins, timerRemainsLS;
function twoDigits( n )
{
return (n <= 9 ? "0" + n : n);
}
/*
Update Tooltip Countdown
MS = milliseconds
LS = long string (Wed Dec 31 1969 10:01:42 (Coordinated Universal Time))
*/
function updateTooltipCountdown()
{
const timerElapsedMS = Date.now() - timerStartMS; // ( 2091 )
const timerRemainsMS = timerDelayMS - timerElapsedMS; // ( 7909 ) divide by 1000 for seconds
timerRemainsLS = new Date( timerRemainsMS ); // (Wed Dec 31 1969 10:01:42 (Coordinated Universal Time))
timerHours = timerRemainsLS.getUTCHours(); // ( 0 )
timerMins = timerRemainsLS.getUTCMinutes(); // ( 9 )
const timeLeft = (timerHours ? timerHours + ':' + twoDigits( timerMins ) : timerMins) + ':' + twoDigits( timerRemainsLS.getUTCSeconds() );
jQuery(function($)
{
$(document.body).tooltip({ selector: "[title]" });
$('#action-health')
.attr('data-original-title', `Health check in ${ timeLeft }`)
.attr('aria-label', `Health check in ${ timeLeft }`)
.attr('data-bs-original-title', `Health check in ${ timeLeft }`)
});
const Heart = document.getElementsByClassName('fa-heart');
Heart[0].style.color = '#FFF';
setTimeout( function()
{
const Heart = document.getElementsByClassName('fa-heart');
Heart[0].style.color = '#FFF';
setTimeout( function()
{
Heart[0].style.color = '#FF6593';
}, timerRemainsLS.getUTCMilliseconds() + 100 );
}, timerRemainsLS.getUTCMilliseconds() + 500 );
setTimeout( function()
{
updateTooltipCountdown();
}, timerRemainsLS.getUTCMilliseconds() + 500 );
}
updateTooltipCountdown();
}
/*
Action > Healthcheck > Initialize
*/
setTimeout( function() { runHealthCheck(); }, timerDelayMS );
/*
Action > Tooltip Resync Timers
*/
runTooltipCountdown( );
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js" integrity="sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq" crossorigin="anonymous"></script>
<script src=" https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js "></script>
<script src="js/tvapp2.min.js"></script>
</body> </body>
</html> </html>