// requirements const fs = require('fs'); const path = require('path'); const async = require('async'); const metadata = require('musicmetadata'); const ffmpeg = require('fluent-ffmpeg'); const fse = require('fs-extra'); const util = require('./util'); const cli = require('./cli'); // convert all files in input directory function batchConvert(config, callback) { let timestamp = process.hrtime(); async.waterfall([ // get files function (waterfallCallback) { util.readDirRecursive(config.input, config.format, waterfallCallback); }, // display info, prompt user and create progressbar function (files, waterfallCallback) { console.log(files.length + ' files found after ' + util.getTimeDiff(timestamp) + ' seconds'); if (config.confirm) { cli.askForConfirmation('start conversion now?', ['yes', 'y'], function (err) { if (err) { return waterfallCallback(err); } waterfallCallback(null, files, cli.createProgressBar(files.length)); }); } else { waterfallCallback(null, files, cli.createProgressBar(files.length)); } }, // process each file function (files, bar, waterfallCallback) { timestamp = process.hrtime(); let errors = []; async.eachLimit(files, config.concurrency, function (file, eachCallback) { convert(file, config, function (err) { bar.tick(); if (err) { err.file = file; errors.push(err); } eachCallback(); }); }, function (err, result) { errors.forEach(function(error) { console.error('error converting file: \'' + error.file + '\'', error); }); if (err) { return waterfallCallback(err); } waterfallCallback(null, timestamp) }); } ], callback); } // convert file to mp3 at specified bitrate function convert(source, config, callback) { async.waterfall([ // get metadata function (waterfallCallback) { extractMetadata(source, waterfallCallback); }, // create path from metadata function (metadata, waterfallCallback) { util.getPathByMetadata(source, config.output, metadata, waterfallCallback); }, // create target directory function (target, waterfallCallback) { target = path.join(path.dirname(target), path.basename(target, path.extname(target)) + '.mp3'); fse.mkdirs(path.dirname(target), function (err) { if (err) { return waterfallCallback(err); } waterfallCallback(null, target, config); }); }, // convert file to mp3 function (target, config, waterfallCallback) { ffmpeg(path.normalize(source)).audioCodec('libmp3lame').audioBitrate(config.bitrate).save(target) .on('error', function (err) { return waterfallCallback(err); }) .on('end', function () { waterfallCallback(); }); } ], callback); } // extract all covers function batchExtract(config, callback) { let timestamp = process.hrtime(); async.waterfall([ // get files function (waterfallCallback) { util.readDirRecursive(config.input, config.format, waterfallCallback); }, // display info, prompt user and create progressbar function (files, waterfallCallback) { console.log(files.length + ' files found after ' + util.getTimeDiff(timestamp) + ' seconds'); if (config.confirm) { cli.askForConfirmation('start artwork extraction now?', ['yes', 'y'], function (err) { if (err) { return waterfallCallback(err); } }); } else { waterfallCallback(null, files, cli.createProgressBar(files.length)); } }, // process each file function (files, bar, waterfallCallback) { timestamp = process.hrtime(); async.eachLimit(files, config.concurrency, function (file, eachCallback) { artwork(file, function (err) { bar.tick(); if (err) { return eachCallback(err); } eachCallback(); }); }, function (err, result) { if (err) { return waterfallCallback(err); } waterfallCallback(null, timestamp); }); } ], callback); } // extract cover artwork function artwork(source, callback) { async.waterfall([ // get metadata function (waterfallCallback) { extractMetadata(source, waterfallCallback); }, // write image to disk function (metadata, waterfallCallback) { if (!metadata.picture) { waterfallCallback(); } for (let counter = 0, length = metadata.picture.length; counter < length; counter++) { const stream = fs.createWriteStream(path.join(path.dirname(source), path.basename(source, path.extname(source)) + '.' + metadata.picture[counter].format)); stream.write(metadata.picture[counter].data); stream.end(); } waterfallCallback(); } ], callback); } // scan files in input directory for missing cover artwork function batchScan(config, callback) { let timestamp = process.hrtime(); async.waterfall([ // get files function (waterfallCallback) { util.readDirRecursive(config.input, config.format, waterfallCallback); }, // display info, prompt user and create progressbar function (files, waterfallCallback) { console.log(files.length + ' files found after ' + util.getTimeDiff(timestamp) + ' seconds'); cli.askForConfirmation('start scan now?', ['yes', 'y'], function (err) { if (err) { return waterfallCallback(err); } waterfallCallback(null, files, cli.createProgressBar(files.length)); }); }, // process each file function (files, bar, waterfallCallback) { let missing = []; timestamp = process.hrtime(); async.eachLimit(files, config.concurrency, function (file, eachCallback) { extractMetadata(file, function (err, metadata) { bar.tick(); if (err) { return eachCallback(err); } if (!metadata.picture || metadata.picture.length === 0) { missing.push(file); } eachCallback(); }); }, function (err) { if (err) { return waterfallCallback(err); } waterfallCallback(null, missing, timestamp); }); } ], callback); } // extract metadata for further processing function extractMetadata(source, callback) { const stream = fs.createReadStream(source); metadata(stream, function (err, metadata) { if (err) { console.error('ERROR AT FILE: ' + source); return callback(err); } stream.close(); callback(null, metadata); }); } // api exports.batchConvert = batchConvert; exports.batchArtwork = batchExtract; exports.batchScan = batchScan; exports.extractMetadata = extractMetadata;