2017-03-24 22:05:51 +01:00
|
|
|
// requirements
|
2017-03-24 22:11:22 +01:00
|
|
|
const fs = require('fs');
|
2017-03-24 22:56:05 +01:00
|
|
|
const path = require('path');
|
|
|
|
const async = require('async');
|
2017-03-24 22:05:51 +01:00
|
|
|
const metadata = require('musicmetadata');
|
2017-03-24 22:56:05 +01:00
|
|
|
const ffmpeg = require('fluent-ffmpeg');
|
|
|
|
const fse = require('fs-extra');
|
2017-03-24 23:36:41 +01:00
|
|
|
const util = require('./util');
|
2017-03-27 22:28:35 +02:00
|
|
|
const cli = require('./cli');
|
|
|
|
|
|
|
|
// convert all files in input directory
|
|
|
|
function batchConvert(config, callback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
let timestamp = process.hrtime();
|
2017-03-27 22:28:35 +02:00
|
|
|
async.waterfall([
|
|
|
|
// get files
|
|
|
|
function (waterfallCallback) {
|
|
|
|
util.readDirRecursive(config.input, config.format, waterfallCallback);
|
|
|
|
},
|
|
|
|
// display info, prompt user and create progressbar
|
|
|
|
function (files, waterfallCallback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
console.log(files.length + ' files found after ' + util.getTimeDiff(timestamp) + ' seconds');
|
2017-03-29 13:28:07 +02:00
|
|
|
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 {
|
2017-03-27 22:28:35 +02:00
|
|
|
waterfallCallback(null, files, cli.createProgressBar(files.length));
|
2017-03-29 13:28:07 +02:00
|
|
|
}
|
2017-03-27 22:28:35 +02:00
|
|
|
},
|
|
|
|
// process each file
|
|
|
|
function (files, bar, waterfallCallback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
timestamp = process.hrtime();
|
2021-12-27 10:13:35 +01:00
|
|
|
let errors = [];
|
2017-03-27 22:28:35 +02:00
|
|
|
async.eachLimit(files, config.concurrency, function (file, eachCallback) {
|
|
|
|
convert(file, config, function (err) {
|
|
|
|
bar.tick();
|
|
|
|
if (err) {
|
2021-12-27 10:13:35 +01:00
|
|
|
err.file = file;
|
|
|
|
errors.push(err);
|
2017-03-27 22:28:35 +02:00
|
|
|
}
|
|
|
|
eachCallback();
|
|
|
|
});
|
2017-03-28 14:54:51 +02:00
|
|
|
}, function (err, result) {
|
2021-12-27 10:13:35 +01:00
|
|
|
errors.forEach(function(error) {
|
|
|
|
console.error('error converting file: \'' + error.file + '\'', error);
|
|
|
|
});
|
2017-03-28 14:54:51 +02:00
|
|
|
if (err) {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
|
|
|
waterfallCallback(null, timestamp)
|
|
|
|
});
|
2017-03-27 22:28:35 +02:00
|
|
|
}
|
|
|
|
], callback);
|
|
|
|
}
|
2017-03-24 22:56:05 +01:00
|
|
|
|
|
|
|
// convert file to mp3 at specified bitrate
|
2017-03-27 22:28:35 +02:00
|
|
|
function convert(source, config, callback) {
|
|
|
|
async.waterfall([
|
|
|
|
// get metadata
|
|
|
|
function (waterfallCallback) {
|
|
|
|
extractMetadata(source, waterfallCallback);
|
2017-03-24 22:56:05 +01:00
|
|
|
},
|
2017-03-27 22:28:35 +02:00
|
|
|
// 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)
|
2017-03-24 22:56:05 +01:00
|
|
|
.on('error', function (err) {
|
2017-03-27 22:28:35 +02:00
|
|
|
return waterfallCallback(err);
|
2017-03-24 22:56:05 +01:00
|
|
|
})
|
|
|
|
.on('end', function () {
|
2017-03-27 22:28:35 +02:00
|
|
|
waterfallCallback();
|
2017-03-24 22:56:05 +01:00
|
|
|
});
|
|
|
|
}
|
2017-03-27 22:28:35 +02:00
|
|
|
], callback);
|
2017-03-24 23:36:41 +01:00
|
|
|
}
|
|
|
|
|
2017-03-28 15:39:37 +02:00
|
|
|
// extract all covers
|
|
|
|
function batchExtract(config, callback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
let timestamp = process.hrtime();
|
2017-03-24 23:36:41 +01:00
|
|
|
async.waterfall([
|
2017-03-27 23:27:33 +02:00
|
|
|
// get files
|
|
|
|
function (waterfallCallback) {
|
|
|
|
util.readDirRecursive(config.input, config.format, waterfallCallback);
|
2017-03-24 23:36:41 +01:00
|
|
|
},
|
2017-03-27 23:27:33 +02:00
|
|
|
// display info, prompt user and create progressbar
|
|
|
|
function (files, waterfallCallback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
console.log(files.length + ' files found after ' + util.getTimeDiff(timestamp) + ' seconds');
|
2017-03-29 13:28:07 +02:00
|
|
|
if (config.confirm) {
|
|
|
|
cli.askForConfirmation('start artwork extraction now?', ['yes', 'y'], function (err) {
|
|
|
|
if (err) {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
} else {
|
2017-03-27 23:27:33 +02:00
|
|
|
waterfallCallback(null, files, cli.createProgressBar(files.length));
|
2017-03-29 13:28:07 +02:00
|
|
|
}
|
2017-03-27 23:27:33 +02:00
|
|
|
},
|
|
|
|
// process each file
|
|
|
|
function (files, bar, waterfallCallback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
timestamp = process.hrtime();
|
2017-03-27 23:27:33 +02:00
|
|
|
async.eachLimit(files, config.concurrency, function (file, eachCallback) {
|
2017-03-28 15:39:37 +02:00
|
|
|
artwork(file, function (err) {
|
2017-03-27 23:27:33 +02:00
|
|
|
bar.tick();
|
|
|
|
if (err) {
|
|
|
|
return eachCallback(err);
|
|
|
|
}
|
|
|
|
eachCallback();
|
|
|
|
});
|
2017-03-28 14:54:51 +02:00
|
|
|
}, function (err, result) {
|
|
|
|
if (err) {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
|
|
|
waterfallCallback(null, timestamp);
|
|
|
|
});
|
2017-03-24 23:36:41 +01:00
|
|
|
}
|
2017-03-27 23:27:33 +02:00
|
|
|
], callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
// extract cover artwork
|
2017-03-28 15:39:37 +02:00
|
|
|
function artwork(source, callback) {
|
2017-03-27 23:27:33 +02:00
|
|
|
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();
|
2017-03-24 23:36:41 +01:00
|
|
|
}
|
2017-03-27 23:27:33 +02:00
|
|
|
], callback);
|
2017-03-24 23:36:41 +01:00
|
|
|
}
|
2017-03-24 22:05:51 +01:00
|
|
|
|
2017-03-28 15:39:37 +02:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2017-03-24 22:05:51 +01:00
|
|
|
// extract metadata for further processing
|
2017-03-28 15:39:37 +02:00
|
|
|
function extractMetadata(source, callback) {
|
|
|
|
const stream = fs.createReadStream(source);
|
2017-03-24 22:05:51 +01:00
|
|
|
metadata(stream, function (err, metadata) {
|
|
|
|
if (err) {
|
2021-12-27 10:13:35 +01:00
|
|
|
console.error('ERROR AT FILE: ' + source);
|
2017-03-24 22:05:51 +01:00
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
stream.close();
|
2017-03-24 23:36:41 +01:00
|
|
|
callback(null, metadata);
|
2017-03-24 22:05:51 +01:00
|
|
|
});
|
2017-03-24 23:36:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// api
|
2017-03-27 22:28:35 +02:00
|
|
|
exports.batchConvert = batchConvert;
|
2017-03-28 15:39:37 +02:00
|
|
|
exports.batchArtwork = batchExtract;
|
|
|
|
exports.batchScan = batchScan;
|
2017-03-24 23:36:41 +01:00
|
|
|
exports.extractMetadata = extractMetadata;
|