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

View File

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

View File

@@ -455,6 +455,7 @@ jobs:
sbom: false
build-args: |-
ARCH=amd64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }}
BUILDDATE=${{ env.NOW_DOCKER_LABEL }}
@@ -511,6 +512,7 @@ jobs:
sbom: false
build-args: |-
ARCH=arm64
RELEASE=${{ inputs.DEV_RELEASE == true && 'development' || 'stable' }}
VERSION=${{ env.IMAGE_VERSION }}
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 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/"
@@ -76,6 +79,7 @@ ENV FILE_URL="urls.txt"
ENV FILE_M3U="playlist.m3u8"
ENV FILE_EPG="xmltv.xml"
ENV FILE_TAR="xmltv.xml.gz"
ENV HEALTH_TIMER=600000
ENV LOG_LEVEL=4
ENV TZ="Etc/UTC"

View File

@@ -67,13 +67,18 @@ const FOLDER_WWW = 'www';
Define > Environment Variables || Defaults
*/
const envAppRelease = process.env.RELEASE || 'stable';
const envUrlRepo = process.env.URL_REPO || 'https://git.binaryninja.net/binaryninja';
const envStreamQuality = process.env.STREAM_QUALITY || 'hd';
const envFileURL = process.env.FILE_URL || 'urls.txt';
const envFileM3U = process.env.FILE_M3U || 'playlist.m3u8';
const envFileXML = process.env.FILE_EPG || 'xmltv.xml';
const envFileGZP = process.env.FILE_GZP || 'xmltv.xml.gz';
const envApiKey = process.env.API_KEY || null;
const envWebIP = process.env.WEB_IP || '0.0.0.0';
const envWebPort = process.env.WEB_PORT || `4124`;
const envWebEncoding = process.env.WEB_ENCODING || 'deflate, br';
const envHealthTimer = process.env.HEALTH_TIMER || 600000;
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/key
http://127.0.0.1:4124/channel
http://127.0.0.1:4124/health
*/
const subdomainRestart = [ 'restart', 'sync', 'resync' ];
@@ -109,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' ];
/*
@@ -210,7 +216,7 @@ class Log
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 );
FILE_URL = path.join( basePath, FOLDER_WWW, `${ envFileURL }` );
@@ -221,7 +227,7 @@ if ( process.pkg )
}
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_M3U = path.resolve( __dirname, FOLDER_WWW, `${ envFileM3U }` );
@@ -282,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( `<message>` ), chalk.gray( `Downloading external file` ), chalk.blueBright( `<source>` ), chalk.gray( `${ url }` ), chalk.blueBright( `<destination>` ), chalk.gray( `${ filePath }` ) );
return new Promise( ( resolve, reject ) =>
{
@@ -294,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( `<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 }` ) );
}
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( `<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 ) );
});
})
.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 ) );
});
});
@@ -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
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 ) )
{
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
{
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;
}
}
}
/*
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( `<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 ) =>
{
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 ) =>
{
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 )
{
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 }` ) );
}
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 )
{
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 }` ) );
}
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 ) =>
{
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 }` ) );
}
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 );
});
});
@@ -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
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
{
@@ -476,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( `<message>` ), chalk.yellowBright( `Source xml file found, but gzip failed generate a compressed .gz fileL` ), chalk.blueBright( `<source>` ), 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( `<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;
}
}
@@ -521,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( `<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 }` ) );
}
@@ -574,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( `<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 ) );
}
@@ -602,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( `<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 ) );
}
}
@@ -704,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( `<message>` ), chalk.redBright( `Cannot find "stream_name` ), chalk.blueBright( `<url>` ), chalk.grey( `${ channelUrl }` ) );
return null;
}
streamName = streamNameMatch[1];
@@ -724,6 +727,8 @@ async function getTokenizedUrl( channelUrl )
const tokenResponse = await fetchPage( tokenUrl );
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
{
const json = JSON.parse( tokenResponse );
@@ -731,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( `<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;
}
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;
}
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;
}
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;
}
}
@@ -760,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( `<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 ) );
return;
}
const decodedUrl = decodeURIComponent( urlParam );
if ( decodedUrl.endsWith( '.ts' ) )
{
@@ -801,31 +807,36 @@ async function serveM3UPlaylist( req, res )
'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 );
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 );
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( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<stream>` ), chalk.gray( `${ urlParam }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) );
return;
@@ -844,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 =
@@ -853,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( `<message>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<error>` ), chalk.redBright( `${ statusCheck.message }` ), chalk.blueBright( `<statusCode>` ), chalk.gray( `${ statusCheck.code }` ) );
res.end( JSON.stringify( statusCheck ) );
}
}
@@ -876,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( `<message>` ), 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( `<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;
}
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( `<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
@@ -961,7 +985,7 @@ async function rewriteM3U( originalUrl, req )
Serves IPTV .m3u playlist
*/
async function serveM3U( response, req )
async function serveM3U( res, req )
{
try
{
@@ -979,22 +1003,36 @@ async function serveM3U( response, req )
return `${ baseUrl }/channel?url=${ encodeURIComponent( fullUrl ) }`;
});
response.writeHead( 200, {
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( `<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 }` ) );
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 )
{
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, {
'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 )
{
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, {
'Content-Type': 'text/plain'
@@ -1070,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( `<message>` ), chalk.gray( `new key created` ), chalk.blueBright( `<key>` ), chalk.gray( `${ key }` ), chalk.blueBright( `<expire>` ), chalk.gray( `${ ttl / 1000 } seconds` ) );
}
function getCache( key )
@@ -1083,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( `<message>` ), chalk.gray( `key has expired, marked for deletion` ), chalk.blueBright( `<key>` ), chalk.gray( `${ key }` ) );
cache.delete( key );
return null;
@@ -1100,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( `<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( 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 )
@@ -1123,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( `<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 )
{
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( /^\/+/, '' );
Log.debug( `www`, chalk.blueBright( `[REQUEST]` ), chalk.white( `` ), chalk.grey( `asset>` ), chalk.greenBright( `${ loadFile }` ), chalk.grey( `<method>` ), chalk.greenBright( `${ method }` ) );
const handleRequest = async() =>
{
/*
@@ -1183,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( `<type>` ), chalk.gray( `api/restart` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), 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( `<type>` ), chalk.gray( `m3u playlist` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveM3U( response, request );
return;
@@ -1211,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( `<type>` ), chalk.gray( `channel` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveM3UPlaylist( request, response );
return;
@@ -1219,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( `<type>` ), chalk.gray( `key` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveKey( request, response );
return;
@@ -1227,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( `<type>` ), chalk.gray( `epg-uncompressed` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveXML( response, request );
return;
@@ -1235,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( `<type>` ), chalk.gray( `epg-compressed` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveGZP( response, request );
return;
@@ -1243,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( `<type>` ), chalk.gray( `api` ), chalk.blueBright( `<file>` ), chalk.gray( `${ loadFile }` ), chalk.blueBright( `<method>` ), chalk.gray( `${ method }` ) );
await serveHealthCheck( request, response );
return;
@@ -1268,6 +1319,8 @@ const server = http.createServer( ( request, response ) =>
sizeGZP: FILE_GZP_SIZE,
dateGZP: FILE_GZP_MODIFIED,
healthTimer: envHealthTimer,
appRelease: envAppRelease,
appName: name,
appVersion: version,
appUrlGithub: repository.url,
@@ -1293,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',
@@ -1311,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( `<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
{
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( `<statusCode>` ), 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' );
});
});
@@ -1360,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( `<message>` ), 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( `<message>` ), chalk.gray( `server is now running on` ), chalk.whiteBright.bgBlack( ` ${ envWebIP }:${ envWebPort } ` ) );
});
})();

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"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.",
"author": "BinaryNinja",
"license": "MIT",

View File

@@ -1,3 +1,7 @@
/*
Boostrap 5 > Table
*/
.table
{
--bs-table-bg: #1b1b1b;
@@ -9,9 +13,36 @@
--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
@@ -208,6 +239,12 @@ p
padding-left: 7px;
}
.header .health
{
color: #ff7575;
text-decoration: none;
}
#breadcrumbs::before
{
margin-top: 4px;
@@ -310,7 +347,7 @@ p
.navbar-social svg
{
font-size: clamp(0.7em, 2vw, 1.1em);
font-size: clamp(0.7em, 2vw, 1.0em);
margin-left: 10px;
}
@@ -430,6 +467,7 @@ p
font-weight: 100;
border: 1px dashed #9e973a;
display: none;
z-index: 5000 !important;
}
#ntfy-firewall
@@ -542,7 +580,7 @@ code
top: 0;
right: 0;
left: 0;
z-index: 999;
z-index: 200;
}
.fixed-bottom
@@ -551,7 +589,7 @@ code
right: 0;
bottom: 0;
left: 0;
z-index: 999;
z-index: 200;
}
.sticky-top
@@ -560,17 +598,25 @@ code
{
position: sticky;
top: 0;
z-index: 999;
z-index: 99999;
}
}
.sticky-bottom
{
position: -webkit-sticky;
position: relative !important;
bottom: 0;
z-index: 500 !important;
}
.sticky-bottom
{
@supports (position: sticky)
{
position: sticky;
bottom: 0;
z-index: 999;
z-index: 200;
}
}
@@ -580,6 +626,40 @@ code
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 {
0% {
opacity: 1;
@@ -620,7 +700,7 @@ code
#dimmer
{
z-index: 300;
z-index: 200;
position: fixed;
background-color: #000000;
width: 100%;

View File

@@ -1,27 +1,31 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<title><%= appName %> - v<%= appVersion %></title>
<meta charset="utf-8">
<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">
<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="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>
<body>
<!-- Header -->
<div class="header">
<nav class="navbar sticky-top container">
<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>
</div>
<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="<%= 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="<%= 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="<%= 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=""><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="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="<%= 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="<%= 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>
</nav>
</div>
@@ -68,7 +72,7 @@
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td>
<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 class="link cell-link"><a id="m3u-link" target="_blank"></a></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> -->
</td>
<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 class="link cell-link"><a id="xml-link" target="_blank"></a></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> -->
</td>
<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 class="link cell-link"><a id="gzp-link" target="_blank"></a></td>
<td class="size cell-size"><span id="gzp-size"></span></td>
@@ -120,71 +124,130 @@
<div class="footer-inner">
<div class="container">
<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>
</div>
</div>
</div>
</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 -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal fade" id="modalTvapp2" tabindex="-1" data-bs-backdrop="static" aria-labelledby="modalTvapp2Label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<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>
</div>
<div class="modal-body">
Do you ever feel like a plastic bag.... drifting through the wind?
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Welcome to costco</button>
<button type="button" class="btn btn-primary">I'll be back</button>
<button type="button" id="btn-secondary" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" id="btn-primary" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<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 urlM3U = urlBase + "/playlist";
const urlXML = urlBase + "/epg";
const urlGZP = urlBase + "/gzip";
const urlM3U = urlBase + '/playlist';
const urlXML = urlBase + '/epg';
const urlGZP = urlBase + '/gzip';
document.getElementById("m3u-name").textContent = "<%= fileM3U %>";
document.getElementById("m3u-name").href = urlM3U;
document.getElementById("m3u-link").textContent = urlM3U;
document.getElementById("m3u-link").href = urlM3U;
document.getElementById("m3u-size").textContent = "<%= sizeM3U %>";
document.getElementById("m3u-date").textContent = "<%= dateM3U %>";
document.getElementById('m3u-name').textContent = '<%= fileM3U %>';
document.getElementById('m3u-name').href = urlM3U;
document.getElementById('m3u-link').textContent = urlM3U;
document.getElementById('m3u-link').href = urlM3U;
document.getElementById('m3u-size').textContent = '<%= sizeM3U %>';
document.getElementById('m3u-date').textContent = '<%= dateM3U %>';
document.getElementById("xml-name").textContent = "<%= fileXML %>";
document.getElementById("xml-name").href = urlXML;
document.getElementById("xml-link").textContent = urlXML;
document.getElementById("xml-link").href = urlXML;
document.getElementById("xml-size").textContent = "<%= sizeXML %>";
document.getElementById("xml-date").textContent = "<%= dateXML %>";
document.getElementById('xml-name').textContent = '<%= fileXML %>';
document.getElementById('xml-name').href = urlXML;
document.getElementById('xml-link').textContent = urlXML;
document.getElementById('xml-link').href = urlXML;
document.getElementById('xml-size').textContent = '<%= sizeXML %>';
document.getElementById('xml-date').textContent = '<%= dateXML %>';
document.getElementById("gzp-name").textContent = "<%= fileGZP %>";
document.getElementById("gzp-name").href = urlGZP;
document.getElementById("gzp-link").textContent = urlGZP;
document.getElementById("gzp-link").href = urlGZP;
document.getElementById("gzp-size").textContent = "<%= sizeGZP %>";
document.getElementById("gzp-date").textContent = "<%= dateGZP %>";
document.getElementById('gzp-name').textContent = '<%= fileGZP %>';
document.getElementById('gzp-name').href = urlGZP;
document.getElementById('gzp-link').textContent = urlGZP;
document.getElementById('gzp-link').href = urlGZP;
document.getElementById('gzp-size').textContent = '<%= sizeGZP %>';
document.getElementById('gzp-date').textContent = '<%= dateGZP %>';
</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
*/
document.addEventListener("DOMContentLoaded", function()
{
document.addEventListener('DOMContentLoaded', function() {
const host = window.location.hostname;
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
if (host === "localhost" || host === "127.0.0.1")
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
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 \
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 \
<a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.</p>";
document.getElementById("ntfy-localhost").innerHTML = msg;
document.getElementById("ntfy-localhost").style.display = "block";
document.getElementById('ntfy-localhost').innerHTML = msg;
document.getElementById('ntfy-localhost').style.display = 'block';
} else {
document.getElementById("ntfy-localhost").style.display = "none";
document.getElementById('ntfy-localhost').style.display = 'none';
}
});
@@ -203,79 +266,242 @@
Notify > Firewall
*/
document.addEventListener("DOMContentLoaded", function()
{
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
document.addEventListener('DOMContentLoaded', function() {
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> \
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>";
document.getElementById("ntfy-firewall").innerHTML = msg;
document.getElementById("ntfy-firewall").style.display = "block";
document.getElementById('ntfy-firewall').innerHTML = msg;
document.getElementById('ntfy-firewall').style.display = 'block';
});
/*
Notify > Restart / Resync
*/
document.addEventListener("DOMContentLoaded", function()
{
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
document.addEventListener('DOMContentLoaded', function() {
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. \
Please refresh this window to see new data</p>";
document.getElementById("ntfy-restart").innerHTML = msg;
document.getElementById("ntfy-restart").style.display = "none";
document.getElementById('ntfy-restart').innerHTML = msg;
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(
{
url: 'restart',
type: 'POST',
data: {
x: 1
internal: 1
},
beforeSend: function( data )
{
const dimmer = document.createElement('div');
dimmer.setAttribute("id", "dimmer");
dimmer.style.visibility = "visible";
dimmer.classList.add("dimmer-in");
dimmer.setAttribute('id', 'dimmer');
dimmer.style.visibility = 'visible';
dimmer.classList.add('dimmer-in');
document.getElementsByTagName('body')[0].appendChild(dimmer);
document.getElementById("ntfy-firewall").style.display = "none";
document.getElementById("ntfy-localhost").style.display = "none";
document.getElementById("ntfy-restart").style.display = "none";
document.getElementById('ntfy-firewall').style.display = 'none';
document.getElementById('ntfy-localhost').style.display = 'none';
document.getElementById('ntfy-restart').style.display = 'none';
const iconResync = document.getElementsByClassName('fa-rotate');
iconResync[0].classList.remove("restart");
iconResync[0].classList.add("spin");
iconResync[0].classList.remove('restart');
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 )
{
/*
On successful restart, wait 1 second, remove dimmer, reload page in 5 seconds
*/
setTimeout( () =>
{
document.getElementById("ntfy-restart").style.display = "block"
const dimmer = document.getElementById("dimmer");
dimmer.classList.remove("dimmer-in");
dimmer.classList.add("dimmer-out");
document.getElementById('ntfy-restart').style.display = 'block'
const dimmer = document.getElementById('dimmer');
dimmer.classList.remove('dimmer-in');
dimmer.classList.add('dimmer-out');
dimmer.remove();
const iconResync = document.getElementsByClassName('fa-rotate');
iconResync[0].classList.remove("spin");
iconResync[0].classList.add("restart");
setTimeout(location.reload.bind(location), 1000);
}, 1000);
setTimeout( function()
{
const iconResync = document.getElementsByClassName('fa-rotate'); // resync favicon
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 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>
</html>