#!/usr/bin/env node // requirements const os = require('os'); const path = require('path'); const fs = require('fs'); const fse = require('fs-extra'); const async = require('async'); const commander = require('commander'); const recursive = require('recursive-readdir'); const metadata = require('musicmetadata'); const progress = require('progress'); const app = require('./package.json'); // generate timestamp const start = process.hrtime(); printLogo(); // general options commander .version(app.version) .usage('[options] ') .option('-c, --concurrency ', 'specify concurrency level', os.cpus().length); // conversion mode commander .command('convert ') .description('convert .flac to .mp3 files') .option('-b, --bitrate ', 'specify conversion bitrate', 320) .action(function (input, output, options) { console.log('--- CONVERT MODE ---'); console.log('input: ' + input); console.log('output: ' + output); console.log('bitrate: ' + commander.options.bitrate); console.log('conc: ' + commander.concurrency); }); // sort mode commander .command('sort ') .description('sort audio files by tags') .option('-f, --format ', 'specify audio format (\'flac\', \'mp3\')', 'flac') .action(sort); // parse command line arguments commander.parse(process.argv); // sort files function sort(input, output, options) { const concurrency = commander.concurrency; async.waterfall([ function (asyncCallback) { recursive(input, [ignoreFilter], asyncCallback); }, function (files, asyncCallback) { console.log(files.length + ' \'' + options.format + '\' files found'); // display progressbar const bar = createProgressBar(files.length); // handle each file async.eachLimit(files, concurrency, function (file, eachCallback) { processFile(output, file, function (err) { bar.tick(); if (err) { return eachCallback(err); } eachCallback(); }); }, function (err) { if (err) { return asyncCallback(err); } asyncCallback(); }); } ], function (err, result) { exit(err); }); function ignoreFilter(file, stats) { return !stats.isDirectory() && path.extname(file).indexOf(options.format) == -1; } } // move file to location defined by its metadata function processFile(output, sourceFile, callback) { async.waterfall([ function (asyncCallback) { extractMetadata(sourceFile, asyncCallback) }, function (sourceFile, metadata, asyncCallback) { pathFromMetadata(sourceFile, output, metadata, asyncCallback); }, function (targetFile, asyncCallback) { moveFile(sourceFile, targetFile, asyncCallback); } ], function (err, results) { if (err) { return callback(err); } callback(null, results); }); } // extract metadata for further processing function extractMetadata(sourceFile, callback) { const stream = fs.createReadStream(sourceFile); metadata(stream, function (err, metadata) { if (err) { return callback(err); } stream.close(); callback(null, sourceFile, metadata) }); } // create path for target file function pathFromMetadata(file, output, metadata, callback) { // define directory let filePath = path.normalize(output); if (metadata.albumartist && metadata.albumartist.length > 0) { let tmp; const artistCount = metadata.albumartist.length; for (let counter = 0; counter < artistCount; counter++) { if (counter > 0) { tmp += ' - ' + metadata.albumartist[counter]; } else { tmp = metadata.albumartist[counter]; } } filePath = path.join(filePath, tmp); } else { filePath = path.join(filePath, metadata.artist[0]); } if (metadata.album) { filePath = path.join(filePath, metadata.album); } // define filename let fileName = ''; if (metadata.disk.no) { fileName += frontFill(metadata.disk.no, '0', 2); } if (metadata.track.no) { if (fileName) { fileName += '-' + frontFill(metadata.track.no, '0', 2) + ' '; } else { fileName += frontFill(metadata.track.no, '0', 2) + ' '; } } if (metadata.artist) { fileName += metadata.artist[0] + ' - '; } if (metadata.title) { fileName += metadata.title; } // append extension fileName += path.extname(file); // join directory and name callback(null, path.join(filePath, fileName)); } // create a ascii progressbar function createProgressBar(total) { return new progress(':bar | progress: :current/:total (:percent) | elapsed: :elapseds | eta: :etas', { total: total, width: 32 }); } // create target directory and move the file function moveFile(source, target, callback) { async.series([ function (asyncCallback) { fse.mkdirs(path.dirname(target), asyncCallback); }, function (asyncCallback) { fse.move(source, target, asyncCallback); } ], function (err, results) { if (err) { return callback(err); } callback(); }); } // fill a string beginning from the front function frontFill(string, fill, length) { while (string.toString().length < length) { string = fill + string; } return string; } // print logo function printLogo() { console.log(' _ _ _ __ __ '); console.log(' | |__ __ _ __| |__ _ ___ _ _ /_\\ | \\/ |'); console.log(' | \'_ \\\/ _` \/ _` \/ _` / -_) \'_\/ _ \\| |\\/| |'); console.log(' |_.__\/\\__,_\\__,_\\__, \\___|_|\/_\/ \\_\\_| |_|'); console.log(' |___/ '); } // shutdown function exit(err) { if (err) { console.error(err); process.exit(1); } const diff = process.hrtime(start); console.log('exiting after ' + ((diff[0] + (diff[1] / 1000000)) / 1000).toFixed(2) + ' seconds'); process.exit(0); }