/**
* Main entry for `utils` module of **YTMP3-JS** project.
*
* This module provides a set of submodules for working with various utilities.
* These submodules are:
*
* - {@link module:utils/logger} - A submodule for logging process.
* - {@link module:utils/yt-urlfmt} - A submodule containing all supported YouTube URLs in regular expressions (_deprecated_).
* - {@link module:utils/url-utils} - A submodule for working with YouTube URLs.
* - {@link module:utils/type-utils} - A submodule for type checking and utility functions.
*
* @module utils
* @requires utils/logger
* @requires utils/yt-urlfmt
* @requires utils/url-utils
* @requires utils/type-utils
* @author Ryuu Mitsuki <https://github.com/mitsuki31>
* @license MIT
* @since 1.0.0
*/
/**
* Options object for configuring the progress bar.
*
* @typedef {Object} ProgressBarOptions
* @property {'auto' | number} [barWidth='auto'] - The width of the progress bar. Can be `'auto'`
* or a number representing the number of characters.
* @property {string} [barCharElapsed='#'] - The character used to represent the progress of the bar.
* @property {string} [barCharTotal='-'] - The character used to represent the total length of the bar.
* @property {boolean} [bytesInfo=true] - Whether to display the bytes downloaded information in MB.
* @since 1.0.0
*/
/**
* The resolved {@link module:utils~ProgressBarOptions ProgressBarOptions} options.
*
* @typedef {ProgressBarOptions} ResolvedProgressBarOptions
* @property {'auto' | number} barWidth - The width of the progress bar. Can be `'auto'`
* or a number representing the number of characters.
* @property {string} barCharElapsed - The character used to represent the progress of the bar.
* @property {string} barCharTotal - The character used to represent the total length of the bar.
* @property {boolean} bytesInfo - Whether to display the bytes downloaded information in MB.
* @since 1.0.0
*/
'use strict';
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
// Local imports
const Logger = require('./logger');
const TypeUtils = require('./type-utils');
const URLFmt = require('./yt-urlfmt');
const URLUtils = require('./url-utils');
// region Constants
/**
* The root directory of the project.
* @type {string}
* @constant
* @package
* @since 1.0.0
*/
const ROOTDIR = path.join(__dirname, '..', '..');
/**
* The output directory for the downloaded audio files.
* @type {string}
* @constant
* @package
* @since 1.0.0
*/
const OUTDIR = path.join(ROOTDIR, 'download');
/**
* The home directory path for the YTMP3-JS configuration and data files.
*
* This path is constructed by joining the user's home directory with the `'.ytmp3-js'` folder.
* On POSIX systems, this will typically be `"/home/<USERNAME>/.ytmp3-js"`, while on Windows systems,
* it will be `"C:\Users\<USERNAME>\.ytmp3-js"`. For Termux Android, it will be
* `"/data/data/com.termux/files/home/.ytmp3-js"`.
*
* @type {string}
* @constant
* @package
* @since 1.1.0
*/
const YTMP3_HOMEDIR = path.join(os.homedir(), '.ytmp3-js');
/**
* The log directory for error logs.
*
* Basically, the log directory is set to:
* - **POSIX**: `$HOME/.ytmp3-js/logs`
* - **Windows**: `%USERPROFILE%\.ytmp3-js\logs`
*
* @type {string}
* @constant
* @package
* @since 1.0.0
*/
const LOGDIR = path.join(YTMP3_HOMEDIR, 'logs');
// region Utilities Function
/**
* Synchronously checks whether the specified directory path is exist,
* creates new if not exist with asynchronous operation.
*
* @param {string} dirpath - The directory path to be created if not exist.
* @returns {Promise<void>}
*
* @async
* @package
* @since 1.0.0
*/
async function createDirIfNotExist(dirpath) {
if (!fs.existsSync(dirpath)) await fs.promises.mkdir(dirpath, { recursive: true });
}
/**
* Similar with {@link module:utils~createDirIfNotExist `createDirIfNotExist`}
* function, but it uses synchronous directory creation.
*
* @package
* @since 1.0.1
*/
function createDirIfNotExistSync(dirpath) {
if (!fs.existsSync(dirpath)) fs.mkdirSync(dirpath, { recursive: true });
}
/**
* Creates the error log file with the specified prefix.
* If the prefix is not specified, it will use `'ytmp3Error'` as the prefix.
*
* @param {string} [prefix='ytmp3Error'] - The prefix of the error log file.
*
* @return {string} The created error log file.
* @private
* @since 1.0.0
*/
function createLogFile(prefix) {
return `${prefix || 'ytmp3Error'}-${
(new Date()).toISOString().split('.')[0].replace(/:/g, '.')}.log`;
}
// region Utilities Class
class ProgressBar {
/**
* Initialize the `ProgressBar` class with specific options to configure the
* progress bar.
*
* @classdesc Class representing a progress bar for downloads.
*
* @class
* @param {ProgressBarOptions} options
* Options object for configuring the progress bar.
* @package
* @since 1.0.0
*/
constructor (options) {
/**
* Options object for configuring the progress bar.
* @type {ResolvedProgressBarOptions}
*/
this.options = {
barWidth: (typeof options?.barWidth === 'string'
|| typeof options?.barWidth === 'number')
? options.barWidth : 'auto',
barCharElapsed: (typeof options?.barCharElapsed === 'string')
? options.barCharElapsed
: '#',
barCharTotal: (typeof options?.barCharTotal === 'string')
? options.barCharTotal
: '-',
bytesInfo: (typeof options?.bytesInfo === 'boolean')
? options.bytesInfo === true
: true
};
/**
* Index for the loading animation.
* @type {number}
*/
this.idxLoading = 0;
/**
* Characters for the loading animation.
* @type {'\\' | '|' | '/' | '-'}
* @default
*/
this.loadings = [ '\\', '|', '/', '-' ];
}
/**
* Creates a string formatted download progress bar with centered percentage.
*
* @method
* @param {number} bytesDownloaded - The number of bytes downloaded so far.
* @param {number} totalBytes - The total number of bytes to download.
*
* @returns {string} The formatted progress bar string with percentage and byte information.
*
* @since 1.0.0
*/
create(bytesDownloaded, totalBytes) {
const {
barCharElapsed,
barCharTotal
} = this.options;
let barWidth = parseBarWidth(this.options.barWidth);
barWidth = Math.round(barWidth / ((barWidth > 60) ? 1.85 : 2.25));
this.idxLoading = (this.idxLoading >= this.loadings.length)
? 0
: this.idxLoading;
const loading = this.loadings[this.idxLoading++];
/**
* Parses the bar width option.
*
* @private
* @param {string | number} val - The bar width value to parse.
* @returns {number} The parsed bar width as a number.
*/
function parseBarWidth(val) {
return (!val || (typeof val === 'string' && val === 'auto'))
? ('columns' in process.stdout)
? Math.min(100, process.stdout.columns)
: 40
: val;
}
// Calculate the progress percentage
const progress = Math.max(0, Math.min(100, Math.floor(
bytesDownloaded / totalBytes * 100
)));
const progressStr = ` [${progress}%] `;
// Calculate the number of characters to fill in the progress bar
const progressBarWidth = Math.floor(barWidth * (progress / 100));
// Calculate where to start the progress bar and the percentage text
const progressBarStart = Math.max(0, progressBarWidth);
const percentagePos = Math.max(0, Math.min(barWidth - 4,
Math.floor((barWidth - 4) / 2)));
// Create the progress bar string with centered percentage
let progressBar = `[${barCharElapsed.repeat(progressBarStart)}${
barCharTotal.repeat(barWidth - progressBarWidth)
}]`;
progressBar = progressBar.substring(0, percentagePos) +
((progress < 100) ? '\x1b[0;96m' : '\x1b[0;92m') +
`${progressStr}\x1b[0m\x1b[1m` +
progressBar.substring(percentagePos + progressStr.length);
// Calculate the number of bytes downloaded in MB
const byteInfo = `[${
(bytesDownloaded / (1024 * 1024)).toFixed(2)
}/${(totalBytes / (1024 * 1024)).toFixed(2)} MB]`;
// Return the formatted progress bar with percentage
return (progress < 100)
// eslint-disable-next-line max-len
? `\x1b[K\x1b[1;93m[...]\x1b[0m \x1b[1m${progressBar} \x1b[0;93m[${loading}]\x1b[0m \x1b[1;95m${byteInfo}\x1b[0m\r`
// eslint-disable-next-line max-len
: `\x1b[K\x1b[1;92m[DONE]\x1b[0m \x1b[1m${progressBar} \x1b[0;92m[\u2714]\x1b[0m \x1b[1;95m${byteInfo}\x1b[0m\n`;
}
}
// Logger module
Object.assign(module.exports, {
Logger,
log: Logger.log,
logger: Logger.logger
});
// TypeUtils module
Object.assign(module.exports, {
TypeUtils,
...TypeUtils
});
// URLUtils module
Object.assign(module.exports, { URLUtils });
// URLFmt module
Object.assign(module.exports, { URLFmt });
// Utils (this) module
Object.assign(module.exports, {
ROOTDIR, OUTDIR, LOGDIR, YTMP3_HOMEDIR,
createDirIfNotExist,
createDirIfNotExistSync,
createLogFile,
ProgressBar
});