Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add global config support and cross-platform enhancements #45

Merged
merged 21 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
383fdd7
chore(npm): Add `lsfnd` dependency
mitsuki31 Aug 30, 2024
2d21e53
refactor(utils): Enhance object type checks and new constant
mitsuki31 Aug 30, 2024
f098658
feat(errors): New custom error classes addition
mitsuki31 Aug 30, 2024
a600b13
refactor(config): Update config file extensions and handling
mitsuki31 Aug 30, 2024
ffa8e45
docs(config): JSDoc addition and improvement
mitsuki31 Aug 30, 2024
8c54c17
refactor(config): Enhance error handling and improve JSDoc
mitsuki31 Aug 30, 2024
2cff700
feat(config): Implement global config file parser with prioritized se…
mitsuki31 Aug 30, 2024
0f70d2d
refactor(index): Split main index for API and CLI usage
mitsuki31 Aug 31, 2024
09f404e
chore(npm): Update `bin` entry point to point to CLI module
mitsuki31 Aug 31, 2024
5785bee
chore(eslint): Update config to include bin directory
mitsuki31 Aug 31, 2024
a5a284f
fix(audioconv): Fix object checker in options resolver
mitsuki31 Aug 31, 2024
73fcd47
test(config): Update tests to reflect `InvalidTypeError` usage
mitsuki31 Aug 31, 2024
3b3fde5
test(utils): Enhance tests for object type checks
mitsuki31 Aug 31, 2024
952fd71
refactor(cli): Improve argparse behavior for intermixed options
mitsuki31 Aug 31, 2024
8031795
feat(cli): Add `--no-config` option to disable configuration files load
mitsuki31 Aug 31, 2024
973fd51
feat(cli): Add support for inverting enabled options
mitsuki31 Aug 31, 2024
6b28cfd
feat(config): Add `searchDir` param to control search directory
mitsuki31 Sep 3, 2024
ee2d13e
test(config): Add new test suites and improvements
mitsuki31 Sep 3, 2024
ea94d38
test(error): Add test module for `error` module
mitsuki31 Sep 3, 2024
f0dc427
refactor(config): Fix path resolution for Windows and POSIX platforms
mitsuki31 Sep 3, 2024
56bf31b
docs(cli): Update the CLI's epilog docs
mitsuki31 Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
402 changes: 402 additions & 0 deletions bin/argparser.js

Large diffs are not rendered by default.

177 changes: 177 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env node

/**
* @file Main binary module for **YTMP3** project to download YouTube audios using CLI.
*
* @requires utils
* @requires ytmp3
* @requires bin/argparser
* @author Ryuu Mitsuki <https://github.com/mitsuki31>
* @license MIT
* @since 0.1.0
*/

'use strict';

const fs = require('fs'); // File system module
const path = require('path'); // Path module
const { EOL } = require('os');
const { promisify } = require('util');
const {
getTempPath,
createTempPath: _createTempPath
} = require('@mitsuki31/temppath');
const createTempPath = promisify(_createTempPath);

const { logger: log } = require('../lib/utils');
const ytmp3 = require('../lib/ytmp3');
const pkg = require('../package.json');
const {
__version__,
__copyright__,
initParser,
filterOptions
} = require('./argparser');


const TEMPDIR = path.join(path.dirname(getTempPath()), 'ytmp3-js');
const DEFAULT_BATCH_FILE = path.join(__dirname, 'downloads.txt');

/** Store the file path of cached multiple download URLs. */
let multipleDlCache = null;


/**
* Creates a cache file for URLs to be downloaded.
*
* This function creates a temporary file in the system's temporary directory
* containing a list of URLs to be downloaded using the
* {@link module:ytmp3~batchDownload `ytmp3.batchDownload`} function.
*
* @param {string[]} urls - URLs to be written to cache file
* @returns {Promise<string>} The path to the cache file for later deletion
*
* @private
* @since 1.0.0
*/
async function createCache(urls) {
const cache = await createTempPath(TEMPDIR, {
asFile: true,
ext: 'dl',
maxLen: 20
});
// Create write stream for cache file
const cacheStream = fs.createWriteStream(cache);

// Write URLs to cache
urls.forEach(url => cacheStream.write(`${url}${EOL}`));

// Close the write stream
cacheStream.end();
return cache;
}

