2017-03-24 22:05:51 +01:00
|
|
|
// requirements
|
2017-03-24 22:11:22 +01:00
|
|
|
const path = require('path');
|
|
|
|
const async = require('async');
|
|
|
|
const fse = require('fs-extra');
|
2017-03-27 22:29:44 +02:00
|
|
|
const recursive = require('recursive-readdir');
|
2017-03-24 22:05:51 +01:00
|
|
|
const progress = require('progress');
|
2017-03-27 22:47:09 +02:00
|
|
|
const cli = require('./cli');
|
|
|
|
const audio = require('./audio');
|
|
|
|
|
|
|
|
// move all files from input to output directory
|
|
|
|
function batchSort(config, callback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
let timestamp = process.hrtime();
|
2017-03-27 22:47:09 +02:00
|
|
|
async.waterfall([
|
|
|
|
// get files
|
|
|
|
function (waterfallCallback) {
|
|
|
|
readDirRecursive(config.input, config.format, waterfallCallback);
|
|
|
|
},
|
|
|
|
// display info, prompt user and create progressbar
|
|
|
|
function (files, waterfallCallback) {
|
2017-03-28 15:39:37 +02:00
|
|
|
console.log(files.length + ' files found after ' + getTimeDiff(timestamp) + ' seconds');
|
2017-03-29 13:28:07 +02:00
|
|
|
if (config.confirm) {
|
|
|
|
cli.askForConfirmation('start sorting now?', ['yes', 'y'], function (err) {
|
|
|
|
if (err) {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
|
|
|
waterfallCallback(null, files, cli.createProgressBar(files.length));
|
|
|
|
});
|
|
|
|
} else {
|
2017-03-28 14:54:51 +02:00
|
|
|
waterfallCallback(null, files, cli.createProgressBar(files.length));
|
2017-03-29 13:28:07 +02:00
|
|
|
}
|
2017-03-27 22:47:09 +02:00
|
|
|
},
|
|
|
|
// process each file
|
|
|
|
function (files, bar, waterfallCallback) {
|
2017-03-28 16:08:01 +02:00
|
|
|
let skipped = [];
|
2017-03-28 14:54:51 +02:00
|
|
|
timestamp = process.hrtime();
|
2017-03-27 22:47:09 +02:00
|
|
|
async.eachLimit(files, config.concurrency, function (file, eachCallback) {
|
2017-03-28 16:08:01 +02:00
|
|
|
moveFile(file, config, function (err, moved) {
|
2017-03-27 22:47:09 +02:00
|
|
|
bar.tick();
|
|
|
|
if (err) {
|
|
|
|
return eachCallback(err);
|
|
|
|
}
|
2017-03-28 16:08:01 +02:00
|
|
|
if (!moved) {
|
|
|
|
skipped.push(file);
|
|
|
|
}
|
2017-03-27 22:47:09 +02:00
|
|
|
eachCallback();
|
|
|
|
})
|
2017-03-28 14:54:51 +02:00
|
|
|
}, function (err, result) {
|
|
|
|
if (err) {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
2017-03-28 16:08:01 +02:00
|
|
|
waterfallCallback(null, skipped, timestamp);
|
2017-03-28 14:54:51 +02:00
|
|
|
});
|
2017-03-27 22:47:09 +02:00
|
|
|
}
|
|
|
|
], callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
// create target directory and move the file
|
|
|
|
function moveFile(source, config, callback) {
|
|
|
|
async.waterfall([
|
|
|
|
// get metadata
|
|
|
|
function (waterfallCallback) {
|
|
|
|
audio.extractMetadata(source, waterfallCallback);
|
|
|
|
},
|
|
|
|
// create path from metadata
|
|
|
|
function (metadata, waterfallCallback) {
|
|
|
|
getPathByMetadata(source, config.output, metadata, waterfallCallback);
|
|
|
|
},
|
|
|
|
// create target directory
|
|
|
|
function (target, waterfallCallback) {
|
|
|
|
fse.mkdirs(path.dirname(target), function (err) {
|
|
|
|
if (err) {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
|
|
|
waterfallCallback(null, target);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
// move file
|
|
|
|
function (target, waterfallCallback) {
|
2017-03-28 13:50:31 +02:00
|
|
|
fse.move(source, target, function (err) {
|
|
|
|
if (err) {
|
|
|
|
if (err.code === 'EEXIST') {
|
2017-03-28 16:08:01 +02:00
|
|
|
return waterfallCallback();
|
2017-03-28 13:50:31 +02:00
|
|
|
} else {
|
|
|
|
return waterfallCallback(err);
|
|
|
|
}
|
|
|
|
}
|
2017-03-28 16:08:01 +02:00
|
|
|
waterfallCallback(null, true);
|
2017-03-28 13:50:31 +02:00
|
|
|
});
|
2017-03-27 22:47:09 +02:00
|
|
|
}
|
|
|
|
], callback);
|
|
|
|
}
|
2017-03-24 22:05:51 +01:00
|
|
|
|
|
|
|
// create path for target file
|
2017-03-27 22:29:44 +02:00
|
|
|
function getPathByMetadata(source, output, metadata, callback) {
|
2017-03-24 22:05:51 +01:00
|
|
|
// define directory
|
|
|
|
let filePath = path.normalize(output);
|
|
|
|
if (metadata.albumartist && metadata.albumartist.length > 0) {
|
|
|
|
let tmp;
|
2017-03-28 13:50:31 +02:00
|
|
|
for (let counter = 0, length = metadata.albumartist.length; counter < length; counter++) {
|
2017-03-24 22:05:51 +01:00
|
|
|
if (counter > 0) {
|
|
|
|
tmp += ' - ' + metadata.albumartist[counter];
|
|
|
|
} else {
|
|
|
|
tmp = metadata.albumartist[counter];
|
|
|
|
}
|
|
|
|
}
|
2017-03-28 14:54:51 +02:00
|
|
|
filePath = path.join(filePath, replaceIllegalChars(tmp.trim()));
|
2017-03-24 22:05:51 +01:00
|
|
|
} else {
|
2017-03-28 14:54:51 +02:00
|
|
|
filePath = path.join(filePath, replaceIllegalChars(metadata.artist[0].trim()));
|
2017-03-24 22:05:51 +01:00
|
|
|
}
|
|
|
|
if (metadata.album) {
|
2017-03-28 14:54:51 +02:00
|
|
|
filePath = path.join(filePath, replaceIllegalChars(metadata.album.trim()));
|
2017-03-24 22:05:51 +01:00
|
|
|
}
|
|
|
|
// 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) {
|
2017-03-28 13:50:31 +02:00
|
|
|
fileName += metadata.artist[0].trim() + ' - ';
|
2017-03-24 22:05:51 +01:00
|
|
|
}
|
|
|
|
if (metadata.title) {
|
2017-03-28 13:50:31 +02:00
|
|
|
fileName += metadata.title.trim();
|
2017-03-24 22:05:51 +01:00
|
|
|
}
|
2017-03-29 16:17:10 +02:00
|
|
|
// check length of filename, append extension and replace illegal chars
|
|
|
|
const ext = path.extname(source).trim();
|
|
|
|
const max = (255 - ext.length);
|
|
|
|
fileName = fileName.trim();
|
|
|
|
if (fileName.length > max) {
|
|
|
|
const suffix = ' (...)';
|
|
|
|
fileName = fileName.substr(0, max - suffix.length).trim() + suffix;
|
|
|
|
}
|
|
|
|
fileName = replaceIllegalChars(fileName + ext);
|
2017-03-29 13:28:07 +02:00
|
|
|
// replace illegal chars, join directory and name
|
2017-03-29 16:17:10 +02:00
|
|
|
callback(null, path.join(filePath, fileName));
|
2017-03-24 22:56:05 +01:00
|
|
|
}
|
2017-03-24 22:05:51 +01:00
|
|
|
|
|
|
|
// fill a string beginning from the front
|
2017-03-24 22:56:05 +01:00
|
|
|
function frontFill(string, fill, length) {
|
2017-03-24 22:05:51 +01:00
|
|
|
while (string.toString().length < length) {
|
|
|
|
string = fill + string;
|
|
|
|
}
|
|
|
|
return string;
|
2017-03-24 22:56:05 +01:00
|
|
|
}
|
2017-03-24 22:05:51 +01:00
|
|
|
|
2017-03-28 14:54:51 +02:00
|
|
|
// replace illegal characters
|
|
|
|
function replaceIllegalChars(string) {
|
2017-03-28 13:50:31 +02:00
|
|
|
return string.replace(/[/\\:*?"<>|]/g, '-');
|
|
|
|
}
|
|
|
|
|
2017-03-27 22:29:44 +02:00
|
|
|
// list files in directory
|
|
|
|
function readDirRecursive(where, extension, callback) {
|
2017-03-28 14:54:51 +02:00
|
|
|
console.log('getting list of \'' + extension + '\' files in directory \'' + where + '\' ...');
|
2017-03-27 22:29:44 +02:00
|
|
|
if (extension.indexOf('.') !== 0) {
|
|
|
|
extension = '.' + extension;
|
|
|
|
}
|
|
|
|
recursive(where, [ignoreFilter], callback);
|
|
|
|
function ignoreFilter(file, stats) {
|
|
|
|
return !stats.isDirectory() && extension.indexOf(path.extname(file)) === -1;
|
2017-03-24 22:05:51 +01:00
|
|
|
}
|
2017-03-24 22:56:05 +01:00
|
|
|
}
|
|
|
|
|
2017-03-28 14:54:51 +02:00
|
|
|
// get difference to timestamp
|
|
|
|
function getTimeDiff(timestamp) {
|
|
|
|
const diff = process.hrtime(timestamp);
|
|
|
|
return (diff[0] + diff[1] / 1e9).toFixed(2);
|
|
|
|
}
|
|
|
|
|
2017-03-24 22:56:05 +01:00
|
|
|
// api
|
2017-03-27 22:47:09 +02:00
|
|
|
exports.batchSort = batchSort;
|
2017-03-24 22:56:05 +01:00
|
|
|
exports.moveFile = moveFile;
|
2017-03-27 22:47:09 +02:00
|
|
|
exports.getPathByMetadata = getPathByMetadata;
|
2017-03-24 22:56:05 +01:00
|
|
|
exports.frontFill = frontFill;
|
2017-03-28 14:54:51 +02:00
|
|
|
exports.readDirRecursive = readDirRecursive;
|
2017-03-28 18:11:00 +02:00
|
|
|
exports.getTimeDiff = getTimeDiff;
|