mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-04 05:15:42 -04:00
feat: add new classes structure
This commit is contained in:
168
tvapp2/classes/CLib.js
Normal file
168
tvapp2/classes/CLib.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
Compress / Uncompress String with base64
|
||||||
|
|
||||||
|
these functions use a unique character table. moving the letters around will cause strings to not
|
||||||
|
be in the correct order once uncompressed.
|
||||||
|
|
||||||
|
@usage new CLib().compress( 'https://daddylive.mp/' )
|
||||||
|
new CLib().uncompress( 'burS7u6FvUHhZfrhkfJoYz8CswTD=' )
|
||||||
|
new CLib().translate( '=', plugin.defTrans, plugin.tvaTrans )
|
||||||
|
|
||||||
|
a custom character set can be specified with two additional parameters. however, anything prior
|
||||||
|
that was encoded will not be decoded by the new character set.
|
||||||
|
|
||||||
|
const strCompress = new CLib().compress( 'test.com' );
|
||||||
|
const strUncompress = new CLib().uncompress( strCompress );
|
||||||
|
|
||||||
|
new CLib().compress( 'test.com', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'rXzxP9ZdvehYlstwiTuV1c07j45Abo2Ama6k3gqpyf8n+/NMSEIUHBQRJDLFCGKO' )
|
||||||
|
new CLib().uncompress( 'oZcUozDkAQH=', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'rXzxP9ZdvehYlstwiTuV1c07j45Abo2Ama6k3gqpyf8n+/NMSEIUHBQRJDLFCGKO' )
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import Log from './Log.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class > CLib
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CLib
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.defTrans = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
this.tvaTrans = 'TVAPp29uqXiv6g5adr1j8nfwZ0bs7Ykm3xl4hczAtoey+/CDKJULSEMBQRFGIHNO';
|
||||||
|
}
|
||||||
|
|
||||||
|
compress( data, defTrans, tvaTrans )
|
||||||
|
{
|
||||||
|
if ( typeof data === 'string' )
|
||||||
|
data = Buffer.from( data, 'utf8' );
|
||||||
|
|
||||||
|
const transDef = defTrans || this.defTrans;
|
||||||
|
const transTva = tvaTrans || this.tvaTrans;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const dataCompress = this.translate( data.toString( 'base64' ), transDef, transTva );
|
||||||
|
|
||||||
|
Log.ok( `clib`, chalk.yellow( `[compress]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Compress string` ),
|
||||||
|
chalk.blueBright( `<strRaw>` ), chalk.gray( `${ data }` ),
|
||||||
|
chalk.blueBright( `<strCompress>` ), chalk.gray( `${ dataCompress }` ) );
|
||||||
|
|
||||||
|
return dataCompress;
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `clib`, chalk.redBright( `[compress]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not compress string; bad string ${ data }` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<strCompress>` ), chalk.gray( `${ data }` ) );
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uncompress( data, defTrans, tvaTrans )
|
||||||
|
{
|
||||||
|
if ( Buffer.isBuffer( data ) )
|
||||||
|
data = data.toString();
|
||||||
|
|
||||||
|
const transDef = defTrans || this.defTrans;
|
||||||
|
const transTva = tvaTrans || this.tvaTrans;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const dataTranslated = this.translate( data, transTva, transDef );
|
||||||
|
const dataUncompress = Buffer.from( dataTranslated, 'base64' ).toString( 'utf8' );
|
||||||
|
|
||||||
|
Log.ok( `clib`, chalk.yellow( `[decompss]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Uncompress string` ),
|
||||||
|
chalk.blueBright( `<strCompress>` ), chalk.gray( `${ data }` ),
|
||||||
|
chalk.blueBright( `<strRaw>` ), chalk.gray( `${ dataUncompress }` ) );
|
||||||
|
|
||||||
|
return dataUncompress;
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `clib`, chalk.redBright( `[decompss]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not uncompress string; bad string ${ data }` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<strCompress>` ), chalk.gray( `${ data }` ) );
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Translate
|
||||||
|
|
||||||
|
compresses or decompresses encoded strings for the functions:
|
||||||
|
- compress
|
||||||
|
- uncompress
|
||||||
|
*/
|
||||||
|
|
||||||
|
translate( str, fromChars, toChars )
|
||||||
|
{
|
||||||
|
let res = '';
|
||||||
|
for ( let i = 0;i < str.length;i++ )
|
||||||
|
{
|
||||||
|
const char = str[i];
|
||||||
|
const index = fromChars.indexOf( char );
|
||||||
|
if ( index !== -1 )
|
||||||
|
res += toChars[index];
|
||||||
|
else
|
||||||
|
res += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Encode: String > Hex > Base64
|
||||||
|
|
||||||
|
encodes a human-readable string into a hex value, and then to base64
|
||||||
|
|
||||||
|
@usage const clib = new CLib()
|
||||||
|
const encoded = clib.encodeToHexBase64('hello'); // Njg2NTZjNmM2Zg==
|
||||||
|
const decoded = clib.decodeFromHexBase64(`${ encoded }`); // hello
|
||||||
|
*/
|
||||||
|
|
||||||
|
encodeToHexBase64( str )
|
||||||
|
{
|
||||||
|
const hex = [...str].map( char =>
|
||||||
|
{
|
||||||
|
const code = char.charCodeAt(0).toString(16);
|
||||||
|
return code.padStart(2, '0');
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const base64 = btoa(hex);
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decode: Base64 > Hex > String
|
||||||
|
|
||||||
|
decodes a base64 value to hex, and then back into a human readable string
|
||||||
|
|
||||||
|
@usage const clib = new CLib()
|
||||||
|
const encoded = clib.encodeToHexBase64('hello'); // Njg2NTZjNmM2Zg==
|
||||||
|
const decoded = clib.decodeFromHexBase64(`${ encoded }`); // hello
|
||||||
|
*/
|
||||||
|
|
||||||
|
decodeFromHexBase64( base64Str )
|
||||||
|
{
|
||||||
|
const hex = atob(base64Str);
|
||||||
|
const chars = hex.match(/.{1,2}/g); // every 2 hex chars = 1 byte
|
||||||
|
|
||||||
|
return chars.map(byte => String.fromCharCode(parseInt(byte, 16))).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@usage import CLib from './classes/CLib.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default CLib;
|
||||||
120
tvapp2/classes/Log.js
Normal file
120
tvapp2/classes/Log.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
Define > Logs
|
||||||
|
|
||||||
|
When assigning text colors, terminals and the windows command prompt can display any color; however apps
|
||||||
|
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
|
||||||
|
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
|
||||||
|
|
||||||
|
Various levels of logs with the following usage:
|
||||||
|
Log.verbose(`This is verbose`)
|
||||||
|
Log.debug(`This is debug`)
|
||||||
|
Log.info(`This is info`)
|
||||||
|
Log.ok(`This is ok`)
|
||||||
|
Log.notice(`This is notice`)
|
||||||
|
Log.warn(`This is warn`)
|
||||||
|
Log.error(
|
||||||
|
`Error fetching sports data with error:`,
|
||||||
|
chalk.white(`→`),
|
||||||
|
chalk.grey(`This is the error message`)
|
||||||
|
);
|
||||||
|
|
||||||
|
Level Type
|
||||||
|
-----------------------------------
|
||||||
|
6 Trace
|
||||||
|
5 Debug
|
||||||
|
4 Info
|
||||||
|
3 Notice
|
||||||
|
2 Warn
|
||||||
|
1 Error
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
/*
|
||||||
|
chalk.level
|
||||||
|
|
||||||
|
@ref https://npmjs.com/package/chalk
|
||||||
|
- 0 All colors disabled
|
||||||
|
- 1 Basic color support (16 colors)
|
||||||
|
- 2 256 color support
|
||||||
|
- 3 Truecolor support (16 million colors)
|
||||||
|
|
||||||
|
When assigning text colors, terminals and the windows command prompt can display any color; however apps
|
||||||
|
such as Portainer console cannot. If you use 16 million colors and are viewing console in Portainer, colors will
|
||||||
|
not be the same as the rgb value. It's best to just stick to Chalk's default colors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
chalk.level = 3;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LOG_LEVEL = process.env.LOG_LEVEL || 4;
|
||||||
|
const { name } = JSON.parse( fs.readFileSync( './package.json' ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class > Log
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Log
|
||||||
|
{
|
||||||
|
static now()
|
||||||
|
{
|
||||||
|
const now = new Date();
|
||||||
|
return chalk.gray( `[${ now.toLocaleTimeString() }]` );
|
||||||
|
}
|
||||||
|
|
||||||
|
static verbose( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 6 )
|
||||||
|
console.debug( chalk.white.bgBlack.blackBright.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static debug( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 7 )
|
||||||
|
console.trace( chalk.white.bgMagenta.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.magentaBright( msg.join( ' ' ) ) );
|
||||||
|
else if ( LOG_LEVEL >= 5 )
|
||||||
|
console.debug( chalk.white.bgGray.bold( ` ${ name } ` ), chalk.white( `⚙️` ), this.now(), chalk.gray( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static info( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 4 )
|
||||||
|
console.info( chalk.white.bgBlueBright.bold( ` ${ name } ` ), chalk.white( `ℹ️` ), this.now(), chalk.blueBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static ok( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 4 )
|
||||||
|
console.log( chalk.white.bgGreen.bold( ` ${ name } ` ), chalk.white( `✅` ), this.now(), chalk.greenBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static notice( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 3 )
|
||||||
|
console.log( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `📌` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static warn( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 2 )
|
||||||
|
console.warn( chalk.white.bgYellow.bold( ` ${ name } ` ), chalk.white( `⚠️` ), this.now(), chalk.yellowBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static error( ...msg )
|
||||||
|
{
|
||||||
|
if ( LOG_LEVEL >= 1 )
|
||||||
|
console.error( chalk.white.bgRedBright.bold( ` ${ name } ` ), chalk.white( `❌` ), this.now(), chalk.redBright( msg.join( ' ' ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@usage import Log from './classes/Log.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default Log;
|
||||||
46
tvapp2/classes/Semaphore.js
Normal file
46
tvapp2/classes/Semaphore.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
Semaphore > Declare
|
||||||
|
|
||||||
|
allows multiple threads to work with the same shared resources
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Semaphore
|
||||||
|
{
|
||||||
|
constructor( max )
|
||||||
|
{
|
||||||
|
this.max = max;
|
||||||
|
this.queue = [];
|
||||||
|
this.active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquire()
|
||||||
|
{
|
||||||
|
if ( this.active < this.max )
|
||||||
|
{
|
||||||
|
this.active++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise( ( resolve ) => this.queue.push( resolve ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
release()
|
||||||
|
{
|
||||||
|
this.active--;
|
||||||
|
if ( this.queue.length > 0 )
|
||||||
|
{
|
||||||
|
const resolve = this.queue.shift();
|
||||||
|
this.active++;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@usage import Log from './classes/Log.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default Semaphore;
|
||||||
|
|
||||||
520
tvapp2/classes/Storage.js
Normal file
520
tvapp2/classes/Storage.js
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
/*
|
||||||
|
Class › Storage
|
||||||
|
|
||||||
|
The storage classes allows you to save specific settings into a json file. These settings are better off being stored in
|
||||||
|
a local file, instead of using up the resources being saved in a database.
|
||||||
|
|
||||||
|
Class supports multiple storage files, but by default, it will save settings in `www/config.json`.
|
||||||
|
|
||||||
|
Settings include Tuner / HDHomeRun device information, etc.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage( envWebFolder, FILE_CFG );
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import path from 'path';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import fs from 'fs';
|
||||||
|
import Log from './Log.js';
|
||||||
|
import Utils from './Utils.js';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
/*
|
||||||
|
CJS › ESM
|
||||||
|
*/
|
||||||
|
|
||||||
|
const __filename = fileURLToPath( import.meta.url ); // get resolved path to file
|
||||||
|
const __dirname = path.dirname( __filename ); // get name of directory
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class › Storage
|
||||||
|
|
||||||
|
constructor ( str:folder, str:file )
|
||||||
|
Initialize ( bool:bForceNew )
|
||||||
|
Setup ( bool:bForceNew )
|
||||||
|
Get ( str:key )
|
||||||
|
Set ( str:key, any:value )
|
||||||
|
Save ( )
|
||||||
|
GetConfig ( )
|
||||||
|
isJsonString ( json:str )
|
||||||
|
isJsonEmpty ( obj:json )
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Storage
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Constructor › Storage
|
||||||
|
|
||||||
|
Initializes a Storage instance for managing the config.json file.
|
||||||
|
Determines the full path to the config file based on folder and file arguments,
|
||||||
|
or uses the default static fileConfig if none are provided.
|
||||||
|
|
||||||
|
Handles Node.js packaged apps (process.pkg) by adjusting paths accordingly.
|
||||||
|
|
||||||
|
@args
|
||||||
|
folder (str) Optional folder where config.json will be stored. Defaults to 'www'.
|
||||||
|
file (str) Optional config file name. Defaults to static Storage.fileConfig.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage(envWebFolder, FILE_CFG);
|
||||||
|
*/
|
||||||
|
|
||||||
|
static fileConfig = path.resolve( process.cwd( ), 'www', 'config.json' );
|
||||||
|
|
||||||
|
constructor( folder, file )
|
||||||
|
{
|
||||||
|
this.folderWeb = folder || 'www';
|
||||||
|
this.fileConfig = file ? path.resolve( folder, file ) : Storage.fileConfig;
|
||||||
|
|
||||||
|
if ( process.pkg )
|
||||||
|
this.fileConfig = path.join( path.dirname( process.execPath ), this.folderWeb, this.fileConfig );
|
||||||
|
else
|
||||||
|
this.fileConfig = path.resolve( process.cwd( ), this.folderWeb, this.fileConfig );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize › Activate Config Setup with Logging
|
||||||
|
|
||||||
|
Activates the Storage.Setup( ) function while providing detailed logging.
|
||||||
|
Ensures the user's config.json file exists, is valid, and is initialized
|
||||||
|
with default values if missing or corrupt.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Logs the start of initialization.
|
||||||
|
- Calls Setup( ) with optional force flag to recreate config.
|
||||||
|
- Catches and logs any errors during setup.
|
||||||
|
|
||||||
|
@args
|
||||||
|
bForceNew (bool) Optional. If true, forces the config file to be removed
|
||||||
|
and regenerated from defaults.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(Promise) Resolves when initialization completes, or logs an error if setup fails.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage(envWebFolder, FILE_CFG);
|
||||||
|
await storage.Initialize(false);
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Initialize( bForceNew )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const bForce = bForceNew || false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.info( `conf`, chalk.yellow( `[initiate]` ), chalk.white( `ℹ️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Initializing config file` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
await new Storage( ).Setup( bForce );
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
console.log( 'Error writing Metadata.json:' + err.message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize › Setup User Config File
|
||||||
|
|
||||||
|
Sets up a user's config.json file, ensuring it exists and is valid JSON.
|
||||||
|
If the file is missing, empty, or invalid, it will be created or replaced.
|
||||||
|
Typically, you should call this via Storage( ).Initialize( ) rather than Setup( ) directly.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Creates parent directory if it doesn't exist.
|
||||||
|
- Removes existing config if bForceNew is true.
|
||||||
|
- Validates existing JSON; backs up invalid files.
|
||||||
|
- Creates default config if missing.
|
||||||
|
- Wires up nconf with argv, env, file, and default values.
|
||||||
|
|
||||||
|
@args
|
||||||
|
bForceNew (bool) Optional flag to force recreate the config file, wiping all existing data.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(Promise) Resolves true when initialization completes successfully.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage = new Storage(envWebFolder, FILE_CFG);
|
||||||
|
await storage.Initialize(false);
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Setup( bForceNew )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return new Promise( ( resolve, reject ) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.info( `conf`, chalk.yellow( `[generate]` ), chalk.white( `ℹ️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Initializing storage setup` ),
|
||||||
|
chalk.blueBright( `<force>` ), chalk.gray( `${ bForceNew }` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
ensure parent directory exists
|
||||||
|
*/
|
||||||
|
const dirPath = path.dirname( this.fileConfig );
|
||||||
|
|
||||||
|
if ( !fs.existsSync( dirPath ) )
|
||||||
|
{
|
||||||
|
fs.mkdirSync( dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if force flag is true, remove existing config file (force)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( bForceNew === true && fs.existsSync( this.fileConfig ) )
|
||||||
|
{
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[generate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Remove original config; force new` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.unlinkSync( this.fileConfig );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Failed to unlink existing config` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ e.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if config exists, validate JSON; if invalid, move to backup and recreate
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( fs.existsSync( this.fileConfig ) )
|
||||||
|
{
|
||||||
|
let raw = null;
|
||||||
|
let parsed = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
raw = fs.readFileSync( this.fileConfig, { encoding: 'utf8' });
|
||||||
|
|
||||||
|
if ( typeof raw !== 'string' || raw.trim( ).length === 0 )
|
||||||
|
{
|
||||||
|
throw new Error( 'Empty config file' );
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = JSON.parse( raw );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
const backupPath = `${ this.fileConfig }.corrupt.${ Date.now( ) }`;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.renameSync( this.fileConfig, backupPath );
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Config file invalid; moved to backup` ),
|
||||||
|
chalk.redBright( `<backup>` ), chalk.gray( `${ backupPath }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
}
|
||||||
|
catch ( renameErr )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Unable to backup invalid config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ renameErr.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
if ( this.rejected )
|
||||||
|
{
|
||||||
|
reject( renameErr );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if config does not exist (or was just moved because it was corrupt), create it atomically
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !fs.existsSync( this.fileConfig ) )
|
||||||
|
{
|
||||||
|
const defaults =
|
||||||
|
{
|
||||||
|
deviceId: 'FFFFFFFF'
|
||||||
|
};
|
||||||
|
|
||||||
|
const tempPath = `${ this.fileConfig }.tmp`;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs.writeFileSync( tempPath, JSON.stringify( defaults, null, 4 ), { encoding: 'utf8' });
|
||||||
|
fs.renameSync( tempPath, this.fileConfig );
|
||||||
|
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[generate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Created new config file with defaults` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
}
|
||||||
|
catch ( writeErr )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Failed to create config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ writeErr.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
if ( this.rejected )
|
||||||
|
{
|
||||||
|
reject( writeErr );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
now that file exists and is valid JSON, wire up nconf
|
||||||
|
*/
|
||||||
|
|
||||||
|
nconf.argv( ).env({ parseValues: true }).file({ file: this.fileConfig }).defaults(
|
||||||
|
{
|
||||||
|
deviceId: 'FFFFFFFF'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not generate and write to new config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ this.fileConfig }` ) );
|
||||||
|
|
||||||
|
if ( this.rejected )
|
||||||
|
{
|
||||||
|
reject( err );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve( true );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get › Retrieve Configuration Value
|
||||||
|
|
||||||
|
Fetches a stored value from the application's persistent configuration
|
||||||
|
using the provided key via the nconf module.
|
||||||
|
|
||||||
|
This function is static, so it can be called without creating a Storage instance.
|
||||||
|
|
||||||
|
@args
|
||||||
|
key (str) The configuration key to retrieve.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(any) The value associated with the key, or undefined if the key does not exist.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const deviceId = Storage.Get('deviceId');
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Get( key )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return nconf.get( key );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set › Store Configuration Value
|
||||||
|
|
||||||
|
Stores a value in the application's persistent configuration using
|
||||||
|
the provided key via the nconf module. Automatically saves the
|
||||||
|
updated configuration to disk by calling Storage.Save( ).
|
||||||
|
|
||||||
|
This function is static, so it can be called without creating a Storage instance.
|
||||||
|
|
||||||
|
@args
|
||||||
|
key (str) The configuration key to set.
|
||||||
|
value (any) The value to store under the specified key.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(void) No return value.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
Storage.Set('deviceId', '105B35EF');
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Set( key, value )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ),
|
||||||
|
chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
nconf.set( key, value );
|
||||||
|
Storage.Save( );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Save › Persist Configuration to Disk
|
||||||
|
|
||||||
|
Saves the current configuration stored in nconf to disk.
|
||||||
|
After saving, the method reads back the file to verify it is valid JSON
|
||||||
|
and logs detailed status messages about success or errors.
|
||||||
|
|
||||||
|
@purpose
|
||||||
|
- Calls nconf.save() to write the current configuration.
|
||||||
|
- Reads back the saved file.
|
||||||
|
- Parses the file as JSON to confirm validity.
|
||||||
|
- Logs success or detailed error messages for failures.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(void) Logs success or error; does not return a value.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
Storage.Save();
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Save( )
|
||||||
|
{
|
||||||
|
const filePath = this.fileConfig;
|
||||||
|
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ),
|
||||||
|
chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
nconf.save( ( err ) =>
|
||||||
|
{
|
||||||
|
if ( err )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[snapshot]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Could not save config` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile( filePath, ( err, data ) =>
|
||||||
|
{
|
||||||
|
if ( err )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[snapshot]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Unable to read config file` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const parsed = JSON.parse( data.toString( ) );
|
||||||
|
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[snapshot]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `Save to config file successful` ),
|
||||||
|
chalk.greenBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
|
||||||
|
Log.debug( `conf`, chalk.yellow( `[snapshot]` ), chalk.white( `⚙️` ),
|
||||||
|
chalk.blueBright( `<msg>` ), chalk.gray( `Read values from saved config file` ),
|
||||||
|
chalk.blueBright( `<file>` ), chalk.gray( `${ filePath }` ),
|
||||||
|
chalk.blueBright( `<values>` ), chalk.gray( `${ JSON.stringify( parsed ) }` ) );
|
||||||
|
}
|
||||||
|
catch ( parseErr )
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[snapshot]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Config file is not valid JSON` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ parseErr.message }` ),
|
||||||
|
chalk.redBright( `<file>` ), chalk.gray( `${ filePath }` ) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetConfig › Return Full Path to Config File
|
||||||
|
|
||||||
|
Returns the full path to the currently used config.json file for this Storage instance.
|
||||||
|
This is useful when you need to know the exact file location without reading its contents.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) Absolute path to the config.json file.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const storage_config = Storage.GetConfig();
|
||||||
|
*/
|
||||||
|
|
||||||
|
static GetConfig( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return this.fileConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
isJsonString › Check if Input is Valid JSON
|
||||||
|
|
||||||
|
Determines whether a given string is valid JSON by attempting
|
||||||
|
to parse it. Returns true if parsing succeeds, false if it throws
|
||||||
|
an error.
|
||||||
|
|
||||||
|
@args
|
||||||
|
json (str) The string to test for valid JSON.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(bool) True if input is valid JSON, false otherwise.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const valid = Storage.isJsonString('{"key":"value"}'); // returns true
|
||||||
|
*/
|
||||||
|
|
||||||
|
static isJsonString( json )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JSON.parse( json );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
helper › json object empty
|
||||||
|
*/
|
||||||
|
|
||||||
|
static isJsonEmpty( json )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
if ( Object.keys( json ).length === 0 )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ( JSON.stringify( json ) === '\"{}\"' )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for ( const key in json )
|
||||||
|
{
|
||||||
|
if ( ! Object.prototype.hasOwnProperty.call( json, key ) )
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@import
|
||||||
|
import Storage from './classes/Storage.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Storage;
|
||||||
453
tvapp2/classes/Tuner.js
Normal file
453
tvapp2/classes/Tuner.js
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
/*
|
||||||
|
Class › Tuner
|
||||||
|
|
||||||
|
Handles HDHomeRun device management and deviceId lifecycle.
|
||||||
|
|
||||||
|
@purpose
|
||||||
|
- Generate / format HDHomeRun device IDs.
|
||||||
|
- Validate device IDs against HDHomeRun rules (length, hex chars, checksum).
|
||||||
|
- Persist device IDs using Storage class.
|
||||||
|
- Automatically generate new device ID if missing, invalid, or uninitialized (FFFFFFFF).
|
||||||
|
- Initialize tuner instances with validated device IDs.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
await new Tuner( Storage.Get( 'deviceId' ) ).Initialize( );
|
||||||
|
const tuner = new Tuner( );
|
||||||
|
await tuner.Initialize( );
|
||||||
|
const validId = await tuner.VerifyDeviceId( );
|
||||||
|
|
||||||
|
@notes
|
||||||
|
- Device IDs are persisted via the Storage class (config.json).
|
||||||
|
- User's device id must be valid before HDHomeRun will initialize.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import Storage from './Storage.js';
|
||||||
|
import Utils from './Utils.js';
|
||||||
|
import Log from './Log.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class › Tuner
|
||||||
|
|
||||||
|
constructor ( str:deviceId )
|
||||||
|
Initialize ( )
|
||||||
|
Start ( )
|
||||||
|
_GenerateDeviceId ( int:len )
|
||||||
|
GenerateDeviceId ( )
|
||||||
|
GetDeviceId ( )
|
||||||
|
FormatDeviceId ( str:deviceid )
|
||||||
|
IsDeviceIdValid ( )
|
||||||
|
VerifyDeviceId ( )
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Tuner
|
||||||
|
{
|
||||||
|
constructor( deviceId )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getConstructorName( ) }` ) );
|
||||||
|
|
||||||
|
this.Name = `HDHomeRun`;
|
||||||
|
this.FriendlyName = `TVApp2`;
|
||||||
|
this.ModelNumber = `HDHR5-4US`;
|
||||||
|
this.FirmwareName = `hdhomerun5_atsc`;
|
||||||
|
this.FirmwareVersion = `0.9.15.00-RC04`;
|
||||||
|
this.DeviceId = deviceId || Storage.Get( 'deviceId' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize › Setup and Start Tuner
|
||||||
|
|
||||||
|
Initializes the tuner by calling the Start( ) method.
|
||||||
|
Catches and logs any errors encountered during startup.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(void) Logs status; does not return a value.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
await tuner.Initialize( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Initialize( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.Start( );
|
||||||
|
}
|
||||||
|
catch ( err )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[initiate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Failure initializing tuner` ),
|
||||||
|
chalk.redBright( `<error>` ), chalk.gray( `${ err.message }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Start › Initialize and Verify Device ID
|
||||||
|
|
||||||
|
Starts the tuner by verifying the current deviceId.
|
||||||
|
If the deviceId is missing or invalid, it will be regenerated and validated.
|
||||||
|
Logs the status of the deviceId once verification completes.
|
||||||
|
|
||||||
|
@args
|
||||||
|
none
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(bool) true if deviceId is valid after verification, false otherwise.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
await tuner.Start( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async Start( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const verifiedId = await new Tuner( ).VerifyDeviceId( this.DeviceId );
|
||||||
|
|
||||||
|
if ( await this.IsDeviceIdValid( verifiedId ) )
|
||||||
|
{
|
||||||
|
Log.ok( `conf`, chalk.yellow( `[validate]` ), chalk.white( `✅` ),
|
||||||
|
chalk.greenBright( `<msg>` ), chalk.gray( `User has valid deviceId` ),
|
||||||
|
chalk.greenBright( `<deviceId>` ), chalk.gray( `${ verifiedId }` ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_GenerateDeviceId › Generate Raw Random Hexadecimal String
|
||||||
|
|
||||||
|
Generates a raw random hexadecimal string using Node.js crypto module.
|
||||||
|
This is typically used as the random portion of a deviceId.
|
||||||
|
|
||||||
|
@args
|
||||||
|
len (int) Optional number of bytes to generate. Defaults to 4 bytes.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) Uppercase hexadecimal string, length = len * 2 characters.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const randomHex = Tuner._GenerateDeviceId( 4 ); // 8-character hex string
|
||||||
|
*/
|
||||||
|
|
||||||
|
static _GenerateDeviceId( len )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return crypto.randomBytes( len || 4 ).toString( 'hex' ).toUpperCase( );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GenerateDeviceId › Generate New HDHomeRun Device ID
|
||||||
|
|
||||||
|
Generates a new, properly formatted HDHomeRun deviceId.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Generates 4 random hexadecimal characters.
|
||||||
|
- Prepends '105' and appends '0' to form base deviceId.
|
||||||
|
- Passes baseId to Tuner.FormatDeviceId( ) to ensure correct checksum and 8-character format.
|
||||||
|
|
||||||
|
@args
|
||||||
|
None
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) A valid, 8-character HDHomeRun deviceId in uppercase hexadecimal.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const newDeviceId = Tuner.GenerateDeviceId( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
static GenerateDeviceId( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const chars = '0123456789ABCDEF';
|
||||||
|
let randomHex = '';
|
||||||
|
|
||||||
|
// generate 4 random hexadecimal chars
|
||||||
|
for ( let i = 0;i < 4;i++ )
|
||||||
|
{
|
||||||
|
randomHex += chars[Math.floor( Math.random( ) * chars.length )];
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseId = '105' + randomHex + '0';
|
||||||
|
return this.FormatDeviceId( baseId );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetDeviceId › Retrieve Stored HDHomeRun Device ID
|
||||||
|
|
||||||
|
Fetches the current deviceId from persistent storage (via Storage.Get).
|
||||||
|
|
||||||
|
@args
|
||||||
|
None
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) The current deviceId stored in configuration.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const deviceId = await tuner.GetDeviceId( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async GetDeviceId( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
return Storage.Get( 'deviceId' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FormatDeviceId › Validate and Format HDHomeRun Device ID
|
||||||
|
|
||||||
|
Fetches the provided deviceId (or instance default) and ensures it is valid
|
||||||
|
according to HDHomeRun rules, then returns a properly formatted ID.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
- Input must be exactly 8 hexadecimal characters.
|
||||||
|
- All characters must be 0-9 or A-F/a-f.
|
||||||
|
- Computes checksum using HDHomeRun-specific lookup table.
|
||||||
|
- Generates a new deviceId integer with checksum applied.
|
||||||
|
- Converts back to 8-character uppercase hexadecimal string.
|
||||||
|
|
||||||
|
Logs detailed errors if the input deviceId is invalid.
|
||||||
|
|
||||||
|
@args
|
||||||
|
deviceid (str) Optional deviceId to format. Defaults to instance deviceId.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str|int) Formatted 8-character hex deviceId, or 0 if input invalid.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const formattedId = Tuner.FormatDeviceId( someDeviceId );
|
||||||
|
*/
|
||||||
|
|
||||||
|
static FormatDeviceId( deviceid )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const deviceId = deviceid || this.DeviceId;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Validate input length
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !deviceId || deviceId.length !== 8 )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must be 8 hexadecimals` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
All chars should be valid hexadecimal
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hexPattern = /^[0-9A-Fa-f]+$/;
|
||||||
|
if ( !hexPattern.test( deviceId ) )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must contain all hex (0-9, A-F, a-f)` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hex string to integer
|
||||||
|
*/
|
||||||
|
|
||||||
|
const deviceIdInt = parseInt( deviceId, 16 );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checksum lookup table
|
||||||
|
*/
|
||||||
|
|
||||||
|
const checksumLookup =
|
||||||
|
[
|
||||||
|
0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calc checksum
|
||||||
|
*/
|
||||||
|
|
||||||
|
let checksum = 0;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 28 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >> 24 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 20 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >> 16 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 12 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >> 8 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >> 4 ) & 0x0F];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calc new device ID
|
||||||
|
*/
|
||||||
|
|
||||||
|
const newDevId = ( deviceIdInt & 0xFFFFFFF0 ) + checksum;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert back to hex string; ensure we get 8 characters with leading zeros; convert to uppercase
|
||||||
|
*/
|
||||||
|
|
||||||
|
return newDevId.toString( 16 ).toUpperCase( ).padStart( 8, '0' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IsDeviceIdValid › Validate HDHomeRun Device ID
|
||||||
|
|
||||||
|
Checks if the current deviceId on this instance is valid according to HDHomeRun rules.
|
||||||
|
|
||||||
|
Validation steps:
|
||||||
|
- Must be exactly 8 characters long.
|
||||||
|
- All characters must be hexadecimal (0-9, A-F, a-f).
|
||||||
|
- Computes checksum using HDHomeRun-specific lookup table; must equal 0.
|
||||||
|
|
||||||
|
Logs detailed errors if the deviceId fails any validation step.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(bool) true if deviceId is valid, false otherwise.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const isValid = await tuner.IsDeviceIdValid( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async IsDeviceIdValid( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define Hexadecimal charset (0-9, A-F, a-f)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hexDigits = new Set( '0123456789ABCDEFabcdef' );
|
||||||
|
const deviceId = this.DeviceId;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Check if device ID is exactly 8 characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !deviceId || deviceId.length !== 8 )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must be 8 hexadecimals` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Check if all characters are hexadecimal
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( !Array.from( deviceId ).every( ( c ) => hexDigits.has( c ) ) )
|
||||||
|
{
|
||||||
|
Log.error( `hdhr`, chalk.redBright( `[validate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `HDHomeRun deviceId must contain all hex (0-A)` ),
|
||||||
|
chalk.redBright( `<deviceId>` ), chalk.gray( `${ deviceId }` ) );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert hex string to integer (equivalent to int.from_bytes with big endian)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const deviceIdInt = parseInt( deviceId, 16 );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checksum lookup table
|
||||||
|
*/
|
||||||
|
|
||||||
|
const checksumLookup =
|
||||||
|
[
|
||||||
|
0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calc checksum
|
||||||
|
*/
|
||||||
|
|
||||||
|
let checksum = 0;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 28 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 24 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 20 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 16 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 12 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 8 ) & 0x0F;
|
||||||
|
checksum ^= checksumLookup[( deviceIdInt >>> 4 ) & 0x0F];
|
||||||
|
checksum ^= ( deviceIdInt >>> 0 ) & 0x0F;
|
||||||
|
|
||||||
|
return checksum === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
VerifyDeviceId › Validate / Generate Device ID
|
||||||
|
|
||||||
|
Checks if the current deviceId on this instance is valid.
|
||||||
|
|
||||||
|
If missing, uninitialized ('FFFFFFFF'), or fails validation:
|
||||||
|
a new deviceId is generated via the static Tuner.GenerateDeviceId( ) method.
|
||||||
|
|
||||||
|
New deviceId is saved to persistent storage via Storage.Set( ) and
|
||||||
|
updated on the instance.
|
||||||
|
|
||||||
|
Function also recursively verifies until a valid deviceId is established.
|
||||||
|
|
||||||
|
@returns
|
||||||
|
(str) A valid deviceId for this tuner instance.
|
||||||
|
|
||||||
|
@usage
|
||||||
|
const validId = await tuner.VerifyDeviceId( );
|
||||||
|
*/
|
||||||
|
|
||||||
|
async VerifyDeviceId( )
|
||||||
|
{
|
||||||
|
Log.verbose( `func`, chalk.yellow( `[executed]` ), chalk.white( `📣` ), chalk.blueBright( `<name>` ), chalk.gray( `${ Utils.getFuncName( ) }` ) );
|
||||||
|
|
||||||
|
const deviceId = this.DeviceId;
|
||||||
|
|
||||||
|
if ( !deviceId || deviceId === 'FFFFFFFF' || !await this.IsDeviceIdValid( ) )
|
||||||
|
{
|
||||||
|
const deviceIdNew = Tuner.GenerateDeviceId( ); // static generates a properly formatted ID
|
||||||
|
if ( deviceId === 'FFFFFFFF' )
|
||||||
|
{
|
||||||
|
Log.info( `conf`, chalk.yellow( `[generate]` ), chalk.white( `📣` ),
|
||||||
|
chalk.yellow( `<msg>` ), chalk.gray( `Generating HDHomeRun deviceId for the first time` ),
|
||||||
|
chalk.yellow( `<deviceId>` ), chalk.gray( `${ deviceIdNew }` ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.error( `conf`, chalk.redBright( `[generate]` ), chalk.white( `❌` ),
|
||||||
|
chalk.redBright( `<msg>` ), chalk.gray( `Invalid deviceId; generating new` ),
|
||||||
|
chalk.redBright( `<oldDeviceId>` ), chalk.gray( `${ deviceId }` ),
|
||||||
|
chalk.redBright( `<deviceIdNew>` ), chalk.gray( `${ deviceIdNew }` ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.Set( 'deviceId', deviceIdNew ); // save to JSON via nconf
|
||||||
|
this.DeviceId = deviceIdNew; // update the instance so validation works
|
||||||
|
|
||||||
|
// verify recursively until valid
|
||||||
|
const verifiedId = await this.VerifyDeviceId( );
|
||||||
|
return verifiedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export class
|
||||||
|
|
||||||
|
@image
|
||||||
|
import Tuner from './classes/Tuner.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Tuner;
|
||||||
48
tvapp2/classes/Utils.js
Normal file
48
tvapp2/classes/Utils.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
class Utils
|
||||||
|
{
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns the name of the function that this function was called from.
|
||||||
|
used for Log.verbose
|
||||||
|
*/
|
||||||
|
|
||||||
|
static getFuncName()
|
||||||
|
{
|
||||||
|
return (new Error()).stack.match(/at (\S+)/g)[1].slice(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns the name of the constructor that this function was called from.
|
||||||
|
used for Log.verbose
|
||||||
|
*/
|
||||||
|
|
||||||
|
static getConstructorName()
|
||||||
|
{
|
||||||
|
return (new Error()).stack.match(/new\s+(\w+)/g)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
helper > str2bool
|
||||||
|
*/
|
||||||
|
|
||||||
|
static str2bool( str )
|
||||||
|
{
|
||||||
|
if ( typeof str === 'string' )
|
||||||
|
{
|
||||||
|
const lower = str.toLowerCase();
|
||||||
|
if ([
|
||||||
|
'1', 'true', 'yes', 'y', 't'
|
||||||
|
].includes( lower ) )
|
||||||
|
str = true;
|
||||||
|
if ([
|
||||||
|
'0', 'false', 'no', 'n', 'f'
|
||||||
|
].includes( lower ) )
|
||||||
|
str = false;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
else return Boolean( str );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Utils;
|
||||||
Reference in New Issue
Block a user