2021-04-09 18:05:32 +00:00
|
|
|
const { Track, AlbumDoesntExists } = require('./types/Track.js')
|
2021-04-09 16:45:57 +00:00
|
|
|
const { streamTrack, generateStreamURL } = require('./decryption.js')
|
2021-04-14 15:11:56 +00:00
|
|
|
const { tagID3 } = require('./tagger.js')
|
2021-04-09 16:45:57 +00:00
|
|
|
const { TrackFormats } = require('deezer-js')
|
2021-04-09 18:05:32 +00:00
|
|
|
const { USER_AGENT_HEADER } = require('./utils/index.js')
|
2021-04-13 16:40:34 +00:00
|
|
|
const { DEFAULTS } = require('./settings.js')
|
|
|
|
const { generatePath } = require('./utils/pathtemplates.js')
|
2021-04-09 18:05:32 +00:00
|
|
|
const got = require('got')
|
2021-04-12 17:32:57 +00:00
|
|
|
const fs = require('fs')
|
2021-04-09 16:45:57 +00:00
|
|
|
|
2021-04-13 16:40:34 +00:00
|
|
|
const extensions = {
|
|
|
|
[TrackFormats.FLAC]: '.flac',
|
|
|
|
[TrackFormats.LOCAL]: '.mp3',
|
|
|
|
[TrackFormats.MP3_320]: '.mp3',
|
|
|
|
[TrackFormats.MP3_128]: '.mp3',
|
|
|
|
[TrackFormats.DEFAULT]: '.mp3',
|
|
|
|
[TrackFormats.MP4_RA3]: '.mp4',
|
|
|
|
[TrackFormats.MP4_RA2]: '.mp4',
|
|
|
|
[TrackFormats.MP4_RA1]: '.mp4'
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
async function getPreferredBitrate(track, bitrate, shouldFallback, uuid, listener){
|
2021-04-09 16:45:57 +00:00
|
|
|
bitrate = parseInt(bitrate)
|
|
|
|
if (track.localTrack) { return TrackFormats.LOCAL }
|
|
|
|
|
|
|
|
let falledBack = false
|
|
|
|
|
|
|
|
const formats_non_360 = {
|
2021-04-13 16:40:34 +00:00
|
|
|
[TrackFormats.FLAC]: "FLAC",
|
|
|
|
[TrackFormats.MP3_320]: "MP3_320",
|
|
|
|
[TrackFormats.MP3_128]: "MP3_128"
|
2021-04-09 16:45:57 +00:00
|
|
|
}
|
|
|
|
const formats_360 = {
|
2021-04-13 16:40:34 +00:00
|
|
|
[TrackFormats.MP4_RA3]: "MP4_RA3",
|
|
|
|
[TrackFormats.MP4_RA2]: "MP4_RA2",
|
|
|
|
[TrackFormats.MP4_RA1]: "MP4_RA1"
|
2021-04-09 16:45:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-13 16:40:34 +00:00
|
|
|
const is360Format = Object.keys(formats_360).indexOf(bitrate) != -1
|
2021-04-09 16:45:57 +00:00
|
|
|
let formats
|
|
|
|
if (!shouldFallback){
|
|
|
|
formats = {...formats_360, ...formats_non_360}
|
|
|
|
}else if (is360Format){
|
|
|
|
formats = {...formats_360}
|
|
|
|
}else{
|
|
|
|
formats = {...formats_non_360}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < Object.keys(formats).length; i++){
|
2021-04-13 16:40:34 +00:00
|
|
|
let formatNumber = Object.keys(formats)[i]
|
|
|
|
let formatName = formats[formatNumber]
|
2021-04-09 16:45:57 +00:00
|
|
|
|
2021-04-09 21:05:24 +00:00
|
|
|
if (formatNumber > bitrate) { continue }
|
|
|
|
if (Object.keys(track.filesizes).indexOf(`FILESIZE_${formatName}`) != -1){
|
2021-04-09 16:45:57 +00:00
|
|
|
if (parseInt(track.filesizes[`FILESIZE_${formatName}`]) != 0) return formatNumber
|
|
|
|
if (!track.filesizes[`FILESIZE_${formatName}_TESTED`]){
|
2021-04-09 21:05:24 +00:00
|
|
|
let request
|
2021-04-09 18:05:32 +00:00
|
|
|
try {
|
2021-04-09 21:05:24 +00:00
|
|
|
request = got.get(
|
2021-04-09 18:05:32 +00:00
|
|
|
generateStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber),
|
|
|
|
{ headers: {'User-Agent': USER_AGENT_HEADER}, timeout: 30000 }
|
|
|
|
).on("response", (response)=>{
|
2021-04-09 21:05:24 +00:00
|
|
|
track.filesizes[`FILESIZE_${formatName}`] = response.headers["content-length"]
|
2021-04-09 18:05:32 +00:00
|
|
|
track.filesizes[`FILESIZE_${formatName}_TESTED`] = true
|
|
|
|
request.cancel()
|
|
|
|
}).on("error", (e)=>{
|
|
|
|
throw e
|
|
|
|
})
|
|
|
|
|
2021-04-09 21:05:24 +00:00
|
|
|
await request
|
|
|
|
} catch (e){
|
|
|
|
if (e.isCanceled) { return formatNumber }
|
|
|
|
console.error(e)
|
|
|
|
throw e
|
|
|
|
}
|
2021-04-09 16:45:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!shouldFallback){
|
|
|
|
throw new PreferredBitrateNotFound
|
|
|
|
}else if (!falledBack){
|
|
|
|
falledBack = true
|
|
|
|
if (listener && uuid){
|
|
|
|
listener.send("queueUpdate", {
|
|
|
|
uuid,
|
|
|
|
bitrateFallback: true,
|
|
|
|
data:{
|
|
|
|
id: track.id,
|
|
|
|
title: track.title,
|
|
|
|
artist: track.mainArtist.name
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
}
|
2021-04-09 16:45:57 +00:00
|
|
|
if (is360Format) throw new TrackNot360
|
|
|
|
return TrackFormats.DEFAULT
|
|
|
|
}
|
2021-04-08 17:37:31 +00:00
|
|
|
|
|
|
|
class Downloader {
|
|
|
|
constructor(dz, downloadObject, settings, listener){
|
|
|
|
this.dz = dz
|
|
|
|
this.downloadObject = downloadObject
|
2021-04-13 16:40:34 +00:00
|
|
|
this.settings = settings || DEFAULTS
|
2021-04-08 17:37:31 +00:00
|
|
|
this.bitrate = downloadObject.bitrate
|
|
|
|
this.listener = listener
|
|
|
|
|
|
|
|
this.extrasPath = null
|
|
|
|
this.playlistCoverName = null
|
|
|
|
this.playlistURLs = []
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
async start(){
|
2021-04-09 16:45:57 +00:00
|
|
|
if (this.downloadObject.__type__ === "Single"){
|
2021-04-09 18:05:32 +00:00
|
|
|
await this.download({
|
2021-04-09 16:45:57 +00:00
|
|
|
trackAPI_gw: this.downloadObject.single.trackAPI_gw,
|
|
|
|
trackAPI: this.downloadObject.single.trackAPI,
|
|
|
|
albumAPI: this.downloadObject.single.albumAPI
|
|
|
|
})
|
|
|
|
} else if (this.downloadObject.__type__ === "Collection") {
|
|
|
|
let tracks = []
|
2021-04-09 18:05:32 +00:00
|
|
|
this.downloadObject.collection.tracks_gw.forEach(async (track, pos) => {
|
|
|
|
tracks[pos] = await this.download({
|
2021-04-09 16:45:57 +00:00
|
|
|
trackAPI_gw: track,
|
|
|
|
albumAPI: this.downloadObject.collection.albumAPI,
|
|
|
|
playlistAPI: this.downloadObject.collection.playlistAPI
|
|
|
|
})
|
|
|
|
})
|
2021-04-08 17:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.listener) this.listener.send("finishedDownload", this.downloadObject.uuid)
|
|
|
|
}
|
|
|
|
|
2021-04-09 16:45:57 +00:00
|
|
|
async download(extraData, track){
|
|
|
|
const { trackAPI_gw, trackAPI, albumAPI, playlistAPI } = extraData
|
2021-04-08 17:37:31 +00:00
|
|
|
if (trackAPI_gw.SNG_ID == "0") throw new DownloadFailed("notOnDeezer")
|
|
|
|
|
|
|
|
// Generate track object
|
|
|
|
if (!track){
|
2021-04-09 21:05:24 +00:00
|
|
|
track = new Track()
|
2021-04-09 18:05:32 +00:00
|
|
|
console.log("Getting tags")
|
|
|
|
try{
|
|
|
|
await track.parseData(
|
|
|
|
this.dz,
|
2021-04-09 21:05:24 +00:00
|
|
|
trackAPI_gw.SNG_ID,
|
2021-04-09 18:05:32 +00:00
|
|
|
trackAPI_gw,
|
|
|
|
trackAPI,
|
|
|
|
albumAPI,
|
|
|
|
playlistAPI
|
|
|
|
)
|
|
|
|
} catch (e){
|
|
|
|
if (e instanceof AlbumDoesntExists) { throw new DownloadError('albumDoesntExists') }
|
|
|
|
console.error(e)
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
|
2021-04-08 17:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the track is encoded
|
|
|
|
if (track.MD5 === "") throw new DownloadFailed("notEncoded", track)
|
|
|
|
|
|
|
|
// Check the target bitrate
|
2021-04-09 18:05:32 +00:00
|
|
|
console.log("Getting bitrate")
|
|
|
|
let selectedFormat
|
|
|
|
try{
|
2021-04-09 21:05:24 +00:00
|
|
|
selectedFormat = await getPreferredBitrate(
|
2021-04-09 18:05:32 +00:00
|
|
|
track,
|
|
|
|
this.bitrate,
|
2021-04-09 21:05:24 +00:00
|
|
|
true, // fallbackBitrate
|
2021-04-09 18:05:32 +00:00
|
|
|
this.downloadObject.uuid, this.listener
|
|
|
|
)
|
|
|
|
}catch (e){
|
|
|
|
if (e instanceof PreferredBitrateNotFound) { throw new DownloadFailed("wrongBitrate", track) }
|
|
|
|
if (e instanceof TrackNot360) { throw new DownloadFailed("no360RA") }
|
|
|
|
console.error(e)
|
|
|
|
throw e
|
|
|
|
}
|
2021-04-08 17:37:31 +00:00
|
|
|
track.bitrate = selectedFormat
|
|
|
|
track.album.bitrate = selectedFormat
|
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
// Apply Settings
|
2021-04-13 16:40:34 +00:00
|
|
|
track.applySettings(this.settings)
|
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
// Generate filename and filepath from metadata
|
2021-04-13 16:40:34 +00:00
|
|
|
let {
|
|
|
|
filename,
|
|
|
|
filepath,
|
|
|
|
artistPath,
|
|
|
|
coverPath,
|
|
|
|
extrasPath
|
|
|
|
} = generatePath(track, this.downloadObject, this.settings)
|
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
// Make sure the filepath exsists
|
2021-04-13 16:40:34 +00:00
|
|
|
fs.mkdirSync(filepath, { recursive: true })
|
2021-04-14 15:11:56 +00:00
|
|
|
let extension = extensions[track.bitrate]
|
|
|
|
let writepath = `${filepath}/${filename}${extension}`
|
2021-04-13 16:40:34 +00:00
|
|
|
|
2021-04-09 18:05:32 +00:00
|
|
|
// Save extrasPath
|
2021-04-13 16:40:34 +00:00
|
|
|
if (extrasPath && !this.extrasPath) this.extrasPath = extrasPath
|
|
|
|
|
|
|
|
// Generate covers URLs
|
2021-04-09 18:05:32 +00:00
|
|
|
// Download and cache the coverart
|
|
|
|
// Save local album art
|
|
|
|
// Save artist art
|
|
|
|
// Save playlist art
|
|
|
|
// Save lyrics in lrc file
|
|
|
|
// Check for overwrite settings
|
|
|
|
|
2021-04-08 17:37:31 +00:00
|
|
|
// Download the track
|
2021-04-09 18:05:32 +00:00
|
|
|
console.log("Downloading")
|
2021-04-09 16:45:57 +00:00
|
|
|
track.downloadURL = generateStreamURL(track.id, track.MD5, track.mediaVersion, track.bitrate)
|
2021-04-13 16:40:34 +00:00
|
|
|
let stream = fs.createWriteStream(writepath)
|
2021-04-12 18:40:39 +00:00
|
|
|
await streamTrack(stream, track, 0, this.downloadObject, this.listener)
|
2021-04-13 16:40:34 +00:00
|
|
|
console.log(filename)
|
2021-04-09 18:05:32 +00:00
|
|
|
// Adding tags
|
2021-04-14 15:11:56 +00:00
|
|
|
if (extension == '.mp3'){
|
|
|
|
tagID3(writepath, track, this.settings.tags)
|
|
|
|
}
|
2021-04-09 16:45:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DownloadError extends Error {
|
|
|
|
constructor(message) {
|
|
|
|
super(message);
|
|
|
|
this.name = "DownloadError"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DownloadFailed extends DownloadError {
|
|
|
|
constructor(errid, track) {
|
|
|
|
super(errid);
|
|
|
|
this.name = "ISRCnotOnDeezer"
|
|
|
|
this.track = track
|
2021-04-08 17:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-09 16:45:57 +00:00
|
|
|
|
|
|
|
class TrackNot360 extends Error {
|
|
|
|
constructor(message) {
|
|
|
|
super(message);
|
|
|
|
this.name = "TrackNot360"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class PreferredBitrateNotFound extends Error {
|
|
|
|
constructor(message) {
|
|
|
|
super(message);
|
|
|
|
this.name = "PreferredBitrateNotFound"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
Downloader,
|
|
|
|
DownloadError,
|
2021-04-09 18:05:32 +00:00
|
|
|
DownloadFailed,
|
|
|
|
getPreferredBitrate,
|
|
|
|
TrackNot360,
|
|
|
|
PreferredBitrateNotFound
|
2021-04-09 16:45:57 +00:00
|
|
|
}
|