1
0
mirror of https://git.fuwafuwa.moe/SMLoadrDev/SMLoadr synced 2024-11-17 04:44:33 +01:00
SMLoadr/SMLoadr.js

2898 lines
102 KiB
JavaScript
Raw Normal View History

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) {
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();
}
}
}
});
}