2018-03-03 00:37:21 +01:00
|
|
|
|
/**
|
|
|
|
|
* Made with love & beer by SMLoadrDevs.
|
2018-07-30 22:05:54 +02:00
|
|
|
|
* https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr
|
2018-03-03 00:37:21 +01:00
|
|
|
|
*
|
|
|
|
|
* Feel free to donate :)
|
|
|
|
|
* BTC: 15GktD5M1kCmESyxfhA6EvmhGzWnRA8gvg
|
|
|
|
|
* BTC Cash: 1LpLtLREzTWzba94wBBpJxcv7r6h6u1jgF
|
|
|
|
|
* ETH: 0xd07c98bF53b21c4921E7b30491Fe0B86E714afeD
|
|
|
|
|
* ETH Classic: 0x7b8f83e4cE082BfCe5B6f6E4F204c914e925f242
|
|
|
|
|
* LTC: LXJwhRmjfUruuwp76rJmLrhJJjHSG8TNxm
|
|
|
|
|
* DASH: XmHzFcygcwtqabgfEtJyq9cen1G5EnvuGR
|
|
|
|
|
*/
|
|
|
|
|
|
2019-03-11 18:24:08 +01:00
|
|
|
|
const chalk = require('chalk');
|
|
|
|
|
const ora = require('ora');
|
|
|
|
|
const sanitize = require('sanitize-filename');
|
|
|
|
|
const Promise = require('bluebird');
|
|
|
|
|
const cacheManager = require('cache-manager');
|
|
|
|
|
require('./node_modules/cache-manager/lib/stores/memory');
|
|
|
|
|
const requestPlus = require('request-plus');
|
|
|
|
|
const id3Writer = require('./libs/browser-id3-writer');
|
|
|
|
|
const flacMetadata = require('./libs/flac-metadata');
|
|
|
|
|
const crypto = require('crypto');
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const stream = require('stream');
|
|
|
|
|
const Finder = require('fs-finder');
|
|
|
|
|
const nodePath = require('path');
|
|
|
|
|
const memoryStats = require('./libs/node-memory-stats');
|
|
|
|
|
const commandLineArgs = require('command-line-args');
|
|
|
|
|
const commandLineUsage = require('command-line-usage');
|
|
|
|
|
const nodeJsonFile = require('jsonfile');
|
|
|
|
|
const openUrl = require('openurl');
|
|
|
|
|
const packageJson = require('./package.json');
|
|
|
|
|
|
|
|
|
|
const configFile = 'SMLoadrConfig.json';
|
|
|
|
|
const ConfigService = require('./src/service/ConfigService');
|
|
|
|
|
let configService = new ConfigService(configFile);
|
|
|
|
|
|
|
|
|
|
const Log = require('log');
|
|
|
|
|
|
|
|
|
|
let DOWNLOAD_DIR = 'DOWNLOADS/';
|
|
|
|
|
let PLAYLIST_DIR = 'PLAYLISTS/';
|
|
|
|
|
let PLAYLIST_FILE_ITEMS = {};
|
|
|
|
|
|
|
|
|
|
let DOWNLOAD_LINKS_FILE = 'downloadLinks.txt';
|
|
|
|
|
let DOWNLOAD_MODE = 'single';
|
|
|
|
|
|
|
|
|
|
const log = new Log('debug', fs.createWriteStream('SMLoadr.log'));
|
|
|
|
|
|
|
|
|
|
const musicQualities = {
|
|
|
|
|
MP3_128: {
|
|
|
|
|
id: 1,
|
|
|
|
|
name: 'MP3 - 128 kbps',
|
|
|
|
|
aproxMaxSizeMb: '100'
|
|
|
|
|
},
|
|
|
|
|
MP3_256: {
|
|
|
|
|
id: 5,
|
|
|
|
|
name: 'MP3 - 256 kbps'
|
|
|
|
|
},
|
|
|
|
|
MP3_320: {
|
|
|
|
|
id: 3,
|
|
|
|
|
name: 'MP3 - 320 kbps',
|
|
|
|
|
aproxMaxSizeMb: '200'
|
|
|
|
|
},
|
|
|
|
|
FLAC: {
|
|
|
|
|
id: 9,
|
|
|
|
|
name: 'FLAC - 1411 kbps',
|
|
|
|
|
aproxMaxSizeMb: '700'
|
|
|
|
|
},
|
|
|
|
|
MP3_MISC: {
|
|
|
|
|
id: 0,
|
|
|
|
|
name: 'User uploaded song'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let selectedMusicQuality = musicQualities.MP3_320;
|
|
|
|
|
|
|
|
|
|
const cliOptionDefinitions = [
|
|
|
|
|
{
|
|
|
|
|
name: 'help',
|
|
|
|
|
alias: 'h',
|
|
|
|
|
description: 'Print this usage guide :)'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'quality',
|
|
|
|
|
alias: 'q',
|
|
|
|
|
type: String,
|
|
|
|
|
defaultValue: 'MP3_320',
|
|
|
|
|
description: 'The quality of the files to download: MP3_128/MP3_320/FLAC'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'path',
|
|
|
|
|
alias: 'p',
|
|
|
|
|
type: String,
|
|
|
|
|
defaultValue: DOWNLOAD_DIR,
|
|
|
|
|
description: 'The path to download the files to: path with / in the end'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'url',
|
|
|
|
|
alias: 'u',
|
|
|
|
|
type: String,
|
|
|
|
|
defaultOption: true,
|
|
|
|
|
description: 'Downloads single deezer url: album/artist/playlist/profile/track url'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'downloadmode',
|
|
|
|
|
alias: 'd',
|
|
|
|
|
type: String,
|
|
|
|
|
defaultValue: 'single',
|
|
|
|
|
description: 'Downloads multiple urls from list: "all" for downloadLinks.txt'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let cliOptions;
|
|
|
|
|
const isCli = process.argv.length > 2;
|
|
|
|
|
|
|
|
|
|
const downloadSpinner = new ora({
|
|
|
|
|
spinner: {
|
|
|
|
|
interval: 400,
|
|
|
|
|
frames: [
|
|
|
|
|
'♫',
|
|
|
|
|
' '
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
color: 'white'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const unofficialApiUrl = 'https://www.deezer.com/ajax/gw-light.php';
|
|
|
|
|
const ajaxActionUrl = 'https://www.deezer.com/ajax/action.php';
|
|
|
|
|
|
|
|
|
|
const formLoginData = {
|
|
|
|
|
type: 'login',
|
|
|
|
|
mail: null,
|
|
|
|
|
password: null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let unofficialApiQueries = {
|
|
|
|
|
api_version: '1.0',
|
|
|
|
|
api_token: '',
|
|
|
|
|
input: 3
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let httpHeaders;
|
|
|
|
|
let requestWithoutCache;
|
|
|
|
|
let requestWithoutCacheAndRetry;
|
|
|
|
|
let requestWithCache;
|
|
|
|
|
|
|
|
|
|
function initRequest() {
|
|
|
|
|
httpHeaders = {
|
|
|
|
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',
|
|
|
|
|
'cache-control': 'max-age=0',
|
|
|
|
|
'accept-language': 'en-US,en;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
|
|
|
'accept-charset': 'utf-8,ISO-8859-1;q=0.8,*;q=0.7',
|
|
|
|
|
'content-type': 'text/plain;charset=UTF-8',
|
|
|
|
|
'cookie': 'arl=' + configService.get('arl')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let requestConfig = {
|
|
|
|
|
retry: {
|
|
|
|
|
attempts: 9999999999,
|
|
|
|
|
delay: 1000, // 1 second
|
|
|
|
|
errorFilter: error => 403 !== error.statusCode // retry all errors
|
|
|
|
|
},
|
|
|
|
|
defaults: {
|
|
|
|
|
headers: httpHeaders,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
requestWithoutCache = requestPlus(requestConfig);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let requestConfigWithoutCacheAndRetry = {
|
|
|
|
|
defaults: {
|
|
|
|
|
headers: httpHeaders
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
requestWithoutCacheAndRetry = requestPlus(requestConfigWithoutCacheAndRetry);
|
|
|
|
|
|
|
|
|
|
const cacheManagerCache = cacheManager.caching({
|
|
|
|
|
store: 'memory',
|
|
|
|
|
max: 1000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
requestConfig.cache = {
|
|
|
|
|
cache: cacheManagerCache,
|
|
|
|
|
cacheOptions: {
|
|
|
|
|
ttl: 3600 * 2 // 2 hours
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
requestWithCache = requestPlus(requestConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Application init.
|
|
|
|
|
*/
|
|
|
|
|
(function initApp() {
|
|
|
|
|
process.on('unhandledRejection', (reason, p) => {
|
|
|
|
|
log.debug(reason + 'Unhandled Rejection at Promise' + p);
|
|
|
|
|
console.error('\n' + reason + '\nUnhandled Rejection at Promise' + JSON.stringify(p) + '\n');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
process.on('uncaughtException', (err) => {
|
|
|
|
|
log.debug(err + 'Uncaught Exception thrown');
|
|
|
|
|
console.error('\n' + err + '\nUncaught Exception thrown' + '\n');
|
|
|
|
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Ignore HTTPS certificate
|
|
|
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// App info
|
|
|
|
|
console.log(chalk.cyan('╔══════════════════════════════════════════════════════════════════╗'));
|
|
|
|
|
console.log(chalk.cyan('║') + chalk.bold.yellow(' SMLoadr v' + packageJson.version + ' ') + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('╠══════════════════════════════════════════════════════════════════╣'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' DOWNLOADS: https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr/releases' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' MANUAL: https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' NEWS: https://t.me/SMLoadrNews ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('╠══════════════════════════════════════════════════════════════════╣'));
|
|
|
|
|
console.log(chalk.cyan('║') + chalk.redBright(' ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ DONATE ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ♥ ') + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' BTC: 15GktD5M1kCmESyxfhA6EvmhGzWnRA8gvg ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' BTC Cash: 1LpLtLREzTWzba94wBBpJxcv7r6h6u1jgF ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' ETH: 0xd07c98bF53b21c4921E7b30491Fe0B86E714afeD ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' ETH Classic: 0x7b8f83e4cE082BfCe5B6f6E4F204c914e925f242 ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' LTC: LXJwhRmjfUruuwp76rJmLrhJJjHSG8TNxm ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('║') + ' DASH: XmHzFcygcwtqabgfEtJyq9cen1G5EnvuGR ' + chalk.cyan('║'));
|
|
|
|
|
console.log(chalk.cyan('╚══════════════════════════════════════════════════════════════════╝\n'));
|
|
|
|
|
console.log(chalk.yellow('Please read the latest manual thoroughly before asking for help!\n'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(DOWNLOAD_LINKS_FILE)) {
|
|
|
|
|
ensureDir(DOWNLOAD_LINKS_FILE);
|
|
|
|
|
fs.writeFileSync(DOWNLOAD_LINKS_FILE, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nodePath.normalize(DOWNLOAD_DIR).replace(/\/$|\\$/, '');
|
|
|
|
|
nodePath.normalize(PLAYLIST_DIR).replace(/\/$|\\$/, '');
|
|
|
|
|
|
|
|
|
|
if (isCli) {
|
|
|
|
|
try {
|
|
|
|
|
cliOptions = commandLineArgs(cliOptionDefinitions);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
downloadSpinner.fail(err.message);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startApp();
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start the app.
|
|
|
|
|
*/
|
|
|
|
|
function startApp() {
|
|
|
|
|
initRequest();
|
|
|
|
|
|
|
|
|
|
downloadSpinner.text = 'Checking for update...';
|
|
|
|
|
downloadSpinner.start();
|
|
|
|
|
|
|
|
|
|
isUpdateAvailable().then((response) => {
|
|
|
|
|
if (response) {
|
|
|
|
|
downloadSpinner.warn('New update available!\n Please update to the latest version!');
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
openUrl.open('https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr/releases');
|
|
|
|
|
|
|
|
|
|
if (isCli) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}, 100);
|
|
|
|
|
} else {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// Nothing, only to keep the app running
|
|
|
|
|
}, 999999999);
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
downloadSpinner.succeed('You have the latest version :)');
|
|
|
|
|
|
|
|
|
|
initDeezerCredentials().then(() => {
|
|
|
|
|
downloadSpinner.text = 'Initiating Deezer API...';
|
|
|
|
|
downloadSpinner.start();
|
|
|
|
|
|
|
|
|
|
initDeezerApi().then(() => {
|
|
|
|
|
downloadSpinner.succeed('Connected to Deezer API');
|
|
|
|
|
|
|
|
|
|
selectMusicQuality();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
if ('Wrong Deezer credentials!' === err) {
|
2019-03-12 13:38:26 +01:00
|
|
|
|
downloadSpinner.fail('Wrong Deezer credentials!\n');
|
2019-03-11 18:24:08 +01:00
|
|
|
|
|
|
|
|
|
configService.set('arl', null);
|
|
|
|
|
|
|
|
|
|
configService.saveConfig();
|
|
|
|
|
|
|
|
|
|
startApp();
|
|
|
|
|
} else {
|
|
|
|
|
downloadSpinner.fail(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
downloadSpinner.fail(err);
|
|
|
|
|
|
|
|
|
|
if (isCli) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a new update of the app is available.
|
|
|
|
|
*
|
|
|
|
|
* @returns {Boolean}
|
|
|
|
|
*/
|
|
|
|
|
function isUpdateAvailable() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
log.debug('Checking for update');
|
|
|
|
|
|
|
|
|
|
requestWithoutCacheAndRetry('https://pastebin.com/raw/1FE65caB').then((response) => {
|
|
|
|
|
log.debug('Checked for update on Pastebin. Response: "' + response + '"');
|
|
|
|
|
|
|
|
|
|
if (response !== packageJson.version) {
|
|
|
|
|
resolve(true);
|
|
|
|
|
} else {
|
|
|
|
|
resolve(false);
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
log.debug('Failed checking on pastebin for update. Trying git repo.');
|
|
|
|
|
|
|
|
|
|
requestWithoutCache('https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr/raw/branch/master/VERSION.md?' + Date.now()).then((response) => {
|
|
|
|
|
log.debug('Checked for update on the git repo. Response: "' + response + '"');
|
|
|
|
|
|
|
|
|
|
if (response !== packageJson.version) {
|
|
|
|
|
resolve(true);
|
|
|
|
|
} else {
|
|
|
|
|
resolve(false);
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject('Could not check for update!');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create directories of the given path if they don't exist.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} filePath
|
|
|
|
|
* @return {boolean}
|
|
|
|
|
*/
|
|
|
|
|
function ensureDir(filePath) {
|
|
|
|
|
const dirName = nodePath.dirname(filePath);
|
|
|
|
|
|
|
|
|
|
if (fs.existsSync(dirName)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureDir(dirName);
|
|
|
|
|
fs.mkdirSync(dirName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch and set the api token.
|
|
|
|
|
*/
|
|
|
|
|
function initDeezerApi() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
log.debug('Init Deezer API');
|
|
|
|
|
|
|
|
|
|
requestWithoutCacheAndRetry({
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'deezer.getUserData',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
if (!response || 0 < Object.keys(response.error).length) {
|
|
|
|
|
throw 'Unable to initialize Deezer API.';
|
|
|
|
|
} else {
|
|
|
|
|
if (response.results['USER']['USER_ID'] !== 0) {
|
|
|
|
|
requestWithoutCacheAndRetry({
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'deezer.getUserData',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
if (!response || 0 < Object.keys(response.error).length) {
|
|
|
|
|
throw 'Unable to initialize Deezer API.';
|
|
|
|
|
} else {
|
|
|
|
|
if (response.results && response.results.checkForm) {
|
|
|
|
|
log.debug('Successfully initiated Deezer API. Checkform: "' + response.results.checkForm + '"');
|
|
|
|
|
|
|
|
|
|
unofficialApiQueries.api_token = response.results.checkForm;
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
} else {
|
|
|
|
|
throw 'Unable to initialize Deezer API.';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
if (404 === err.statusCode) {
|
|
|
|
|
err = 'Could not connect to Deezer.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
reject('Wrong Deezer credentials!');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ask and set new Deezer account credentials.
|
|
|
|
|
*/
|
|
|
|
|
function initDeezerCredentials() {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
let arl = configService.get('arl');
|
|
|
|
|
|
|
|
|
|
if (arl) {
|
|
|
|
|
resolve();
|
|
|
|
|
} else {
|
|
|
|
|
console.log(chalk.yellow('\nVisit https://www.deezer.com/register if you don\'t have an account yet.\n'));
|
|
|
|
|
|
|
|
|
|
let questions = [
|
|
|
|
|
{
|
|
|
|
|
type: 'input',
|
|
|
|
|
name: 'arl',
|
|
|
|
|
prefix: '♫',
|
|
|
|
|
message: 'arl cookie:'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
inquirer.prompt(questions).then(answers => {
|
|
|
|
|
configService.set('arl', answers.arl);
|
|
|
|
|
|
|
|
|
|
configService.saveConfig();
|
|
|
|
|
initRequest();
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Encrypt a deezer password.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} deezerEmail
|
|
|
|
|
* @param {String} unencryptedDeezerPassword
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function encryptDeezerPassword(deezerEmail, unencryptedDeezerPassword) {
|
|
|
|
|
try {
|
|
|
|
|
let cipher = crypto.createCipher('aes-256-cbc', deezerEmail + '-SMLoadr');
|
|
|
|
|
let encryptedPassword = cipher.update(unencryptedDeezerPassword, 'utf-8', 'hex');
|
|
|
|
|
encryptedPassword += cipher.final('hex');
|
|
|
|
|
|
|
|
|
|
return encryptedPassword;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrypt an encrypted deezer password.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} deezerEmail
|
|
|
|
|
* @param {String} encryptedDeezerPassword
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function decryptDeezerPassword(deezerEmail, encryptedDeezerPassword) {
|
|
|
|
|
try {
|
|
|
|
|
let decipher = crypto.createDecipher('aes-256-cbc', deezerEmail + '-SMLoadr');
|
|
|
|
|
let decryptedPassword = decipher.update(encryptedDeezerPassword, 'hex', 'utf-8');
|
|
|
|
|
decryptedPassword += decipher.final('utf-8');
|
|
|
|
|
|
|
|
|
|
return decryptedPassword;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a cid for a unofficial api request.
|
|
|
|
|
*
|
|
|
|
|
* @return {Number}
|
|
|
|
|
*/
|
|
|
|
|
function getApiCid() {
|
|
|
|
|
return Math.floor(1e9 * Math.random());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Show user selection for the music download quality.
|
|
|
|
|
*/
|
|
|
|
|
function selectMusicQuality() {
|
|
|
|
|
console.log('');
|
|
|
|
|
|
|
|
|
|
if (isCli) {
|
|
|
|
|
let cliHelp = cliOptions['help'];
|
|
|
|
|
|
|
|
|
|
if (cliHelp || null === cliHelp) {
|
|
|
|
|
const helpSections = [
|
|
|
|
|
{
|
|
|
|
|
header: 'CLI Options',
|
|
|
|
|
optionList: cliOptionDefinitions
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
content: 'More help here: https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr',
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
console.log(commandLineUsage(helpSections));
|
|
|
|
|
process.exit(1);
|
|
|
|
|
} else {
|
|
|
|
|
let cliUrl = cliOptions['url'];
|
|
|
|
|
let cliQuality = cliOptions['quality'];
|
|
|
|
|
let cliPath = cliOptions['path'];
|
|
|
|
|
let cliDownloadMode = cliOptions['downloadmode'];
|
|
|
|
|
|
|
|
|
|
switch (cliQuality) {
|
|
|
|
|
case 'MP3_128':
|
|
|
|
|
selectedMusicQuality = musicQualities.MP3_128;
|
|
|
|
|
break;
|
|
|
|
|
case 'MP3_320':
|
|
|
|
|
selectedMusicQuality = musicQualities.MP3_320;
|
|
|
|
|
break;
|
|
|
|
|
case 'FLAC':
|
|
|
|
|
selectedMusicQuality = musicQualities.FLAC;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DOWNLOAD_DIR = nodePath.normalize(cliPath).replace(/\/$|\\$/, '');
|
|
|
|
|
DOWNLOAD_MODE = cliDownloadMode;
|
|
|
|
|
|
|
|
|
|
downloadSpinner.warn(chalk.yellow('Do not scroll while downloading! This will mess up the UI!'));
|
|
|
|
|
|
|
|
|
|
if ('all' === DOWNLOAD_MODE) {
|
|
|
|
|
downloadLinksFromFile();
|
|
|
|
|
} else if ('single' === DOWNLOAD_MODE) {
|
|
|
|
|
startDownload(cliUrl).then(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}, 100);
|
|
|
|
|
}, 100);
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
downloadSpinner.fail(err);
|
|
|
|
|
downloadStateInstance.finish();
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
inquirer.prompt([
|
|
|
|
|
{
|
|
|
|
|
type: 'list',
|
|
|
|
|
name: 'musicQuality',
|
|
|
|
|
prefix: '♫',
|
|
|
|
|
message: 'Select music quality:',
|
|
|
|
|
choices: [
|
|
|
|
|
'MP3 - 128 kbps',
|
|
|
|
|
'MP3 - 320 kbps',
|
|
|
|
|
'FLAC - 1411 kbps'
|
|
|
|
|
],
|
|
|
|
|
default: 1
|
|
|
|
|
}
|
|
|
|
|
]).then((answers) => {
|
|
|
|
|
switch (answers.musicQuality) {
|
|
|
|
|
case 'MP3 - 128 kbps':
|
|
|
|
|
selectedMusicQuality = musicQualities.MP3_128;
|
|
|
|
|
break;
|
|
|
|
|
case 'MP3 - 320 kbps':
|
|
|
|
|
selectedMusicQuality = musicQualities.MP3_320;
|
|
|
|
|
break;
|
|
|
|
|
case 'FLAC - 1411 kbps':
|
|
|
|
|
selectedMusicQuality = musicQualities.FLAC;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectDownloadMode();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ask for download mode (single or all).
|
|
|
|
|
*/
|
|
|
|
|
function selectDownloadMode() {
|
|
|
|
|
inquirer.prompt([
|
|
|
|
|
{
|
|
|
|
|
type: 'list',
|
|
|
|
|
name: 'downloadMode',
|
|
|
|
|
prefix: '♫',
|
|
|
|
|
message: 'Select download mode:',
|
|
|
|
|
choices: [
|
|
|
|
|
'Single (Download single link)',
|
|
|
|
|
'All (Download all links in "' + DOWNLOAD_LINKS_FILE + '")'
|
|
|
|
|
],
|
|
|
|
|
default: 0
|
|
|
|
|
}
|
|
|
|
|
]).then((answers) => {
|
|
|
|
|
if ('All (Download all links in "' + DOWNLOAD_LINKS_FILE + '")' === answers.downloadMode) {
|
|
|
|
|
console.log('');
|
|
|
|
|
downloadSpinner.warn(chalk.yellow('Do not scroll while downloading! This will mess up the UI!'));
|
|
|
|
|
|
|
|
|
|
downloadLinksFromFile();
|
|
|
|
|
} else {
|
|
|
|
|
askForNewDownload();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download all links from file
|
|
|
|
|
*/
|
|
|
|
|
function downloadLinksFromFile() {
|
|
|
|
|
const lines = fs
|
|
|
|
|
.readFileSync(DOWNLOAD_LINKS_FILE, 'utf-8')
|
|
|
|
|
.split(/^(.*)[\r|\n]/)
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
if (lines[0]) {
|
|
|
|
|
const firstLine = lines[0].trim();
|
|
|
|
|
|
|
|
|
|
if ('' === firstLine) {
|
|
|
|
|
removeFirstLineFromFile(DOWNLOAD_LINKS_FILE);
|
|
|
|
|
downloadLinksFromFile();
|
|
|
|
|
} else {
|
|
|
|
|
startDownload(firstLine, true).then(() => {
|
|
|
|
|
removeFirstLineFromFile(DOWNLOAD_LINKS_FILE);
|
|
|
|
|
downloadLinksFromFile();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
downloadSpinner.fail(err);
|
|
|
|
|
downloadStateInstance.finish(false);
|
|
|
|
|
|
|
|
|
|
removeFirstLineFromFile(DOWNLOAD_LINKS_FILE);
|
|
|
|
|
downloadLinksFromFile();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
downloadSpinner.succeed('Finished downloading from text file');
|
|
|
|
|
|
|
|
|
|
if (isCli) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}, 100);
|
|
|
|
|
} else {
|
|
|
|
|
console.log('\n');
|
|
|
|
|
selectDownloadMode();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove the first line from the given file.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} filePath
|
|
|
|
|
*/
|
|
|
|
|
function removeFirstLineFromFile(filePath) {
|
|
|
|
|
const lines = fs
|
|
|
|
|
.readFileSync(filePath, 'utf-8')
|
|
|
|
|
.split(/^(.*)[\r|\n]/)
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
let contentToWrite = '';
|
|
|
|
|
|
|
|
|
|
if (lines[1]) {
|
|
|
|
|
contentToWrite = lines[1].trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(filePath, contentToWrite);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ask for a album, playlist or track link to start the download.
|
|
|
|
|
*/
|
|
|
|
|
function askForNewDownload() {
|
|
|
|
|
console.log('\n');
|
|
|
|
|
|
|
|
|
|
let questions = [
|
|
|
|
|
{
|
|
|
|
|
type: 'input',
|
|
|
|
|
name: 'deezerUrl',
|
|
|
|
|
prefix: '♫',
|
|
|
|
|
message: 'Deezer URL:',
|
|
|
|
|
validate: (deezerUrl) => {
|
|
|
|
|
if (deezerUrl) {
|
|
|
|
|
let deezerUrlType = getDeezerUrlParts(deezerUrl).type;
|
|
|
|
|
let allowedDeezerUrlTypes = [
|
|
|
|
|
'album',
|
|
|
|
|
'artist',
|
|
|
|
|
'playlist',
|
|
|
|
|
'profile',
|
|
|
|
|
'track'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (allowedDeezerUrlTypes.includes(deezerUrlType)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'Deezer URL example: https://www.deezer.com/album|artist|playlist|profile|track/0123456789';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
inquirer.prompt(questions).then(answers => {
|
|
|
|
|
downloadSpinner.warn(chalk.yellow('Do not scroll while downloading! This will mess up the UI!'));
|
|
|
|
|
|
|
|
|
|
startDownload(answers.deezerUrl).then(() => {
|
|
|
|
|
askForNewDownload();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
downloadSpinner.fail(err);
|
|
|
|
|
downloadStateInstance.finish();
|
|
|
|
|
askForNewDownload();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove empty files.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} filePaths
|
|
|
|
|
*/
|
|
|
|
|
function removeEmptyFiles(filePaths) {
|
|
|
|
|
filePaths.forEach((filePath) => {
|
|
|
|
|
if (fs.existsSync(filePath)) {
|
|
|
|
|
const fileContent = fs.readFileSync(filePath, 'utf-8').trim();
|
|
|
|
|
|
|
|
|
|
if ('' === fileContent) {
|
|
|
|
|
fs.unlinkSync(filePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class downloadState {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.currentlyDownloading = {};
|
|
|
|
|
this.currentlyDownloadingPaths = [];
|
|
|
|
|
this.downloading = false;
|
|
|
|
|
this.numberTracksFinished = 0;
|
|
|
|
|
this.numberTracksToDownload = 0;
|
|
|
|
|
this.downloadType = '';
|
|
|
|
|
this.downloadTypeId = 0;
|
|
|
|
|
this.downloadTypeName = '';
|
|
|
|
|
this.downloadedSuccessfully = null;
|
|
|
|
|
this.downloadedUnsuccessfully = null;
|
|
|
|
|
this.downloadedWithWarning = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start(downloadType, downloadTypeId) {
|
|
|
|
|
this.downloading = true;
|
|
|
|
|
this.downloadType = downloadType;
|
|
|
|
|
this.downloadTypeId = downloadTypeId;
|
|
|
|
|
|
|
|
|
|
this.downloadedSuccessfully = fs.createWriteStream('downloadedSuccessfully.txt', {
|
|
|
|
|
flags: 'a' // 'a' means appending (old data will be preserved)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.downloadedUnsuccessfully = fs.createWriteStream('downloadedUnsuccessfully.txt', {
|
|
|
|
|
flags: 'a' // 'a' means appending (old data will be preserved)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.downloadedWithWarning = fs.createWriteStream('downloadedWithWarning.txt', {
|
|
|
|
|
flags: 'a' // 'a' means appending (old data will be preserved)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.display();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateNumberTracksToDownload(numberTracksToDownload) {
|
|
|
|
|
this.numberTracksToDownload = numberTracksToDownload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finish(showFinishMessage = true) {
|
|
|
|
|
this.downloading = false;
|
|
|
|
|
|
|
|
|
|
if (showFinishMessage) {
|
|
|
|
|
let downloadTypeAndName = this.downloadType;
|
|
|
|
|
|
|
|
|
|
if (this.downloadTypeName) {
|
|
|
|
|
downloadTypeAndName += ' "' + this.downloadTypeName + '"';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadSpinner.succeed('Finished downloading ' + downloadTypeAndName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('-' !== this.downloadTypeId.toString().charAt(0)) {
|
|
|
|
|
this.downloadedSuccessfully.write('https://www.deezer.com/' + this.downloadType + '/' + this.downloadTypeId + '\r\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.downloadedSuccessfully.end();
|
|
|
|
|
this.downloadedUnsuccessfully.end();
|
|
|
|
|
this.downloadedWithWarning.end();
|
|
|
|
|
|
|
|
|
|
removeEmptyFiles([
|
|
|
|
|
'downloadedSuccessfully.txt',
|
|
|
|
|
'downloadedUnsuccessfully.txt',
|
|
|
|
|
'downloadedWithWarning.txt'
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
this.currentlyDownloading = {};
|
|
|
|
|
this.currentlyDownloadingPaths = [];
|
|
|
|
|
this.numberTracksFinished = 0;
|
|
|
|
|
this.numberTracksToDownload = 0;
|
|
|
|
|
this.downloadType = '';
|
|
|
|
|
this.downloadTypeId = 0;
|
|
|
|
|
this.downloadTypeName = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDownloadTypeName(downloadTypeName) {
|
|
|
|
|
this.downloadTypeName = downloadTypeName;
|
|
|
|
|
|
|
|
|
|
this.display();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
add(trackId, message) {
|
|
|
|
|
|
|
|
|
|
this.currentlyDownloading[trackId] = message;
|
|
|
|
|
|
|
|
|
|
this.display();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(trackId, message) {
|
|
|
|
|
this.add(trackId, message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remove(trackId) {
|
|
|
|
|
delete this.currentlyDownloading[trackId];
|
|
|
|
|
|
|
|
|
|
this.display();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
success(trackId, message) {
|
|
|
|
|
downloadSpinner.succeed(message);
|
|
|
|
|
|
|
|
|
|
this.numberTracksFinished++;
|
|
|
|
|
this.remove(trackId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warn(trackId, message) {
|
|
|
|
|
downloadSpinner.warn(message);
|
|
|
|
|
|
|
|
|
|
if ('-' !== trackId.toString().charAt(0)) {
|
|
|
|
|
this.downloadedWithWarning.write('https://www.deezer.com/track/' + trackId + '\r\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.numberTracksFinished++;
|
|
|
|
|
this.remove(trackId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fail(trackId, message) {
|
|
|
|
|
downloadSpinner.fail(message);
|
|
|
|
|
|
|
|
|
|
if ('-' !== trackId.toString().charAt(0)) {
|
|
|
|
|
this.downloadedUnsuccessfully.write('https://www.deezer.com/track/' + trackId + '\r\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.numberTracksFinished++;
|
|
|
|
|
this.remove(trackId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display() {
|
|
|
|
|
if (this.downloading) {
|
|
|
|
|
let downloadTypeAndName = this.downloadType;
|
|
|
|
|
|
|
|
|
|
if (this.downloadTypeName) {
|
|
|
|
|
downloadTypeAndName += ' "' + this.downloadTypeName + '"';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let finishedPercentage = '0.00';
|
|
|
|
|
|
|
|
|
|
if (0 !== this.numberTracksToDownload) {
|
|
|
|
|
finishedPercentage = (this.numberTracksFinished / this.numberTracksToDownload * 100).toFixed(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let downloadSpinnerText = chalk.green('Downloading ' + downloadTypeAndName + ' [' + this.numberTracksFinished + '/' + this.numberTracksToDownload + ' - ' + finishedPercentage + '%]:\n');
|
|
|
|
|
|
|
|
|
|
if (0 < Object.keys(this.currentlyDownloading).length) {
|
|
|
|
|
downloadSpinnerText += ' › ' + Object.values(this.currentlyDownloading).join('\n › ');
|
|
|
|
|
} else {
|
|
|
|
|
downloadSpinnerText += ' › Fetching infos...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadSpinner.start(downloadSpinnerText);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addCurrentlyDownloadingPath(downloadPath) {
|
|
|
|
|
this.currentlyDownloadingPaths.push(downloadPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeCurrentlyDownloadingPath(downloadPath) {
|
|
|
|
|
const index = this.currentlyDownloadingPaths.indexOf(downloadPath);
|
|
|
|
|
|
|
|
|
|
if (-1 !== index) {
|
|
|
|
|
this.currentlyDownloadingPaths.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isCurrentlyDownloadingPathUsed(downloadPath) {
|
|
|
|
|
return (this.currentlyDownloadingPaths.indexOf(downloadPath) > -1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let downloadStateInstance = new downloadState();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start a deezer download.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} deezerUrl
|
|
|
|
|
* @param {Boolean} downloadFromFile
|
|
|
|
|
*/
|
|
|
|
|
function startDownload(deezerUrl, downloadFromFile = false) {
|
|
|
|
|
log.debug('------------------------------------------');
|
|
|
|
|
log.debug('Started download task: "' + deezerUrl + '"');
|
|
|
|
|
|
|
|
|
|
const deezerUrlParts = getDeezerUrlParts(deezerUrl);
|
|
|
|
|
|
|
|
|
|
downloadStateInstance.start(deezerUrlParts.type, deezerUrlParts.id);
|
|
|
|
|
|
|
|
|
|
switch (deezerUrlParts.type) {
|
|
|
|
|
case 'album':
|
|
|
|
|
case 'playlist':
|
|
|
|
|
case 'profile':
|
|
|
|
|
return downloadMultiple(deezerUrlParts.type, deezerUrlParts.id).then(() => {
|
|
|
|
|
downloadStateInstance.finish(!downloadFromFile);
|
|
|
|
|
});
|
|
|
|
|
case 'artist':
|
|
|
|
|
return downloadArtist(deezerUrlParts.id).then(() => {
|
|
|
|
|
downloadStateInstance.finish(!downloadFromFile);
|
|
|
|
|
});
|
|
|
|
|
case 'track':
|
|
|
|
|
downloadStateInstance.updateNumberTracksToDownload(1);
|
|
|
|
|
|
|
|
|
|
return downloadSingleTrack(deezerUrlParts.id).then(() => {
|
|
|
|
|
downloadStateInstance.finish(!downloadFromFile);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the url type (album/artist/playlist/profile/track) and the id from the deezer url.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} deezerUrl
|
|
|
|
|
*
|
|
|
|
|
* @return {Object}
|
|
|
|
|
*/
|
|
|
|
|
function getDeezerUrlParts(deezerUrl) {
|
|
|
|
|
const urlParts = deezerUrl.split(/\/(\w+)\/(\d+)/);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: urlParts[1],
|
|
|
|
|
id: urlParts[2]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download all tracks of an artists.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
*/
|
|
|
|
|
function downloadArtist(id) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
let requestParams = {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'artist.getData',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
body: {
|
|
|
|
|
art_id: id,
|
|
|
|
|
filter_role_id: [0],
|
|
|
|
|
lang: 'us',
|
|
|
|
|
tab: 0,
|
|
|
|
|
nb: -1,
|
|
|
|
|
start: 0
|
|
|
|
|
},
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
requestWithCache(requestParams).then((response) => {
|
|
|
|
|
if (!response || 0 < Object.keys(response.error).length) {
|
|
|
|
|
if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
downloadArtist(id).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
throw 'Could not fetch the artist!';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log.debug('Got artist infos for "artist/' + id + '"');
|
|
|
|
|
|
|
|
|
|
const artistName = response.results.ART_NAME;
|
|
|
|
|
downloadStateInstance.setDownloadTypeName(artistName);
|
|
|
|
|
|
|
|
|
|
requestParams.qs.method = 'album.getDiscography';
|
|
|
|
|
requestParams.qs.cid = getApiCid();
|
|
|
|
|
requestParams.body = {
|
|
|
|
|
art_id: id,
|
|
|
|
|
filter_role_id: [0],
|
|
|
|
|
lang: 'us',
|
|
|
|
|
nb: 500,
|
|
|
|
|
nb_songs: -1,
|
|
|
|
|
start: 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
requestWithoutCache(requestParams).then((response) => {
|
|
|
|
|
if (!response || 0 < Object.keys(response.error).length) {
|
|
|
|
|
if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
downloadArtist(id).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
throw 'Could not fetch "' + artistName + '" albums!';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log.debug('Got all albums for "artist/' + id + '"');
|
|
|
|
|
|
|
|
|
|
if (0 < response.results.data.length) {
|
|
|
|
|
let trackList = [];
|
|
|
|
|
let albumList = {};
|
|
|
|
|
|
|
|
|
|
response.results.data.forEach((album) => {
|
|
|
|
|
albumList[album.ALB_ID] = album;
|
|
|
|
|
|
|
|
|
|
album.SONGS.data.forEach((track) => {
|
|
|
|
|
trackList.push(track);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
downloadStateInstance.updateNumberTracksToDownload(trackList.length);
|
|
|
|
|
|
|
|
|
|
trackListDownload(trackList, albumList).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
downloadSpinner.warn('No tracks to download for artist "' + artistName + '"');
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download multiple tracks (album, playlist or users favourite tracks)
|
|
|
|
|
*
|
|
|
|
|
* @param {String} type
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
*/
|
|
|
|
|
function downloadMultiple(type, id) {
|
|
|
|
|
let requestBody;
|
|
|
|
|
let requestQueries = unofficialApiQueries;
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'album':
|
|
|
|
|
requestQueries.method = 'deezer.pageAlbum';
|
|
|
|
|
requestBody = {
|
|
|
|
|
alb_id: id,
|
|
|
|
|
lang: 'en',
|
|
|
|
|
tab: 0
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'playlist':
|
|
|
|
|
requestQueries.method = 'deezer.pagePlaylist';
|
|
|
|
|
requestBody = {
|
|
|
|
|
playlist_id: id,
|
|
|
|
|
lang: 'en',
|
|
|
|
|
nb: -1,
|
|
|
|
|
start: 0,
|
|
|
|
|
tab: 0,
|
|
|
|
|
tags: true,
|
|
|
|
|
header: true
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'profile':
|
|
|
|
|
requestQueries.method = 'deezer.pageProfile';
|
|
|
|
|
requestBody = {
|
|
|
|
|
user_id: id,
|
|
|
|
|
tab: 'loved',
|
|
|
|
|
nb: -1
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let requestParams = {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: requestQueries,
|
|
|
|
|
body: requestBody,
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let request = requestWithoutCache;
|
|
|
|
|
|
|
|
|
|
if (!['playlist', 'profile'].includes(type)) {
|
|
|
|
|
request = requestWithCache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
request(requestParams).then((response) => {
|
|
|
|
|
if (!response || 0 < Object.keys(response.error).length || ('playlist' === type && 1 === Number(response.results.DATA.STATUS) && 0 < response.results.DATA.DURATION && 0 === response.results.SONGS.data.length)) {
|
|
|
|
|
if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
downloadMultiple(type, id).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else if ('playlist' === type && response.results && response.results.DATA && 1 === Number(response.results.DATA.STATUS && 0 < response.results.DATA.DURATION && 0 === response.results.SONGS.data.length)) {
|
|
|
|
|
throw 'Other users private playlists are not supported!';
|
|
|
|
|
} else {
|
|
|
|
|
throw 'Could not fetch the ' + type + '!';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log.debug('Got track list for "' + type + '/' + id + '"');
|
|
|
|
|
|
|
|
|
|
let trackList = [];
|
|
|
|
|
let albumList = {};
|
|
|
|
|
let downloadTypeName = '';
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'album':
|
|
|
|
|
trackList = response.results.SONGS.data;
|
|
|
|
|
|
|
|
|
|
response.results.DATA.SONGS = response.results.SONGS;
|
|
|
|
|
albumList[response.results.DATA.ALB_ID] = response.results.DATA;
|
|
|
|
|
|
|
|
|
|
downloadTypeName = response.results.DATA.ALB_TITLE;
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
case 'playlist':
|
|
|
|
|
trackList = response.results.SONGS.data;
|
|
|
|
|
downloadTypeName = response.results.DATA.TITLE;
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
case 'profile':
|
|
|
|
|
trackList = response.results.TAB.loved.data;
|
|
|
|
|
downloadTypeName = response.results.DATA.USER.DISPLAY_NAME;
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadStateInstance.setDownloadTypeName(downloadTypeName);
|
|
|
|
|
|
|
|
|
|
if (0 < trackList.length) {
|
|
|
|
|
// We don't want to generate a playlist file if this is no playlist
|
|
|
|
|
if (['profile', 'album'].includes(type)) {
|
|
|
|
|
PLAYLIST_FILE_ITEMS = null;
|
|
|
|
|
} else {
|
|
|
|
|
PLAYLIST_FILE_ITEMS = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadStateInstance.updateNumberTracksToDownload(trackList.length);
|
|
|
|
|
|
|
|
|
|
trackListDownload(trackList, albumList).then(() => {
|
|
|
|
|
// Generate the playlist file
|
|
|
|
|
if (PLAYLIST_FILE_ITEMS != null) {
|
|
|
|
|
const playlistName = multipleWhitespacesToSingle(sanitizeFilename(response.results.DATA.TITLE));
|
|
|
|
|
const playlistFile = nodePath.join(PLAYLIST_DIR, playlistName + '.m3u8');
|
|
|
|
|
let playlistFileContent = '';
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < PLAYLIST_FILE_ITEMS.length; i++) {
|
|
|
|
|
playlistFileContent += PLAYLIST_FILE_ITEMS[i] + '\r\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackList.forEach((trackInfos) => {
|
|
|
|
|
if (PLAYLIST_FILE_ITEMS[trackInfos.SNG_ID]) {
|
|
|
|
|
const playlistFileItem = PLAYLIST_FILE_ITEMS[trackInfos.SNG_ID];
|
|
|
|
|
|
|
|
|
|
playlistFileContent += '#EXTINF:' + playlistFileItem.trackDuration + ',' + playlistFileItem.trackArtist + ' - ' + playlistFileItem.trackTitle + '\r\n';
|
|
|
|
|
playlistFileContent += '../' + playlistFileItem.trackSavePath + '\r\n';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ensureDir(playlistFile);
|
|
|
|
|
fs.writeFileSync(playlistFile, playlistFileContent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
downloadSpinner.warn('No tracks to download for ' + type + ' "' + downloadTypeName + '"');
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the number of parallel downloads to use for the current available memory and selected quality.
|
|
|
|
|
*
|
|
|
|
|
* @return {Number}
|
|
|
|
|
*/
|
|
|
|
|
function getNumberOfParallelDownloads() {
|
|
|
|
|
let freeMemoryMb;
|
|
|
|
|
const approxMaxSizeMb = selectedMusicQuality.aproxMaxSizeMb;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
freeMemoryMb = memoryStats.free() / 1024 / 1024;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
freeMemoryMb = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let numberOfParallel = parseInt(((freeMemoryMb - 300) / approxMaxSizeMb).toString());
|
|
|
|
|
|
|
|
|
|
if (20 < numberOfParallel) {
|
|
|
|
|
numberOfParallel = 20;
|
|
|
|
|
} else if (1 > numberOfParallel) {
|
|
|
|
|
numberOfParallel = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return numberOfParallel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map through a track list and download it.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackList
|
|
|
|
|
* @param {Object} albumInfos
|
|
|
|
|
*/
|
|
|
|
|
function trackListDownload(trackList, albumInfos = {}) {
|
|
|
|
|
const numberOfParallel = getNumberOfParallelDownloads();
|
|
|
|
|
|
|
|
|
|
return Promise.map(trackList, (trackInfos) => {
|
|
|
|
|
let trackAlbumInfos;
|
|
|
|
|
|
|
|
|
|
if (albumInfos[trackInfos.ALB_ID]) {
|
|
|
|
|
trackAlbumInfos = albumInfos[trackInfos.ALB_ID];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackInfos.SNG_TITLE_VERSION = trackInfos.SNG_TITLE;
|
|
|
|
|
|
|
|
|
|
if (trackInfos.VERSION) {
|
|
|
|
|
trackInfos.SNG_TITLE_VERSION = (trackInfos.SNG_TITLE + ' ' + trackInfos.VERSION).trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let artistName = trackInfos.ART_NAME;
|
|
|
|
|
|
|
|
|
|
if (trackAlbumInfos && '' !== trackAlbumInfos.ART_NAME) {
|
|
|
|
|
artistName = trackAlbumInfos.ART_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artistName = multipleWhitespacesToSingle(sanitizeFilename(artistName));
|
|
|
|
|
|
|
|
|
|
if ('' === artistName.trim()) {
|
|
|
|
|
artistName = 'Unknown artist';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('various' === artistName.trim().toLowerCase()) {
|
|
|
|
|
artistName = 'Various Artists';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let albumName = multipleWhitespacesToSingle(sanitizeFilename(trackInfos.ALB_TITLE));
|
|
|
|
|
|
|
|
|
|
if ('' === albumName.trim()) {
|
|
|
|
|
albumName = 'Unknown album';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
albumName += ' (Album)';
|
|
|
|
|
|
|
|
|
|
let saveFileDir = nodePath.join(DOWNLOAD_DIR, artistName, albumName);
|
|
|
|
|
|
|
|
|
|
if (trackAlbumInfos && trackAlbumInfos.SONGS && trackAlbumInfos.SONGS.data && 0 < trackAlbumInfos.SONGS.data.length && '' !== trackAlbumInfos.SONGS.data[trackAlbumInfos.SONGS.data.length - 1].DISK_NUMBER) {
|
|
|
|
|
const albumNumberOfDisks = trackAlbumInfos.SONGS.data[trackAlbumInfos.SONGS.data.length - 1].DISK_NUMBER;
|
|
|
|
|
|
|
|
|
|
if (albumNumberOfDisks > 1) {
|
|
|
|
|
saveFileDir += nodePath.join(saveFileDir, 'Disc ' + toTwoDigits(trackInfos.DISK_NUMBER));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let saveFileName = multipleWhitespacesToSingle(sanitizeFilename(toTwoDigits(trackInfos.TRACK_NUMBER) + ' ' + trackInfos.SNG_TITLE_VERSION));
|
|
|
|
|
let fileExtension = 'mp3';
|
|
|
|
|
|
|
|
|
|
if (musicQualities.FLAC.id === selectedMusicQuality.id) {
|
|
|
|
|
fileExtension = 'flac';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const downloadingMessage = artistName + ' - ' + trackInfos.SNG_TITLE_VERSION;
|
|
|
|
|
downloadStateInstance.add(trackInfos.SNG_ID, downloadingMessage);
|
|
|
|
|
|
|
|
|
|
if (fs.existsSync(saveFileDir)) {
|
|
|
|
|
let files = Finder.from(saveFileDir).findFiles(saveFileName + '.' + fileExtension);
|
|
|
|
|
|
|
|
|
|
if (0 < files.length) {
|
|
|
|
|
addTrackToPlaylist(files[0], trackInfos);
|
|
|
|
|
|
|
|
|
|
const warningMessage = artistName + ' - ' + trackInfos.SNG_TITLE_VERSION + ' \n › Song already exists';
|
|
|
|
|
downloadStateInstance.success(trackInfos.SNG_ID, warningMessage);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return downloadSingleTrack(trackInfos.SNG_ID, trackInfos, trackAlbumInfos);
|
|
|
|
|
}, {
|
|
|
|
|
concurrency: numberOfParallel
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download a track + id3tags (album cover...) and save it in the downloads folder.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
* @param {Object} albumInfos
|
|
|
|
|
* @param {Boolean} isAlternativeTrack
|
|
|
|
|
* @param {Number} numberRetry
|
|
|
|
|
*/
|
|
|
|
|
function downloadSingleTrack(id, trackInfos = {}, albumInfos = {}, isAlternativeTrack = false, numberRetry = 0) {
|
|
|
|
|
let dirPath;
|
|
|
|
|
let saveFilePath;
|
|
|
|
|
let originalTrackInfos;
|
|
|
|
|
let fileExtension = 'mp3';
|
|
|
|
|
let trackQuality;
|
|
|
|
|
|
|
|
|
|
log.debug('Start downloading "track/' + id + '"');
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
if ('-' === id.toString().charAt(0) && 0 < Object.keys(trackInfos).length) {
|
|
|
|
|
getTrackAlternative(trackInfos).then((alternativeTrackInfos) => {
|
|
|
|
|
downloadStateInstance.remove(id);
|
|
|
|
|
|
|
|
|
|
log.debug('Using alternative "track/' + alternativeTrackInfos.SNG_ID + '" for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
downloadSingleTrack(alternativeTrackInfos.SNG_ID, {}, {}, true).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
startTrackInfoFetching();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
startTrackInfoFetching();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startTrackInfoFetching() {
|
|
|
|
|
if (!isAlternativeTrack && 0 < Object.keys(trackInfos).length) {
|
|
|
|
|
originalTrackInfos = trackInfos;
|
|
|
|
|
|
|
|
|
|
afterTrackInfoFetching();
|
|
|
|
|
} else {
|
|
|
|
|
getTrackInfos(id).then((trackInfosResponse) => {
|
|
|
|
|
originalTrackInfos = trackInfosResponse;
|
|
|
|
|
|
|
|
|
|
afterTrackInfoFetching();
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
errorHandling(err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function afterTrackInfoFetching() {
|
|
|
|
|
if (!isAlternativeTrack || 0 === Object.keys(trackInfos).length) {
|
|
|
|
|
trackInfos = originalTrackInfos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackQuality = getValidTrackQuality(originalTrackInfos);
|
|
|
|
|
|
|
|
|
|
originalTrackInfos.SNG_TITLE_VERSION = originalTrackInfos.SNG_TITLE;
|
|
|
|
|
|
|
|
|
|
if (originalTrackInfos.VERSION) {
|
|
|
|
|
originalTrackInfos.SNG_TITLE_VERSION = (originalTrackInfos.SNG_TITLE + ' ' + originalTrackInfos.VERSION).trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < Object.keys(albumInfos).length || 0 === trackInfos.ALB_ID) {
|
|
|
|
|
afterAlbumInfoFetching();
|
|
|
|
|
} else {
|
|
|
|
|
const downloadingMessage = trackInfos.ART_NAME + ' - ' + trackInfos.SNG_TITLE_VERSION;
|
|
|
|
|
downloadStateInstance.update(originalTrackInfos.SNG_ID, downloadingMessage);
|
|
|
|
|
|
|
|
|
|
getAlbumInfos(trackInfos.ALB_ID).then((albumInfosResponse) => {
|
|
|
|
|
albumInfos = albumInfosResponse;
|
|
|
|
|
|
|
|
|
|
albumInfos.TYPE = 'album';
|
|
|
|
|
albumInfos.GENRES = [];
|
|
|
|
|
|
|
|
|
|
afterAlbumInfoFetching();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
afterAlbumInfoFetching();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function afterAlbumInfoFetching() {
|
|
|
|
|
originalTrackInfos.ALB_UPC = '';
|
|
|
|
|
originalTrackInfos.ALB_LABEL = '';
|
|
|
|
|
originalTrackInfos.ALB_NUM_TRACKS = '';
|
|
|
|
|
originalTrackInfos.ALB_NUM_DISCS = '';
|
|
|
|
|
|
|
|
|
|
if (albumInfos.UPC) {
|
|
|
|
|
originalTrackInfos.ALB_UPC = albumInfos.UPC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (albumInfos.PHYSICAL_RELEASE_DATE && !trackInfos.ALB_RELEASE_DATE) {
|
|
|
|
|
originalTrackInfos.ALB_RELEASE_DATE = albumInfos.PHYSICAL_RELEASE_DATE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (albumInfos.SONGS && 0 < albumInfos.SONGS.data.length && albumInfos.SONGS.data[albumInfos.SONGS.data.length - 1].DISK_NUMBER) {
|
|
|
|
|
originalTrackInfos.ALB_NUM_DISCS = albumInfos.SONGS.data[albumInfos.SONGS.data.length - 1].DISK_NUMBER;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
originalTrackInfos.ALB_ART_NAME = originalTrackInfos.ART_NAME;
|
|
|
|
|
|
|
|
|
|
if (albumInfos.ART_NAME) {
|
|
|
|
|
originalTrackInfos.ALB_ART_NAME = albumInfos.ART_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!originalTrackInfos.ARTISTS || 0 === originalTrackInfos.ARTISTS.length) {
|
|
|
|
|
originalTrackInfos.ARTISTS = [
|
|
|
|
|
{
|
|
|
|
|
ART_ID: originalTrackInfos.ART_ID,
|
|
|
|
|
ART_NAME: originalTrackInfos.ALB_ART_NAME,
|
|
|
|
|
ART_PICTURE: originalTrackInfos.ART_PICTURE
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('various' === originalTrackInfos.ALB_ART_NAME.trim().toLowerCase()) {
|
|
|
|
|
originalTrackInfos.ALB_ART_NAME = 'Various Artists';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (albumInfos.LABEL_NAME) {
|
|
|
|
|
originalTrackInfos.ALB_LABEL = albumInfos.LABEL_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (albumInfos.SONGS && albumInfos.SONGS.data.length) {
|
|
|
|
|
originalTrackInfos.ALB_NUM_TRACKS = albumInfos.SONGS.data.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const downloadingMessage = trackInfos.ALB_ART_NAME + ' - ' + trackInfos.SNG_TITLE_VERSION;
|
|
|
|
|
downloadStateInstance.update(originalTrackInfos.SNG_ID, downloadingMessage);
|
|
|
|
|
|
|
|
|
|
if (0 === trackInfos.ALB_ID) {
|
|
|
|
|
afterAlbumInfoOfficialApiFetching();
|
|
|
|
|
} else {
|
|
|
|
|
getAlbumInfosOfficialApi(trackInfos.ALB_ID).then((albumInfosResponse) => {
|
|
|
|
|
albumInfos.TYPE = albumInfosResponse.record_type;
|
|
|
|
|
albumInfos.GENRES = [];
|
|
|
|
|
|
|
|
|
|
albumInfosResponse.genres.data.forEach((albumGenre) => {
|
|
|
|
|
albumInfos.GENRES.push(albumGenre.name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAlbumInfoOfficialApiFetching();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
afterAlbumInfoOfficialApiFetching();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function afterAlbumInfoOfficialApiFetching() {
|
|
|
|
|
originalTrackInfos.ALB_GENRES = albumInfos.GENRES;
|
|
|
|
|
|
|
|
|
|
if (albumInfos.TYPE) {
|
|
|
|
|
originalTrackInfos.ALB_RELEASE_TYPE = albumInfos.TYPE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isAlternativeTrack) {
|
|
|
|
|
trackInfos.DURATION = originalTrackInfos.DURATION;
|
|
|
|
|
trackInfos.GAIN = originalTrackInfos.GAIN;
|
|
|
|
|
trackInfos.LYRICS_ID = originalTrackInfos.LYRICS_ID;
|
|
|
|
|
trackInfos.LYRICS = originalTrackInfos.LYRICS;
|
|
|
|
|
} else {
|
|
|
|
|
trackInfos = originalTrackInfos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackQuality) {
|
|
|
|
|
let artistName = multipleWhitespacesToSingle(sanitizeFilename(trackInfos.ALB_ART_NAME));
|
|
|
|
|
|
|
|
|
|
if ('' === artistName.trim()) {
|
|
|
|
|
artistName = 'Unknown artist';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let albumType = 'Album';
|
|
|
|
|
|
|
|
|
|
if (albumInfos.TYPE) {
|
|
|
|
|
albumType = albumInfos.TYPE.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if ('ep' === albumType) {
|
|
|
|
|
albumType = 'EP';
|
|
|
|
|
} else {
|
|
|
|
|
albumType = capitalizeFirstLetter(albumType);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let albumName = multipleWhitespacesToSingle(sanitizeFilename(trackInfos.ALB_TITLE));
|
|
|
|
|
|
|
|
|
|
if ('' === albumName.trim()) {
|
|
|
|
|
albumName = 'Unknown album';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
albumName += ' (' + albumType + ')';
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_NUM_DISCS > 1) {
|
|
|
|
|
dirPath = nodePath.join(DOWNLOAD_DIR, artistName, albumName, 'Disc ' + toTwoDigits(trackInfos.DISK_NUMBER));
|
|
|
|
|
} else {
|
|
|
|
|
dirPath = nodePath.join(DOWNLOAD_DIR, artistName, albumName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (musicQualities.FLAC.id === trackQuality.id) {
|
|
|
|
|
fileExtension = 'flac';
|
|
|
|
|
}
|
|
|
|
|
saveFilePath = dirPath + nodePath.sep;
|
|
|
|
|
|
|
|
|
|
if (trackInfos.TRACK_NUMBER) {
|
|
|
|
|
saveFilePath += toTwoDigits(trackInfos.TRACK_NUMBER) + ' ';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveFilePath += multipleWhitespacesToSingle(sanitizeFilename(trackInfos.SNG_TITLE_VERSION));
|
|
|
|
|
|
|
|
|
|
saveFilePath += '.' + fileExtension;
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(saveFilePath) && !downloadStateInstance.isCurrentlyDownloadingPathUsed(saveFilePath)) {
|
|
|
|
|
downloadStateInstance.addCurrentlyDownloadingPath(saveFilePath);
|
|
|
|
|
|
|
|
|
|
return downloadTrack(originalTrackInfos, trackQuality.id, saveFilePath).then((decryptedTrackBuffer) => {
|
|
|
|
|
onTrackDownloadComplete(decryptedTrackBuffer);
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
log.debug('Failed downloading "track/' + trackInfos.SNG_ID + '". Error: "' + error + '"');
|
|
|
|
|
|
|
|
|
|
if (originalTrackInfos.FALLBACK && originalTrackInfos.FALLBACK.SNG_ID && trackInfos.SNG_ID !== originalTrackInfos.FALLBACK.SNG_ID && originalTrackInfos.SNG_ID !== originalTrackInfos.FALLBACK.SNG_ID) {
|
|
|
|
|
downloadStateInstance.removeCurrentlyDownloadingPath(saveFilePath);
|
|
|
|
|
downloadStateInstance.remove(originalTrackInfos.SNG_ID);
|
|
|
|
|
|
|
|
|
|
log.debug('Using alternative "track/' + originalTrackInfos.FALLBACK.SNG_ID + '" for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
downloadSingleTrack(originalTrackInfos.FALLBACK.SNG_ID, trackInfos, albumInfos, true).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const error = {
|
|
|
|
|
message: '-',
|
|
|
|
|
name: 'notAvailableButAlternative'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
errorHandling(error);
|
|
|
|
|
} else {
|
|
|
|
|
getTrackAlternative(trackInfos).then((alternativeTrackInfos) => {
|
|
|
|
|
downloadStateInstance.removeCurrentlyDownloadingPath(saveFilePath);
|
|
|
|
|
downloadStateInstance.remove(originalTrackInfos.SNG_ID);
|
|
|
|
|
|
|
|
|
|
log.debug('Using alternative "track/' + alternativeTrackInfos.SNG_ID + '" for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
if (albumInfos.ALB_TITLE) {
|
|
|
|
|
albumInfos = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadSingleTrack(alternativeTrackInfos.SNG_ID, trackInfos, albumInfos, true).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
const errorMessage = trackInfos.ALB_ART_NAME + ' - ' + trackInfos.SNG_TITLE_VERSION + '\n › Deezer doesn\'t provide the song anymore';
|
|
|
|
|
|
|
|
|
|
errorHandling(errorMessage);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
addTrackToPlaylist(saveFilePath, trackInfos);
|
|
|
|
|
|
|
|
|
|
const error = {
|
|
|
|
|
message: trackInfos.ALB_ART_NAME + ' - ' + trackInfos.SNG_TITLE_VERSION + ' \n › Song already exists',
|
|
|
|
|
name: 'songAlreadyExists'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
errorHandling(error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
errorHandling(trackInfos.ALB_ART_NAME + ' - ' + trackInfos.SNG_TITLE_VERSION + '\n › Deezer doesn\'t provide the song anymore');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTrackDownloadComplete(decryptedTrackBuffer) {
|
|
|
|
|
let downloadMessageAppend = '';
|
|
|
|
|
|
|
|
|
|
if (isAlternativeTrack && originalTrackInfos.SNG_TITLE_VERSION.trim().toLowerCase() !== trackInfos.SNG_TITLE_VERSION.trim().toLowerCase()) {
|
|
|
|
|
downloadMessageAppend = '\n › Used "' + originalTrackInfos.ALB_ART_NAME + ' - ' + originalTrackInfos.SNG_TITLE_VERSION + '" as alternative';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackQuality !== selectedMusicQuality) {
|
|
|
|
|
let selectedMusicQualityName = musicQualities[Object.keys(musicQualities).find(key => musicQualities[key] === selectedMusicQuality)].name;
|
|
|
|
|
let trackQualityName = musicQualities[Object.keys(musicQualities).find(key => musicQualities[key] === trackQuality)].name;
|
|
|
|
|
|
|
|
|
|
downloadMessageAppend += '\n › Used "' + trackQualityName + '" because "' + selectedMusicQualityName + '" wasn\'t available';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const successMessage = '' + trackInfos.ALB_ART_NAME + ' - ' + trackInfos.SNG_TITLE_VERSION + '' + downloadMessageAppend;
|
|
|
|
|
|
|
|
|
|
addTrackTags(decryptedTrackBuffer, trackInfos, saveFilePath).then(() => {
|
|
|
|
|
downloadStateInstance.success(originalTrackInfos.SNG_ID, successMessage);
|
|
|
|
|
|
|
|
|
|
downloadStateInstance.removeCurrentlyDownloadingPath(saveFilePath);
|
|
|
|
|
addTrackToPlaylist(saveFilePath, trackInfos);
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
const warningMessage = successMessage + '\n › Failed writing ID3 tags';
|
|
|
|
|
downloadStateInstance.warn(originalTrackInfos.SNG_ID, warningMessage);
|
|
|
|
|
|
|
|
|
|
downloadStateInstance.removeCurrentlyDownloadingPath(saveFilePath);
|
|
|
|
|
addTrackToPlaylist(saveFilePath, trackInfos);
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function errorHandling(err) {
|
|
|
|
|
if (404 === err.statusCode) {
|
|
|
|
|
err = 'Track "' + id + '" not found';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (err.name && err.message) {
|
|
|
|
|
if ('-' !== err.message) {
|
|
|
|
|
if ('songAlreadyExists' === err.name) {
|
|
|
|
|
downloadStateInstance.success(originalTrackInfos.SNG_ID, err.message);
|
|
|
|
|
} else {
|
|
|
|
|
downloadStateInstance.fail(originalTrackInfos.SNG_ID, err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
downloadStateInstance.fail(id, err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('notAvailableButAlternative' !== err.name && 'invalidApiToken' !== err.name) {
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get track infos of a song by id.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
*/
|
|
|
|
|
function getTrackInfos(id) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
return requestWithCache({
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'deezer.pageTrack',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
body: {
|
|
|
|
|
sng_id: id
|
|
|
|
|
},
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
log.debug('Got track infos for "track/' + id + '"');
|
|
|
|
|
|
|
|
|
|
if (response && 0 === Object.keys(response.error).length && response.results && response.results.DATA) {
|
|
|
|
|
let trackInfos = response.results.DATA;
|
|
|
|
|
|
|
|
|
|
if (response.results.LYRICS) {
|
|
|
|
|
trackInfos.LYRICS = response.results.LYRICS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve(trackInfos);
|
|
|
|
|
} else if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
getTrackInfos(id).then((trackInfos) => {
|
|
|
|
|
resolve(trackInfos);
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get alternative track for a song by its track infos.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
*/
|
|
|
|
|
function getTrackAlternative(trackInfos) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
return requestWithCache({
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'search.music',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
body: {
|
|
|
|
|
QUERY: 'artist:\'' + trackInfos.ART_NAME + '\' track:\'' + trackInfos.SNG_TITLE + '\'',
|
|
|
|
|
OUTPUT: 'TRACK',
|
|
|
|
|
NB: 50,
|
|
|
|
|
FILTER: 0
|
|
|
|
|
},
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
log.debug('Got alternative track for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
if (response && 0 === Object.keys(response.error).length && response.results && response.results.data && 0 < response.results.data.length) {
|
|
|
|
|
const foundTracks = response.results.data;
|
|
|
|
|
let matchingTracks = [];
|
|
|
|
|
if (foundTracks.length > 0) {
|
|
|
|
|
foundTracks.forEach((foundTrack) => {
|
|
|
|
|
if (trackInfos.MD5_ORIGIN === foundTrack.MD5_ORIGIN && trackInfos.DURATION - 5 <= foundTrack.DURATION && trackInfos.DURATION + 10 >= foundTrack.DURATION) {
|
|
|
|
|
matchingTracks.push(foundTrack);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (1 === matchingTracks.length) {
|
|
|
|
|
resolve(matchingTracks[0]);
|
|
|
|
|
} else {
|
|
|
|
|
let foundAlternativeTrack = false;
|
|
|
|
|
|
|
|
|
|
if (0 === matchingTracks.length) {
|
|
|
|
|
foundTracks.forEach((foundTrack) => {
|
|
|
|
|
if (trackInfos.MD5_ORIGIN === foundTrack.MD5_ORIGIN) {
|
|
|
|
|
matchingTracks.push(foundTrack);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchingTracks.forEach((foundTrack) => {
|
|
|
|
|
foundTrack.SNG_TITLE_VERSION = foundTrack.SNG_TITLE;
|
|
|
|
|
|
|
|
|
|
if (foundTrack.VERSION) {
|
|
|
|
|
foundTrack.SNG_TITLE_VERSION = (foundTrack.SNG_TITLE + ' ' + foundTrack.VERSION).trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (removeWhitespacesAndSpecialChars(trackInfos.SNG_TITLE_VERSION).toLowerCase() === removeWhitespacesAndSpecialChars(foundTrack.SNG_TITLE_VERSION).toLowerCase()) {
|
|
|
|
|
foundAlternativeTrack = true;
|
|
|
|
|
|
|
|
|
|
resolve(foundTrack);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!foundAlternativeTrack) {
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
} else if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
getTrackAlternative(trackInfos).then((alternativeTrackInfos) => {
|
|
|
|
|
resolve(alternativeTrackInfos);
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject();
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove whitespaces and special characters from the given string.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} string
|
|
|
|
|
*/
|
|
|
|
|
function removeWhitespacesAndSpecialChars(string) {
|
|
|
|
|
return string.replace(/[^A-Z0-9]/ig, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get infos of an album by id.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
*/
|
|
|
|
|
function getAlbumInfos(id) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
return requestWithCache({
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'deezer.pageAlbum',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
body: {
|
|
|
|
|
alb_id: id,
|
|
|
|
|
lang: 'us',
|
|
|
|
|
tab: 0
|
|
|
|
|
},
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
log.debug('Got album infos for "album/' + id + '"');
|
|
|
|
|
|
|
|
|
|
if (response && 0 === Object.keys(response.error).length && response.results && response.results.DATA && response.results.SONGS) {
|
|
|
|
|
let albumInfos = response.results.DATA;
|
|
|
|
|
albumInfos.SONGS = response.results.SONGS;
|
|
|
|
|
|
|
|
|
|
resolve(albumInfos);
|
|
|
|
|
} else if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
getAlbumInfos(id).then((albumInfos) => {
|
|
|
|
|
resolve(albumInfos);
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get infos of an album from the official api by id.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
*/
|
|
|
|
|
function getAlbumInfosOfficialApi(id) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
return requestWithCache({
|
|
|
|
|
url: 'https://api.deezer.com/album/' + id,
|
|
|
|
|
json: true
|
|
|
|
|
}).then((albumInfos) => {
|
|
|
|
|
log.debug('Got album infos (official api) for "album/' + id + '"');
|
|
|
|
|
|
|
|
|
|
if (albumInfos && !albumInfos.error) {
|
|
|
|
|
resolve(albumInfos);
|
|
|
|
|
} else {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get lyrics of a track by id.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} id
|
|
|
|
|
*/
|
|
|
|
|
function getTrackLyrics(id) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
return requestWithCache({
|
|
|
|
|
method: 'POST',
|
|
|
|
|
url: unofficialApiUrl,
|
|
|
|
|
qs: Object.assign(unofficialApiQueries, {
|
|
|
|
|
method: 'song.getLyrics',
|
|
|
|
|
cid: getApiCid()
|
|
|
|
|
}),
|
|
|
|
|
body: {
|
|
|
|
|
sng_id: id
|
|
|
|
|
},
|
|
|
|
|
json: true,
|
|
|
|
|
jar: true
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
log.debug('Got lyrics for "track/' + id + '"');
|
|
|
|
|
|
|
|
|
|
if (response && 0 === Object.keys(response.error).length && response.results && response.results.LYRICS_ID) {
|
|
|
|
|
let trackLyrics = response.results;
|
|
|
|
|
|
|
|
|
|
resolve(trackLyrics);
|
|
|
|
|
} else if (response.error.VALID_TOKEN_REQUIRED) {
|
|
|
|
|
initDeezerApi();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
getTrackLyrics(id).then((trackLyrics) => {
|
|
|
|
|
resolve(trackLyrics);
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject({statusCode: 404});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a track to the playlist file content.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} saveFilePath
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
*/
|
|
|
|
|
function addTrackToPlaylist(saveFilePath, trackInfos) {
|
|
|
|
|
if (PLAYLIST_FILE_ITEMS != null) {
|
|
|
|
|
let saveFilePathForPlaylist = saveFilePath.replace(/\\+/g, '/');
|
|
|
|
|
|
|
|
|
|
if (!trackInfos.ALB_ART_NAME) {
|
|
|
|
|
trackInfos.ALB_ART_NAME = trackInfos.ART_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let artistName = multipleWhitespacesToSingle(sanitizeFilename(trackInfos.ALB_ART_NAME));
|
|
|
|
|
|
|
|
|
|
if ('' === artistName.trim()) {
|
|
|
|
|
artistName = 'Unknown artist';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PLAYLIST_FILE_ITEMS[trackInfos.SNG_ID] = {
|
|
|
|
|
trackTitle: trackInfos.SNG_TITLE_VERSION,
|
|
|
|
|
trackArtist: artistName,
|
|
|
|
|
trackDuration: trackInfos.DURATION,
|
|
|
|
|
trackSavePath: saveFilePathForPlaylist
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Capitalizes the first letter of a string
|
|
|
|
|
*
|
|
|
|
|
* @param {String} string
|
|
|
|
|
*
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function capitalizeFirstLetter(string) {
|
|
|
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds a zero to the beginning if the number has only one digit.
|
|
|
|
|
*
|
|
|
|
|
* @param {Number} number
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function toTwoDigits(number) {
|
|
|
|
|
return (number < 10 ? '0' : '') + number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replaces multiple whitespaces with a single one.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} string
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function multipleWhitespacesToSingle(string) {
|
|
|
|
|
return string.replace(/[ _,]+/g, ' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replaces multiple whitespaces with a single one.
|
|
|
|
|
*
|
|
|
|
|
* @param {String} fileName
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function sanitizeFilename(fileName) {
|
|
|
|
|
fileName = fileName.replace('/', '-');
|
|
|
|
|
|
|
|
|
|
return sanitize(fileName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the URL to download the track.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
* @param {Number} trackQuality
|
|
|
|
|
*
|
|
|
|
|
* @returns {String}
|
|
|
|
|
*/
|
|
|
|
|
function getTrackDownloadUrl(trackInfos, trackQuality) {
|
|
|
|
|
const step1 = [trackInfos.MD5_ORIGIN, trackQuality, trackInfos.SNG_ID, trackInfos.MEDIA_VERSION].join('¤');
|
|
|
|
|
|
|
|
|
|
let step2 = crypto.createHash('md5').update(step1, 'ascii').digest('hex') + '¤' + step1 + '¤';
|
|
|
|
|
while (step2.length % 16 > 0) step2 += ' ';
|
|
|
|
|
|
|
|
|
|
const step3 = crypto.createCipheriv('aes-128-ecb', 'jo6aey6haid2Teih', '').update(step2, 'ascii', 'hex');
|
|
|
|
|
const cdn = trackInfos.MD5_ORIGIN[0];
|
|
|
|
|
|
|
|
|
|
return 'https://e-cdns-proxy-' + cdn + '.dzcdn.net/mobile/1/' + step3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse file size and check if it is defined & is non zero zero
|
|
|
|
|
*
|
|
|
|
|
* @returns {Boolean}
|
|
|
|
|
*/
|
|
|
|
|
function fileSizeIsDefined(filesize) {
|
|
|
|
|
return !('undefined' === typeof filesize || 0 === parseInt(filesize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a downloadable track quality.
|
|
|
|
|
*
|
|
|
|
|
* FLAC -> 320kbps -> 256kbps -> 128kbps
|
|
|
|
|
* 320kbps -> FLAC -> 256kbps -> 128kbps
|
|
|
|
|
* 128kbps -> 256kbps -> 320kbps -> FLAC
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
*
|
|
|
|
|
* @returns {Object|Boolean}
|
|
|
|
|
*/
|
|
|
|
|
function getValidTrackQuality(trackInfos) {
|
|
|
|
|
if (fileSizeIsDefined(trackInfos.FILESIZE_MP3_MISC)) {
|
|
|
|
|
return musicQualities.MP3_MISC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (musicQualities.FLAC === selectedMusicQuality) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_FLAC)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_320)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_256)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_128)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_128;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_256;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_320;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.FLAC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (musicQualities.MP3_320 === selectedMusicQuality) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_320)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_FLAC)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_256)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_128)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_128;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_256;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.FLAC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_320;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (musicQualities.MP3_128 === selectedMusicQuality) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_128)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_256)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_MP3_320)) {
|
|
|
|
|
if (!fileSizeIsDefined(trackInfos.FILESIZE_FLAC)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.FLAC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_320;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_256;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musicQualities.MP3_128;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the blowfish key to decrypt the track
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
*/
|
|
|
|
|
function getBlowfishKey(trackInfos) {
|
|
|
|
|
const SECRET = 'g4el58wc0zvf9na1';
|
|
|
|
|
|
|
|
|
|
const idMd5 = crypto.createHash('md5').update(trackInfos.SNG_ID.toString(), 'ascii').digest('hex');
|
|
|
|
|
let bfKey = '';
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
|
|
|
bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bfKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrypt a deezer track.
|
|
|
|
|
*
|
|
|
|
|
* @param {Buffer} trackBuffer
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
*
|
|
|
|
|
* @return {Buffer}
|
|
|
|
|
*/
|
|
|
|
|
function decryptTrack(trackBuffer, trackInfos) {
|
|
|
|
|
const blowFishKey = getBlowfishKey(trackInfos);
|
|
|
|
|
let i = 0;
|
|
|
|
|
let position = 0;
|
|
|
|
|
|
|
|
|
|
let decryptedBuffer = new Buffer(trackBuffer.length);
|
|
|
|
|
decryptedBuffer.fill(0);
|
|
|
|
|
|
|
|
|
|
while (position < trackBuffer.length) {
|
|
|
|
|
let chunkSize = 2048;
|
|
|
|
|
|
|
|
|
|
if ((trackBuffer.length - position) < 2048) {
|
|
|
|
|
chunkSize = trackBuffer.length - position;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let encryptedChunk = new Buffer(chunkSize);
|
|
|
|
|
encryptedChunk.fill(0);
|
|
|
|
|
trackBuffer.copy(encryptedChunk, 0, position, position + chunkSize);
|
|
|
|
|
|
|
|
|
|
if (i % 3 > 0 || chunkSize < 2048) {
|
|
|
|
|
// Already decrypted
|
|
|
|
|
} else {
|
|
|
|
|
let cipher = crypto.createDecipheriv('bf-cbc', blowFishKey, new Buffer([0, 1, 2, 3, 4, 5, 6, 7]));
|
|
|
|
|
|
|
|
|
|
cipher.setAutoPadding(false);
|
|
|
|
|
encryptedChunk = cipher.update(encryptedChunk, 'binary', 'binary') + cipher.final();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decryptedBuffer.write(encryptedChunk.toString('binary'), position, 'binary');
|
|
|
|
|
|
|
|
|
|
position += chunkSize;
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return decryptedBuffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download the track, decrypt it and write it to a file.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
* @param {Number} trackQualityId
|
|
|
|
|
* @param {String} saveFilePath
|
|
|
|
|
* @param {Number} numberRetry
|
|
|
|
|
*/
|
|
|
|
|
function downloadTrack(trackInfos, trackQualityId, saveFilePath, numberRetry = 0) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const trackDownloadUrl = getTrackDownloadUrl(trackInfos, trackQualityId);
|
|
|
|
|
|
|
|
|
|
log.debug('Started downloading "track/' + trackInfos.SNG_ID + '" in "' + trackQualityId + '". Download url: "' + trackDownloadUrl + '"');
|
|
|
|
|
|
|
|
|
|
requestWithoutCache({
|
|
|
|
|
url: trackDownloadUrl,
|
|
|
|
|
headers: httpHeaders,
|
|
|
|
|
jar: true,
|
|
|
|
|
encoding: null
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
log.debug('Got download response for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
const decryptedTrackBuffer = decryptTrack(response, trackInfos);
|
|
|
|
|
|
|
|
|
|
resolve(decryptedTrackBuffer);
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
if (403 === err.statusCode) {
|
|
|
|
|
let maxNumberRetry = 1;
|
|
|
|
|
|
|
|
|
|
if ((trackInfos.RIGHTS && 0 !== Object.keys(trackInfos.RIGHTS).length) || (trackInfos.AVAILABLE_COUNTRIES && trackInfos.AVAILABLE_COUNTRIES.STREAM_ADS && 0 < trackInfos.AVAILABLE_COUNTRIES.STREAM_ADS.length)) {
|
|
|
|
|
maxNumberRetry = 20;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maxNumberRetry >= numberRetry) {
|
|
|
|
|
numberRetry += 1;
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
downloadTrack(trackInfos, trackQualityId, saveFilePath, numberRetry).then((decryptedTrackBuffer) => {
|
|
|
|
|
resolve(decryptedTrackBuffer);
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
reject(error);
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
} else {
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download the album cover of a track.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
* @param {String} saveFilePath
|
|
|
|
|
* @param {Number} numberRetry
|
|
|
|
|
*/
|
|
|
|
|
function downloadAlbumCover(trackInfos, saveFilePath, numberRetry = 0) {
|
|
|
|
|
|
|
|
|
|
const albumCoverUrl = 'https://e-cdns-images.dzcdn.net/images/cover/' + trackInfos.ALB_PICTURE + '/1400x1400-000000-94-0-0.jpg';
|
|
|
|
|
const albumCoverSavePath = nodePath.dirname(saveFilePath) + '/cover.jpg';
|
|
|
|
|
const tempAlbumCoverSavePath = albumCoverSavePath + '.temp';
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
// check to make sure there is a cover for this album
|
|
|
|
|
if (!trackInfos.ALB_PICTURE) {
|
|
|
|
|
reject();
|
|
|
|
|
} else {
|
|
|
|
|
if (!fs.existsSync(albumCoverSavePath)) {
|
|
|
|
|
if (!fs.existsSync(tempAlbumCoverSavePath)) {
|
|
|
|
|
log.debug('Started downloading album cover for "track/' + trackInfos.SNG_ID + '". Album cover url: "' + albumCoverUrl + '"');
|
|
|
|
|
|
|
|
|
|
ensureDir(tempAlbumCoverSavePath);
|
|
|
|
|
fs.writeFileSync(tempAlbumCoverSavePath, '');
|
|
|
|
|
|
|
|
|
|
requestWithoutCache({
|
|
|
|
|
url: albumCoverUrl,
|
|
|
|
|
headers: httpHeaders,
|
|
|
|
|
jar: true,
|
|
|
|
|
encoding: null
|
|
|
|
|
}).then((response) => {
|
|
|
|
|
log.debug('Got album cover download response for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
ensureDir(tempAlbumCoverSavePath);
|
|
|
|
|
|
|
|
|
|
fs.writeFile(tempAlbumCoverSavePath, response, (err) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
removeTempAlbumCover();
|
|
|
|
|
reject();
|
|
|
|
|
} else {
|
|
|
|
|
log.debug('Finished downloading album cover for "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
if (fs.existsSync(tempAlbumCoverSavePath)) {
|
|
|
|
|
fs.renameSync(tempAlbumCoverSavePath, albumCoverSavePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve(albumCoverSavePath);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
if (403 === err.statusCode) {
|
|
|
|
|
if (4 >= numberRetry) {
|
|
|
|
|
numberRetry += 1;
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
removeTempAlbumCover();
|
|
|
|
|
|
|
|
|
|
downloadAlbumCover(trackInfos, saveFilePath, numberRetry).then((albumCoverSavePath) => {
|
|
|
|
|
resolve(albumCoverSavePath);
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject();
|
|
|
|
|
});
|
|
|
|
|
}, 500);
|
|
|
|
|
} else {
|
|
|
|
|
removeTempAlbumCover();
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
removeTempAlbumCover();
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
downloadAlbumCover(trackInfos, saveFilePath, numberRetry).then((albumCoverSavePath) => {
|
|
|
|
|
resolve(albumCoverSavePath);
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject();
|
|
|
|
|
});
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log.debug('Album cover for "track/' + trackInfos.SNG_ID + '" already exists');
|
|
|
|
|
|
|
|
|
|
resolve(albumCoverSavePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function removeTempAlbumCover() {
|
|
|
|
|
if (fs.existsSync(tempAlbumCoverSavePath)) {
|
|
|
|
|
fs.unlinkSync(tempAlbumCoverSavePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add tags to the mp3/flac file.
|
|
|
|
|
*
|
|
|
|
|
* @param {Buffer} decryptedTrackBuffer
|
|
|
|
|
* @param {Object} trackInfos
|
|
|
|
|
* @param {String} saveFilePath
|
|
|
|
|
* @param {Number} numberRetry
|
|
|
|
|
*/
|
|
|
|
|
function addTrackTags(decryptedTrackBuffer, trackInfos, saveFilePath, numberRetry = 0) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
log.debug('Started tagging "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
downloadAlbumCover(trackInfos, saveFilePath).then((albumCoverSavePath) => {
|
|
|
|
|
log.debug('Got album cover and started tagging "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
startTagging(albumCoverSavePath);
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
startTagging();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function startTagging(albumCoverSavePath = null) {
|
|
|
|
|
try {
|
|
|
|
|
if (trackInfos.LYRICS || !trackInfos.LYRICS_ID || 0 === trackInfos.LYRICS_ID) {
|
|
|
|
|
afterLyricsFetching();
|
|
|
|
|
} else {
|
|
|
|
|
getTrackLyrics(trackInfos.SNG_ID).then((trackLyrics) => {
|
|
|
|
|
trackInfos.LYRICS = trackLyrics;
|
|
|
|
|
|
|
|
|
|
afterLyricsFetching();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
afterLyricsFetching();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function afterLyricsFetching() {
|
|
|
|
|
let trackMetadata = {
|
|
|
|
|
title: '',
|
|
|
|
|
album: '',
|
|
|
|
|
releaseType: '',
|
|
|
|
|
genre: '',
|
|
|
|
|
artists: [],
|
|
|
|
|
albumArtist: '',
|
|
|
|
|
trackNumber: '',
|
|
|
|
|
trackNumberCombined: '',
|
|
|
|
|
partOfSet: '',
|
|
|
|
|
partOfSetCombined: '',
|
|
|
|
|
label: '',
|
|
|
|
|
copyright: '',
|
|
|
|
|
composer: [],
|
|
|
|
|
publisher: [],
|
|
|
|
|
producer: [],
|
|
|
|
|
engineer: [],
|
|
|
|
|
writer: [],
|
|
|
|
|
author: [],
|
|
|
|
|
mixer: [],
|
|
|
|
|
ISRC: '',
|
|
|
|
|
duration: '',
|
|
|
|
|
bpm: '',
|
|
|
|
|
upc: '',
|
|
|
|
|
explicit: '',
|
|
|
|
|
tracktotal: '',
|
|
|
|
|
disctotal: '',
|
|
|
|
|
compilation: '',
|
|
|
|
|
unsynchronisedLyrics: '',
|
|
|
|
|
synchronisedLyrics: '',
|
|
|
|
|
media: 'Digital Media',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_TITLE_VERSION) {
|
|
|
|
|
trackMetadata.title = trackInfos.SNG_TITLE_VERSION;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_TITLE) {
|
|
|
|
|
trackMetadata.album = trackInfos.ALB_TITLE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_ART_NAME) {
|
|
|
|
|
trackMetadata.albumArtist = trackInfos.ALB_ART_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.DURATION) {
|
|
|
|
|
trackMetadata.duration = trackInfos.DURATION;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_UPC) {
|
|
|
|
|
trackMetadata.upc = trackInfos.ALB_UPC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_RELEASE_TYPE) {
|
|
|
|
|
let releaseType = trackInfos.ALB_RELEASE_TYPE;
|
|
|
|
|
|
|
|
|
|
if ('ep' === releaseType) {
|
|
|
|
|
releaseType = 'EP';
|
|
|
|
|
} else {
|
|
|
|
|
releaseType = capitalizeFirstLetter(releaseType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackMetadata.releaseType = releaseType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_GENRES && trackInfos.ALB_GENRES[0]) {
|
|
|
|
|
trackMetadata.genre = trackInfos.ALB_GENRES[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.TRACK_NUMBER) {
|
|
|
|
|
trackMetadata.trackNumber = trackInfos.TRACK_NUMBER;
|
|
|
|
|
trackMetadata.trackNumberCombined = trackInfos.TRACK_NUMBER;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_NUM_TRACKS) {
|
|
|
|
|
trackMetadata.tracktotal = trackInfos.ALB_NUM_TRACKS;
|
|
|
|
|
trackMetadata.trackNumberCombined += '/' + trackInfos.ALB_NUM_TRACKS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.DISK_NUMBER) {
|
|
|
|
|
trackMetadata.partOfSet = trackInfos.DISK_NUMBER;
|
|
|
|
|
trackMetadata.partOfSetCombined = trackInfos.DISK_NUMBER;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_NUM_DISCS) {
|
|
|
|
|
trackMetadata.disctotal = trackInfos.ALB_NUM_DISCS;
|
|
|
|
|
trackMetadata.partOfSetCombined += '/' + trackInfos.ALB_NUM_DISCS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_RELEASE_DATE || trackInfos.PHYSICAL_RELEASE_DATE) {
|
|
|
|
|
let releaseDate = trackInfos.ALB_RELEASE_DATE;
|
|
|
|
|
|
|
|
|
|
if (!trackInfos.ALB_RELEASE_DATE) {
|
|
|
|
|
releaseDate = trackInfos.PHYSICAL_RELEASE_DATE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackMetadata.releaseYear = releaseDate.slice(0, 4);
|
|
|
|
|
trackMetadata.releaseDate = releaseDate.slice(0, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ALB_LABEL) {
|
|
|
|
|
trackMetadata.label = trackInfos.ALB_LABEL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.COPYRIGHT) {
|
|
|
|
|
trackMetadata.copyright = trackInfos.COPYRIGHT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ISRC) {
|
|
|
|
|
trackMetadata.ISRC = trackInfos.ISRC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.BPM) {
|
|
|
|
|
trackMetadata.bpm = trackInfos.BPM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.EXPLICIT_LYRICS) {
|
|
|
|
|
trackMetadata.explicit = trackInfos.EXPLICIT_LYRICS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.ARTISTS) {
|
|
|
|
|
let trackArtists = [];
|
|
|
|
|
|
|
|
|
|
trackInfos.ARTISTS.forEach((trackArtist) => {
|
|
|
|
|
if (trackArtist.ART_NAME) {
|
|
|
|
|
trackArtist = trackArtist.ART_NAME.split(new RegExp(' and | & | featuring | feat. | Ft. | ft. | vs | vs. | x | - |, ', 'g'));
|
|
|
|
|
trackArtist = trackArtist.map(Function.prototype.call, String.prototype.trim);
|
|
|
|
|
|
|
|
|
|
trackArtists = trackArtists.concat(trackArtist);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackArtists = [...new Set(trackArtists)];
|
|
|
|
|
trackMetadata.artists = trackArtists;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS) {
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.composer) {
|
|
|
|
|
trackMetadata.composer = trackInfos.SNG_CONTRIBUTORS.composer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.musicpublisher) {
|
|
|
|
|
trackMetadata.publisher = trackInfos.SNG_CONTRIBUTORS.musicpublisher;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.producer) {
|
|
|
|
|
trackMetadata.producer = trackInfos.SNG_CONTRIBUTORS.producer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.engineer) {
|
|
|
|
|
trackMetadata.engineer = trackInfos.SNG_CONTRIBUTORS.engineer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.writer) {
|
|
|
|
|
trackMetadata.writer = trackInfos.SNG_CONTRIBUTORS.writer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.author) {
|
|
|
|
|
trackMetadata.author = trackInfos.SNG_CONTRIBUTORS.author;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.SNG_CONTRIBUTORS.mixer) {
|
|
|
|
|
trackMetadata.mixer = trackInfos.SNG_CONTRIBUTORS.mixer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('Various Artists' === trackMetadata.performerInfo) {
|
|
|
|
|
trackMetadata.compilation = 1;
|
|
|
|
|
} else {
|
|
|
|
|
trackMetadata.compilation = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.LYRICS) {
|
|
|
|
|
if (trackInfos.LYRICS.LYRICS_TEXT) {
|
|
|
|
|
trackMetadata.unsynchronisedLyrics = trackInfos.LYRICS.LYRICS_TEXT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackInfos.LYRICS.LYRICS_SYNC_JSON) {
|
|
|
|
|
const syncedLyrics = trackInfos.LYRICS.LYRICS_SYNC_JSON;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < syncedLyrics.length; i++) {
|
|
|
|
|
if (syncedLyrics[i].lrc_timestamp) {
|
|
|
|
|
trackMetadata.synchronisedLyrics += syncedLyrics[i].lrc_timestamp + syncedLyrics[i].line + '\r\n';
|
|
|
|
|
} else if (i + 1 < syncedLyrics.length) {
|
|
|
|
|
trackMetadata.synchronisedLyrics += syncedLyrics[i + 1].lrc_timestamp + syncedLyrics[i].line + '\r\n';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let saveFilePathExtension = nodePath.extname(saveFilePath);
|
|
|
|
|
|
|
|
|
|
if ('.mp3' === saveFilePathExtension) {
|
|
|
|
|
if ('' !== trackMetadata.synchronisedLyrics.trim()) {
|
|
|
|
|
const lyricsFile = saveFilePath.slice(0, -4) + '.lrc';
|
|
|
|
|
|
|
|
|
|
ensureDir(lyricsFile);
|
|
|
|
|
fs.writeFileSync(lyricsFile, trackMetadata.synchronisedLyrics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.debug('Started MP3 tagging "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
const writer = new id3Writer(decryptedTrackBuffer);
|
|
|
|
|
let coverBuffer;
|
|
|
|
|
|
|
|
|
|
if (albumCoverSavePath && fs.existsSync(albumCoverSavePath)) {
|
|
|
|
|
coverBuffer = fs.readFileSync(albumCoverSavePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer
|
|
|
|
|
.setFrame('TIT2', trackMetadata.title)
|
|
|
|
|
.setFrame('TALB', trackMetadata.album)
|
|
|
|
|
.setFrame('TCON', [trackMetadata.genre])
|
|
|
|
|
.setFrame('TPE2', trackMetadata.albumArtist)
|
|
|
|
|
.setFrame('TPE1', [trackMetadata.artists.join(', ')])
|
|
|
|
|
.setFrame('TRCK', trackMetadata.trackNumberCombined)
|
|
|
|
|
.setFrame('TPOS', trackMetadata.partOfSetCombined)
|
|
|
|
|
.setFrame('WCOP', trackMetadata.copyright)
|
|
|
|
|
.setFrame('TPUB', trackMetadata.publisher.join('/'))
|
|
|
|
|
.setFrame('TLEN', trackMetadata.duration)
|
|
|
|
|
.setFrame('TMED', trackMetadata.media)
|
|
|
|
|
.setFrame('TCOM', trackMetadata.composer)
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'Artists',
|
|
|
|
|
value: trackMetadata.artists.join('/')
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'RELEASETYPE',
|
|
|
|
|
value: trackMetadata.releaseType
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'ISRC',
|
|
|
|
|
value: trackMetadata.ISRC
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'BARCODE',
|
|
|
|
|
value: trackMetadata.upc
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'LABEL',
|
|
|
|
|
value: trackMetadata.label
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'LYRICIST',
|
|
|
|
|
value: trackMetadata.writer.join('/')
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'MIXARTIST',
|
|
|
|
|
value: trackMetadata.mixer.join('/')
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'INVOLVEDPEOPLE',
|
|
|
|
|
value: trackMetadata.producer.concat(trackMetadata.engineer).join('/')
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'COMPILATION',
|
|
|
|
|
value: trackMetadata.compilation
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'EXPLICIT',
|
|
|
|
|
value: trackMetadata.explicit
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'SOURCE',
|
|
|
|
|
value: 'Deezer'
|
|
|
|
|
})
|
|
|
|
|
.setFrame('TXXX', {
|
|
|
|
|
description: 'SOURCEID',
|
|
|
|
|
value: trackInfos.SNG_ID
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.unsynchronisedLyrics) {
|
|
|
|
|
writer.setFrame('USLT', {
|
|
|
|
|
description: '',
|
|
|
|
|
lyrics: trackMetadata.unsynchronisedLyrics
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (coverBuffer) {
|
|
|
|
|
writer.setFrame('APIC', {
|
|
|
|
|
type: 3,
|
|
|
|
|
data: coverBuffer,
|
|
|
|
|
description: ''
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < parseInt(trackMetadata.releaseYear)) {
|
|
|
|
|
writer.setFrame('TYER', trackMetadata.releaseYear);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < parseInt(trackMetadata.releaseDate)) {
|
|
|
|
|
writer.setFrame('TDAT', trackMetadata.releaseDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < parseInt(trackMetadata.bpm)) {
|
|
|
|
|
writer.setFrame('TBPM', trackMetadata.bpm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.addTag();
|
|
|
|
|
|
|
|
|
|
const taggedTrackBuffer = Buffer.from(writer.arrayBuffer);
|
|
|
|
|
|
|
|
|
|
ensureDir(saveFilePath);
|
|
|
|
|
fs.writeFileSync(saveFilePath, taggedTrackBuffer);
|
|
|
|
|
|
|
|
|
|
log.debug('Finished MP3 tagging "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
} else if ('.flac' === saveFilePathExtension) {
|
|
|
|
|
if ('' !== trackMetadata.synchronisedLyrics.trim()) {
|
|
|
|
|
const lyricsFile = saveFilePath.slice(0, -5) + '.lrc';
|
|
|
|
|
|
|
|
|
|
ensureDir(lyricsFile);
|
|
|
|
|
fs.writeFileSync(lyricsFile, trackMetadata.synchronisedLyrics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.debug('Started FLAC tagging "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
let flacComments = [
|
|
|
|
|
'SOURCE=Deezer',
|
|
|
|
|
'SOURCEID=' + trackInfos.SNG_ID
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.title) {
|
|
|
|
|
flacComments.push('TITLE=' + trackMetadata.title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.album) {
|
|
|
|
|
flacComments.push('ALBUM=' + trackMetadata.album);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.genre) {
|
|
|
|
|
flacComments.push('GENRE=' + trackMetadata.genre);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.albumArtist) {
|
|
|
|
|
flacComments.push('ALBUMARTIST=' + trackMetadata.albumArtist);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < trackMetadata.artists.length) {
|
|
|
|
|
flacComments.push('ARTIST=' + trackMetadata.artists.join(', '));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.trackNumber) {
|
|
|
|
|
flacComments.push('TRACKNUMBER=' + trackMetadata.trackNumber);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.tracktotal) {
|
|
|
|
|
flacComments.push('TRACKTOTAL=' + trackMetadata.tracktotal);
|
|
|
|
|
flacComments.push('TOTALTRACKS=' + trackMetadata.tracktotal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.partOfSet) {
|
|
|
|
|
flacComments.push('DISCNUMBER=' + trackMetadata.partOfSet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.disctotal) {
|
|
|
|
|
flacComments.push('DISCTOTAL=' + trackMetadata.disctotal);
|
|
|
|
|
flacComments.push('TOTALDISCS=' + trackMetadata.disctotal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.label) {
|
|
|
|
|
flacComments.push('LABEL=' + trackMetadata.label);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.copyright) {
|
|
|
|
|
flacComments.push('COPYRIGHT=' + trackMetadata.copyright);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.duration) {
|
|
|
|
|
flacComments.push('LENGTH=' + trackMetadata.duration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.ISRC) {
|
|
|
|
|
flacComments.push('ISRC=' + trackMetadata.ISRC);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.upc) {
|
|
|
|
|
flacComments.push('BARCODE=' + trackMetadata.upc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.media) {
|
|
|
|
|
flacComments.push('MEDIA=' + trackMetadata.media);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.compilation) {
|
|
|
|
|
flacComments.push('COMPILATION=' + trackMetadata.compilation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('' !== trackMetadata.explicit) {
|
|
|
|
|
flacComments.push('EXPLICIT=' + trackMetadata.explicit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackMetadata.releaseType) {
|
|
|
|
|
flacComments.push('RELEASETYPE=' + trackMetadata.releaseType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackMetadata.artists.forEach((artist) => {
|
|
|
|
|
flacComments.push('ARTISTS=' + artist);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.composer.forEach((composer) => {
|
|
|
|
|
flacComments.push('COMPOSER=' + composer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.publisher.forEach((publisher) => {
|
|
|
|
|
flacComments.push('ORGANIZATION=' + publisher);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.producer.forEach((producer) => {
|
|
|
|
|
flacComments.push('PRODUCER=' + producer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.engineer.forEach((engineer) => {
|
|
|
|
|
flacComments.push('ENGINEER=' + engineer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.writer.forEach((writer) => {
|
|
|
|
|
flacComments.push('WRITER=' + writer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.author.forEach((author) => {
|
|
|
|
|
flacComments.push('AUTHOR=' + author);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trackMetadata.mixer.forEach((mixer) => {
|
|
|
|
|
flacComments.push('MIXER=' + mixer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (trackMetadata.unsynchronisedLyrics) {
|
|
|
|
|
flacComments.push('LYRICS=' + trackMetadata.unsynchronisedLyrics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < parseInt(trackMetadata.releaseYear)) {
|
|
|
|
|
flacComments.push('YEAR=' + trackMetadata.releaseYear);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < parseInt(trackMetadata.releaseDate)) {
|
|
|
|
|
flacComments.push('DATE=' + trackMetadata.releaseDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (0 < parseInt(trackMetadata.bpm)) {
|
|
|
|
|
flacComments.push('BPM=' + trackMetadata.bpm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reader = new stream.PassThrough();
|
|
|
|
|
reader.end(decryptedTrackBuffer);
|
|
|
|
|
|
|
|
|
|
ensureDir(saveFilePath);
|
|
|
|
|
|
|
|
|
|
const writer = fs.createWriteStream(saveFilePath);
|
|
|
|
|
let processor = new flacMetadata.Processor({parseMetaDataBlocks: true});
|
|
|
|
|
let vendor = 'reference libFLAC 1.2.1 20070917';
|
|
|
|
|
let coverBuffer;
|
|
|
|
|
|
|
|
|
|
if (albumCoverSavePath && fs.existsSync(albumCoverSavePath)) {
|
|
|
|
|
coverBuffer = fs.readFileSync(albumCoverSavePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mdbVorbisComment;
|
|
|
|
|
let mdbVorbisPicture;
|
|
|
|
|
|
|
|
|
|
processor.on('preprocess', (mdb) => {
|
|
|
|
|
// Remove existing VORBIS_COMMENT and PICTURE blocks, if any.
|
|
|
|
|
if (flacMetadata.Processor.MDB_TYPE_VORBIS_COMMENT === mdb.type) {
|
|
|
|
|
mdb.remove();
|
|
|
|
|
} else if (coverBuffer && flacMetadata.Processor.MDB_TYPE_PICTURE === mdb.type) {
|
|
|
|
|
mdb.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mdb.isLast) {
|
|
|
|
|
mdbVorbisComment = flacMetadata.data.MetaDataBlockVorbisComment.create(!coverBuffer, vendor, flacComments);
|
|
|
|
|
|
|
|
|
|
if (coverBuffer) {
|
|
|
|
|
mdbVorbisPicture = flacMetadata.data.MetaDataBlockPicture.create(true, 3, 'image/jpeg', '', 1400, 1400, 24, 0, coverBuffer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mdb.isLast = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
processor.on('postprocess', (mdb) => {
|
|
|
|
|
if (flacMetadata.Processor.MDB_TYPE_VORBIS_COMMENT === mdb.type && null !== mdb.vendor) {
|
|
|
|
|
vendor = mdb.vendor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mdbVorbisComment) {
|
|
|
|
|
processor.push(mdbVorbisComment.publish());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mdbVorbisPicture) {
|
|
|
|
|
processor.push(mdbVorbisPicture.publish());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
reader.on('end', () => {
|
|
|
|
|
log.debug('Finished FLAC tagging "track/' + trackInfos.SNG_ID + '"');
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
reader.pipe(processor).pipe(writer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.debug('Error tagging "track/' + trackInfos.SNG_ID + '". Number retries: "' + numberRetry + '". Error: ' + err);
|
|
|
|
|
|
|
|
|
|
if (10 > numberRetry) {
|
|
|
|
|
numberRetry += 1;
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
addTrackTags(decryptedTrackBuffer, trackInfos, saveFilePath, numberRetry).then(() => {
|
|
|
|
|
resolve();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
reject();
|
|
|
|
|
});
|
|
|
|
|
}, 500);
|
|
|
|
|
} else {
|
|
|
|
|
ensureDir(saveFilePath);
|
|
|
|
|
fs.writeFileSync(saveFilePath, decryptedTrackBuffer);
|
|
|
|
|
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|