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 : 15 GktD5M1kCmESyxfhA6EvmhGzWnRA8gvg
* BTC Cash : 1 LpLtLREzTWzba94wBBpJxcv7r6h6u1jgF
* 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 Keep in mind that Facebook login and family accounts are not supported.\n Create a new account if you use one.\n' ) ;
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 - > 320 kbps - > 256 kbps - > 128 kbps
* 320 kbps - > FLAC - > 256 kbps - > 128 kbps
* 128 kbps - > 256 kbps - > 320 kbps - > 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 ( ) ;
}
}
}
} ) ;
}