/**
* Deletes the cache file if it exists
*
* @returns {Promise<boolean>} `true` if the cache file is deleted successfully
* @private
* @since 1.0.0
*/
async function deleteCache() {
if (!multipleDlCache) return false;
if (fs.existsSync(multipleDlCache)) {
// Delete the parent directory of the cache file
await fs.promises.rm(path.dirname(multipleDlCache), { recursive: true, force: true });
}
return true;
}

/**
* Main function.
* @private
* @since 1.0.0
*/
async function main() {
const {
urls,
batchFile,
version,
copyright,
downloadOptions,
printConfig
} = await filterOptions({
options: initParser().parse_intermixed_args()
});

// Version
if (version === 1) {
process.stdout.write(__version__);
return;
} else if (version >= 2) {
// If got '-VV' or '--version --version', then verbosely print this module
// version and all dependencies' version
const deps = Object.keys(pkg.dependencies);
process.stdout.write(__version__);
for (const dep of deps) {
process.stdout.write(`\x1b[1m ${
((deps.indexOf(dep) !== deps.length - 1) ? '├── ' : '└── ')
}${dep} :: v${require(dep + '/package.json').version}\x1b[0m\n`);
}
return;
}
// Copyright
if (copyright) {
process.stdout.write(__copyright__);
return;
}
// Print configuration
if (printConfig) {
console.log(downloadOptions);
return;
}

let downloadSucceed = false;
try {
if ((!urls || (urls && !urls.length)) && !batchFile) {
const defaultBatchFileBase = path.basename(DEFAULT_BATCH_FILE);
log.info(`\x1b[2mNo URL and batch file specified, searching \x1b[93m${
defaultBatchFileBase}\x1b[0m\x1b[2m ...\x1b[0m`);
if (!fs.existsSync(DEFAULT_BATCH_FILE)) {
log.error(`Cannot find \x1b[93m${
defaultBatchFileBase}\x1b[0m at current directory`);
log.error('Aborted');
process.exit(1);
}
log.info('\x1b[95mMode: \x1b[97mBatch Download\x1b[0m');
downloadSucceed = !!await ytmp3.batchDownload(DEFAULT_BATCH_FILE, downloadOptions);
} else if ((!urls || (urls && !urls.length)) && batchFile) {
log.info('\x1b[95mMode: \x1b[97mBatch Download\x1b[0m');
downloadSucceed = !!await ytmp3.batchDownload(batchFile, downloadOptions);
} else if (urls.length && !batchFile) {
if (Array.isArray(urls) && urls.length > 1) {
log.info('\x1b[95mMode: \x1b[97mMultiple Downloads\x1b[0m');
multipleDlCache = await createCache(urls);
downloadSucceed = !!await ytmp3.batchDownload(multipleDlCache, downloadOptions);
} else {
log.info('\x1b[95mMode: \x1b[97mSingle Download\x1b[0m');
downloadSucceed = !!await ytmp3.singleDownload(urls[0], downloadOptions);
}
}
} catch (dlErr) {
log.error(dlErr.message);
console.error(dlErr.stack);
process.exit(1);
} finally {
await deleteCache();
}

if (downloadSucceed) {
log.info(`Downloaded files saved at \x1b[93m${downloadOptions.outDir}\x1b[0m`);
}
}


if (require.main === module) {
main();
}
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = [
files: [
'index.js',
'lib/**/*.js',
'bin/**/*',
'eslint.config.js'
],
languageOptions: {
Expand Down
1 change: 1 addition & 0 deletions eslint.config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = [
'**/coverage/',
'config/example/**.{mjs,js,json}',
'lib/',
'bin/',
'index.js',
'eslint.config.*'
]
Expand Down
Loading
Loading