mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-07 08:55:42 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0690e1551b
|
|||
|
d1a7460c05
|
|||
|
70e349d7e3
|
|||
|
568c3fc219
|
|||
|
f55ecae8f3
|
|||
|
e4436ad7b7
|
|||
|
67d7019a93
|
|||
|
4b45c0a2a2
|
|||
|
b3aae7b837
|
|||
|
122286bd7b
|
|||
|
6708bb17a3
|
|||
|
5fa7cd9d85
|
|||
|
25ac27dd64
|
|||
|
a659e03512
|
@@ -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 }}
|
||||
|
||||
|
||||
2
.github/workflows/deploy-docker-gitea.yml
vendored
2
.github/workflows/deploy-docker-gitea.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
2
.github/workflows/deploy-docker-github.yml
vendored
2
.github/workflows/deploy-docker-github.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
301
tvapp2/index.js
301
tvapp2/index.js
@@ -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 } ` ) );
|
||||
});
|
||||
})();
|
||||
|
||||
2
tvapp2/node_modules/.package-lock.json
generated
vendored
2
tvapp2/node_modules/.package-lock.json
generated
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tvapp2",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
4
tvapp2/package-lock.json
generated
4
tvapp2/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
96
tvapp2/www/css/tvapp2.min.css
vendored
96
tvapp2/www/css/tvapp2.min.css
vendored
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user