diff --git a/deemix/decryption.js b/deemix/decryption.js index 13b21d8..1aa3e26 100644 --- a/deemix/decryption.js +++ b/deemix/decryption.js @@ -34,7 +34,11 @@ async function streamTrack(outputStream, track, start=0, downloadObject, listene let chunkLength = start let complete = 0 - let itemName = `[${track.mainArtist.name} - ${track.title}]` + let itemData = { + id: track.id, + title: track.title, + artist: track.mainArtist.name + } let error = '' let request = got.stream(track.downloadURL, { @@ -50,7 +54,7 @@ async function streamTrack(outputStream, track, start=0, downloadObject, listene let responseRange = response.headers["content-range"] if (listener) listener.send('downloadInfo', { uuid: downloadObject.uuid, - itemName, + data: itemData, state: "downloading", alreadyStarted: true, value: responseRange @@ -58,7 +62,7 @@ async function streamTrack(outputStream, track, start=0, downloadObject, listene }else { if (listener) listener.send('downloadInfo', { uuid: downloadObject.uuid, - itemName, + data: itemData, state: "downloading", alreadyStarted: false, value: complete @@ -100,8 +104,8 @@ async function streamTrack(outputStream, track, start=0, downloadObject, listene } class DownloadEmpty extends Error { - constructor(message) { - super(message); + constructor() { + super() this.name = "DownloadEmpty" } } diff --git a/deemix/downloader.js b/deemix/downloader.js index 709678f..90a1663 100644 --- a/deemix/downloader.js +++ b/deemix/downloader.js @@ -9,7 +9,7 @@ const { TrackFormats } = require('deezer-js') const got = require('got') const fs = require('fs') const { tmpdir } = require('os') -const { queue, each, eachOf } = require('async') +const { queue, each } = require('async') const { exec } = require("child_process") const extensions = { @@ -26,7 +26,7 @@ const extensions = { const TEMPDIR = tmpdir()+`/deemix-imgs` fs.mkdirSync(TEMPDIR, { recursive: true }) -async function downloadImage(url, path, overwrite){ +async function downloadImage(url, path, overwrite = OverwriteOption.DONT_OVERWRITE){ if (fs.existsSync(path) && ![OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS, OverwriteOption.KEEP_BOTH].includes(overwrite)) return path const downloadStream = got.stream(url, { headers: {'User-Agent': USER_AGENT_HEADER}, timeout: 30000}) @@ -42,7 +42,7 @@ async function downloadImage(url, path, overwrite){ let pictureURL = url.slice(urlBase.length) let pictureSize = parseInt(pictureURL.slice(0, pictureURL.indexOf('x'))) if (pictureSize > 1200) - return downloadImage(urlBase+pictureURL.replace(`${pictureSize}x${pictureSize}`, '1200x1200')) + return downloadImage(urlBase+pictureURL.replace(`${pictureSize}x${pictureSize}`, '1200x1200'), path, overwrite) } return null } @@ -102,6 +102,7 @@ async function getPreferredBitrate(track, bitrate, shouldFallback, uuid, listene if (e instanceof got.ReadError || e instanceof got.TimeoutError){ return await testBitrate(track, formatNumber, formatName) } + if (e instanceof got.HTTPError) return null console.trace(e) throw e } @@ -115,7 +116,8 @@ async function getPreferredBitrate(track, bitrate, shouldFallback, uuid, listene if (Object.keys(track.filesizes).includes(`FILESIZE_${formatName}`)){ if (parseInt(track.filesizes[`FILESIZE_${formatName}`]) != 0) return formatNumber if (!track.filesizes[`FILESIZE_${formatName}_TESTED`]){ - return await testBitrate(track, formatNumber, formatName) + let testedBitrate = await testBitrate(track, formatNumber, formatName) + if (testedBitrate) return testedBitrate } } @@ -154,53 +156,54 @@ class Downloader { this.playlistURLs = [] } - log(itemName, state){ + log(data, state){ if (this.listener) - this.listener.send('downloadInfo', { uuid: this.downloadObject.uuid, itemName, state }) + this.listener.send('downloadInfo', { uuid: this.downloadObject.uuid, data, state }) } - warn(itemName, state, solution){ + warn(data, state, solution){ if (this.listener) - this.listener.send('downloadWarn', { uuid: this.downloadObject.uuid, itemName, state , solution }) + this.listener.send('downloadWarn', { uuid: this.downloadObject.uuid, data, state , solution }) } async start(){ - if (!this.downloadObject.isCanceled){ - if (this.downloadObject.__type__ === "Single"){ - let track = await this.downloadWrapper({ - trackAPI_gw: this.downloadObject.single.trackAPI_gw, - trackAPI: this.downloadObject.single.trackAPI, - albumAPI: this.downloadObject.single.albumAPI - }) - await this.afterDownloadSingle(track) - } else if (this.downloadObject.__type__ === "Collection") { - let tracks = [] - - let q = queue(async (data) => { - let {track, pos} = data - tracks[pos] = await this.downloadWrapper({ - trackAPI_gw: track, - albumAPI: this.downloadObject.collection.albumAPI, - playlistAPI: this.downloadObject.collection.playlistAPI - }) - }, this.settings.queueConcurrency) - - this.downloadObject.collection.tracks_gw.forEach((track, pos) => { - q.push({track, pos}) - }) - - await q.drain() - await this.afterDownloadCollection(tracks) + if (this.downloadObject.isCanceled){ + if (this.listener){ + this.listener.send('currentItemCancelled', this.downloadObject.uuid) + this.listener.send("removedFromQueue", this.downloadObject.uuid) } + return + } + + if (this.downloadObject.__type__ === "Single"){ + let track = await this.downloadWrapper({ + trackAPI_gw: this.downloadObject.single.trackAPI_gw, + trackAPI: this.downloadObject.single.trackAPI, + albumAPI: this.downloadObject.single.albumAPI + }) + if (track) await this.afterDownloadSingle(track) + } else if (this.downloadObject.__type__ === "Collection") { + let tracks = [] + + let q = queue(async (data) => { + let {track, pos} = data + tracks[pos] = await this.downloadWrapper({ + trackAPI_gw: track, + albumAPI: this.downloadObject.collection.albumAPI, + playlistAPI: this.downloadObject.collection.playlistAPI + }) + }, this.settings.queueConcurrency) + + this.downloadObject.collection.tracks_gw.forEach((track, pos) => { + q.push({track, pos}) + }) + + await q.drain() + await this.afterDownloadCollection(tracks) } if (this.listener){ - if (this.downloadObject.isCanceled){ - this.listener.send('currentItemCancelled', this.downloadObject.uuid) - this.listener.send("removedFromQueue", this.downloadObject.uuid) - } else { - this.listener.send("finishDownload", this.downloadObject.uuid) - } + this.listener.send("finishDownload", this.downloadObject.uuid) } } @@ -210,12 +213,16 @@ class Downloader { if (this.downloadObject.isCanceled) throw new DownloadCanceled if (trackAPI_gw.SNG_ID == "0") throw new DownloadFailed("notOnDeezer") - let itemName = `[${trackAPI_gw.ART_NAME} - ${trackAPI_gw.SNG_TITLE.trim()}]` + let itemData = { + id: trackAPI_gw.SNG_ID, + title: trackAPI_gw.SNG_TITLE.trim(), + artist: trackAPI_gw.ART_NAME + } // Generate track object if (!track){ track = new Track() - this.log(itemName, "getTags") + this.log(itemData, "getTags") try{ await track.parseData( this.dz, @@ -232,17 +239,21 @@ class Downloader { console.trace(e) throw e } - this.log(itemName, "gotTags") + this.log(itemData, "gotTags") } if (this.downloadObject.isCanceled) throw new DownloadCanceled - itemName = `[${track.mainArtist.name} - ${track.title}]` + itemData = { + id: track.id, + title: track.title, + artist: track.mainArtist.name + } // Check if the track is encoded if (track.MD5 === "") throw new DownloadFailed("notEncoded", track) // Check the target bitrate - this.log(itemName, "getBitrate") + this.log(itemData, "getBitrate") let selectedFormat try{ selectedFormat = await getPreferredBitrate( @@ -259,7 +270,7 @@ class Downloader { } track.bitrate = selectedFormat track.album.bitrate = selectedFormat - this.log(itemName, "gotBitrate") + this.log(itemData, "gotBitrate") // Apply Settings track.applySettings(this.settings) @@ -292,9 +303,9 @@ class Downloader { track.album.embeddedCoverPath = `${TEMPDIR}/${track.album.isPlaylist ? 'pl'+track.playlist.id : 'alb'+track.album.id}_${this.settings.embeddedArtworkSize}${ext}` // Download and cache the coverart - this.log(itemName, "getAlbumArt") + this.log(itemData, "getAlbumArt") track.album.embeddedCoverPath = await downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath) - this.log(itemName, "gotAlbumArt") + this.log(itemData, "gotAlbumArt") // Save local album art if (coverPath){ @@ -320,7 +331,7 @@ class Downloader { // Deezer doesn't support png artist images if (picFormat === 'jpg'){ let extendedFormat = `${picFormat}-${this.settings.jpegImageQuality}` - let url = track.album.pic.getURL(this.settings.localArtworkSize, extendedFormat) + let url = track.album.mainArtist.pic.getURL(this.settings.localArtworkSize, extendedFormat) // Skip non deezer pictures at the wrong format if (track.album.mainArtist.pic.md5 == "") return returnData.artistURLs.push({url, ext: picFormat}) @@ -395,22 +406,22 @@ class Downloader { if (e instanceof got.HTTPError) throw new DownloadFailed('notAvailable', track) throw e } - this.log(itemName, "downloaded") + this.log(itemData, "downloaded") } else { - this.log(itemName, "alreadyDownloaded") + this.log(itemData, "alreadyDownloaded") this.downloadObject.completeTrackProgress(this.listener) } // Adding tags if (!trackAlreadyDownloaded || [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE].includes(this.settings.overwriteFile) && !track.local){ - this.log(itemName, "tagging") + this.log(itemData, "tagging") if (extension == '.mp3'){ tagID3(writepath, track, this.settings.tags) if (this.settings.tags.saveID3v1) tagID3v1(writepath, track, this.settings.tags) } else if (extension == '.flac'){ tagFLAC(writepath, track, this.settings.tags) } - this.log(itemName, "tagged") + this.log(itemData, "tagged") } if (track.searched) returnData.searched = true @@ -424,26 +435,21 @@ class Downloader { extrasPath: String(this.extrasPath) }) returnData.filename = writepath.slice(extrasPath.length+1) - returnData.data = { - id: track.id, - title: track.title, - artist: track.mainArtist.name - } + returnData.data = itemData return returnData } async downloadWrapper(extraData, track){ const { trackAPI_gw } = extraData // Temp metadata to generate logs - let tempTrack = { + let itemData = { id: trackAPI_gw.SNG_ID, title: trackAPI_gw.SNG_TITLE.trim(), artist: trackAPI_gw.ART_NAME } if (trackAPI_gw.VERSION && trackAPI_gw.SNG_TITLE.includes(trackAPI_gw.VERSION)) - tempTrack.title += ` ${trackAPI_gw.VERSION.trim()}` + itemData.title += ` ${trackAPI_gw.VERSION.trim()}` - let itemName = `[${tempTrack.artist} - ${tempTrack.title}]` let result try { result = await this.download(extraData, track) @@ -452,14 +458,14 @@ class Downloader { if (e.track){ let track = e.track if (track.fallbackID != 0){ - this.warn(itemName, e.errid, 'fallback') + this.warn(itemData, e.errid, 'fallback') let newTrack = await this.dz.gw.get_track_with_fallback(track.fallbackID) track.parseEssentialData(newTrack) track.retriveFilesizes(this.dz) return await this.downloadWrapper(extraData, track) } if (!track.searched && this.settings.fallbackSearch){ - this.warn(itemName, e.errid, 'search') + this.warn(itemData, e.errid, 'search') let searchedID = await this.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title) if (searchedID != "0"){ let newTrack = await this.dz.gw.get_track_with_fallback(searchedID) @@ -484,13 +490,13 @@ class Downloader { result = {error:{ message: e.message, errid: e.errid, - data: tempTrack + data: itemData }} } else if (! (e instanceof DownloadCanceled)){ console.trace(e) result = {error:{ message: e.message, - data: tempTrack + data: itemData }} } } @@ -514,6 +520,7 @@ class Downloader { } async afterDownloadSingle(track){ + if (!track) return if (!this.extrasPath) this.extrasPath = this.settings.downloadLocation // Save local album artwork @@ -550,7 +557,8 @@ class Downloader { let errors = "" let searched = "" - await eachOf(tracks, async (track, i) => { + for (let i=0; i < tracks.length; i++){ + let track = tracks[i] if (!track) return if (track.error){ @@ -574,7 +582,7 @@ class Downloader { // Save filename for playlist file playlist[i] = track.filename || "" - }) + } // Create errors logfile if (this.settings.logErrors && errors != "") @@ -633,14 +641,14 @@ class DownloadFailed extends DownloadError { } } -class TrackNot360 extends Error { +class TrackNot360 extends DownloadError { constructor() { super() this.name = "TrackNot360" } } -class PreferredBitrateNotFound extends Error { +class PreferredBitrateNotFound extends DownloadError { constructor() { super() this.name = "PreferredBitrateNotFound" diff --git a/deemix/itemgen.js b/deemix/itemgen.js index 0067b0d..9bd71d4 100644 --- a/deemix/itemgen.js +++ b/deemix/itemgen.js @@ -78,11 +78,9 @@ async function generateAlbumItem(dz, id, bitrate, rootArtist){ // If the album is a single download as a track if (albumAPI.nb_tracks == 1){ - if (albumAPI.tracks.data.length){ + if (albumAPI.tracks.data.length) return generateTrackItem(dz, albumAPI.tracks.data[0].id, bitrate, null, albumAPI) - } else { - throw new GenerationError(`https://deezer.com/album/${id}`, "Single has no tracks.") - } + throw new GenerationError(`https://deezer.com/album/${id}`, "Single has no tracks.") } let tracksArray = await dz.gw.get_album_tracks(id) diff --git a/deemix/types/Album.js b/deemix/types/Album.js index 91c377b..c01af15 100644 --- a/deemix/types/Album.js +++ b/deemix/types/Album.js @@ -14,7 +14,7 @@ class Album { this.artist = {"Main": []} this.artists = [] this.mainArtist = null - this.date = null + this.date = new Date() this.dateString = "" this.trackTotal = "0" this.discTotal = "0" @@ -89,7 +89,6 @@ class Album { this.label = albumAPI.label || this.label this.explicit = Boolean(albumAPI.explicit_lyrics || false) if (albumAPI.release_date){ - this.date = new Date() this.date.year = albumAPI.release_date.slice(0,4) this.date.month = albumAPI.release_date.slice(5,7) this.date.day = albumAPI.release_date.slice(8,10) @@ -117,7 +116,8 @@ class Album { this.title = albumAPI_gw.ALB_TITLE this.mainArtist = new Artist( albumAPI_gw.ART_ID, - albumAPI_gw.ART_NAME + albumAPI_gw.ART_NAME, + "Main" ) this.artists = [albumAPI_gw.ART_NAME] @@ -136,7 +136,6 @@ class Album { this.pic.md5 = albumAPI_gw.ALB_PICTURE } if (albumAPI_gw.PHYSICAL_RELEASE_DATE){ - this.date = new Date() this.date.year = albumAPI_gw.PHYSICAL_RELEASE_DATE.slice(0,4) this.date.month = albumAPI_gw.PHYSICAL_RELEASE_DATE.slice(5,7) this.date.day = albumAPI_gw.PHYSICAL_RELEASE_DATE.slice(8,10) diff --git a/deemix/types/Track.js b/deemix/types/Track.js index 403ef5a..33c2464 100644 --- a/deemix/types/Track.js +++ b/deemix/types/Track.js @@ -32,7 +32,7 @@ class Track { this.album = null this.trackNumber = "0" this.discNumber = "0" - this.date = null + this.date = new Date() this.lyrics = null this.bpm = 0 this.contributors = {} @@ -87,9 +87,8 @@ class Track { timeout: 30000 }).json() }catch (e){ - console.error("Track.retriveFilesizes: ", e.message) await new Promise(r => setTimeout(r, 2000)) // sleep(2000ms) - return this.retriveFilesizes(dz) + this.retriveFilesizes(dz) } if (result_json.error.length){ throw new TrackError(result_json.error) } const response = result_json.results @@ -190,12 +189,11 @@ class Track { trackAPI_gw.ALB_PICTURE || "", "cover" ) - this.mainArtist = new Artist(trackAPI_gw.ART_NAME) + this.mainArtist = new Artist("0", trackAPI_gw.ART_NAME, "Main") this.artists = [trackAPI_gw.ART_NAME] this.artist = { 'Main': [trackAPI_gw.ART_NAME] } - this.date = new Date() this.album.artist = this.artist this.album.artists = this.artists this.album.date = this.date @@ -221,14 +219,15 @@ class Track { this.mainArtist = new Artist( trackAPI_gw.ART_ID, trackAPI_gw.ART_NAME, + "Main", trackAPI_gw.ART_PICTRUE ) if (trackAPI_gw.PHYSICAL_RELEASE_DATE){ - const day = trackAPI_gw.PHYSICAL_RELEASE_DATE.slice(8,10) - const month = trackAPI_gw.PHYSICAL_RELEASE_DATE.slice(5,7) - const year = trackAPI_gw.PHYSICAL_RELEASE_DATE.slice(0,4) - this.date = new Date(day, month, year) + this.date.day = trackAPI_gw.PHYSICAL_RELEASE_DATE.slice(8,10) + this.date.month = trackAPI_gw.PHYSICAL_RELEASE_DATE.slice(5,7) + this.date.year = trackAPI_gw.PHYSICAL_RELEASE_DATE.slice(0,4) + this.date.fixDayMonth() } } @@ -339,6 +338,7 @@ class Track { this.artist[art_type][i] = changeCase(artist, settings.artistCasing) }) }) + this.generateMainFeatStrings() } // Generate artist tag diff --git a/deemix/utils/deezer.js b/deemix/utils/deezer.js index d265610..d66a12b 100644 --- a/deemix/utils/deezer.js +++ b/deemix/utils/deezer.js @@ -9,7 +9,7 @@ const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34" async function getAccessToken(email, password){ password = _md5(password, 'utf8') const hash = _md5([CLIENT_ID, email, password, CLIENT_SECRET].join(''), 'utf8') - let response = await got.get(`https://api.deezer.com/auth/token`,{ + let response = await got.get("https://api.deezer.com/auth/token",{ searchParams: { app_id: CLIENT_ID, login: email, diff --git a/deemix/utils/localpaths.js b/deemix/utils/localpaths.js index 2390370..a2371bf 100644 --- a/deemix/utils/localpaths.js +++ b/deemix/utils/localpaths.js @@ -8,6 +8,7 @@ let userdata = "" let musicdata = "" function checkPath(path){ + if (path === "") return "" if (!fs.existsSync(path)) return "" if (!canWrite(path)) return "" return path @@ -31,6 +32,7 @@ function getConfigFolder(){ userdata = `${homedata}${sep}.config${sep}` userdata = checkPath(userdata) } + if (userdata === "") userdata = `${process.cwd()}${sep}config${sep}` else userdata += `deemix${sep}` diff --git a/deemix/utils/pathtemplates.js b/deemix/utils/pathtemplates.js index 190c46a..73feebe 100644 --- a/deemix/utils/pathtemplates.js +++ b/deemix/utils/pathtemplates.js @@ -56,7 +56,6 @@ function pad(num, max_val, settings) { } function generatePath(track, downloadObject, settings){ - let filenameTemplate = "%artist% - %title%"; let singleTrack = false if (downloadObject.type === "track"){