feat: add new classes structure

This commit is contained in:
2025-09-30 22:49:37 -07:00
parent 259d27a2ce
commit 2a09bc1ea3
6 changed files with 1355 additions and 0 deletions

168
tvapp2/classes/CLib.js Normal file
View 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
View 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;

View 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
View 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
View 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
View 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;