mirror of
https://gitlab.com/deeplydrumming/DeemixFix.git
synced 2026-01-15 16:32:59 -03:00
created deemix submodule, version 3.6.15 with spotify playlist fix. No longer hosted on NPM, but as local submodule
This commit is contained in:
13
deemix/.eslintrc.json
Normal file
13
deemix/.eslintrc.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"env": {
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
74
deemix/.gitignore
vendored
Normal file
74
deemix/.gitignore
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# development
|
||||
*.map
|
||||
dev.sh
|
||||
|
||||
# distribution
|
||||
dist/*
|
||||
|
||||
yarn.lock
|
||||
188
deemix/deemix/decryption.js
Normal file
188
deemix/deemix/decryption.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const got = require('got')
|
||||
const fs = require('fs')
|
||||
const { _md5, _ecbCrypt, _ecbDecrypt, generateBlowfishKey, decryptChunk } = require('./utils/crypto.js')
|
||||
const { DownloadCanceled, DownloadEmpty } = require('./errors.js')
|
||||
|
||||
const { USER_AGENT_HEADER, pipeline } = require('./utils/index.js')
|
||||
|
||||
function generateStreamPath (sngID, md5, mediaVersion, format) {
|
||||
let urlPart = md5 + '¤' + format + '¤' + sngID + '¤' + mediaVersion
|
||||
const md5val = _md5(urlPart)
|
||||
let step2 = md5val + '¤' + urlPart + '¤'
|
||||
step2 += '.'.repeat(16 - (step2.length % 16))
|
||||
urlPart = _ecbCrypt('jo6aey6haid2Teih', step2)
|
||||
return urlPart
|
||||
}
|
||||
|
||||
function reverseStreamPath (urlPart) {
|
||||
const step2 = _ecbDecrypt('jo6aey6haid2Teih', urlPart)
|
||||
const [, md5, format, sngID, mediaVersion] = step2.split('¤')
|
||||
return [sngID, md5, mediaVersion, format]
|
||||
}
|
||||
|
||||
function generateCryptedStreamURL (sngID, md5, mediaVersion, format) {
|
||||
const urlPart = generateStreamPath(sngID, md5, mediaVersion, format)
|
||||
return 'https://e-cdns-proxy-' + md5[0] + '.dzcdn.net/mobile/1/' + urlPart
|
||||
}
|
||||
|
||||
function generateStreamURL (sngID, md5, mediaVersion, format) {
|
||||
const urlPart = generateStreamPath(sngID, md5, mediaVersion, format)
|
||||
return 'https://cdns-proxy-' + md5[0] + '.dzcdn.net/api/1/' + urlPart
|
||||
}
|
||||
|
||||
function reverseStreamURL (url) {
|
||||
const urlPart = url.slice(url.find('/1/') + 3)
|
||||
return reverseStreamPath(urlPart)
|
||||
}
|
||||
|
||||
async function streamTrack (writepath, track, downloadObject, listener) {
|
||||
if (downloadObject && downloadObject.isCanceled) throw new DownloadCanceled()
|
||||
const headers = { 'User-Agent': USER_AGENT_HEADER }
|
||||
let chunkLength = 0
|
||||
let complete = 0
|
||||
const isCryptedStream = track.downloadURL.includes('/mobile/') || track.downloadURL.includes('/media/')
|
||||
let blowfishKey
|
||||
const outputStream = fs.createWriteStream(writepath)
|
||||
let timeout = null
|
||||
|
||||
const itemData = {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.mainArtist.name
|
||||
}
|
||||
let error = ''
|
||||
|
||||
if (isCryptedStream) blowfishKey = generateBlowfishKey(String(track.id))
|
||||
|
||||
async function * decrypter (source) {
|
||||
let modifiedStream = Buffer.alloc(0)
|
||||
for await (const chunk of source) {
|
||||
if (!isCryptedStream) {
|
||||
yield chunk
|
||||
} else {
|
||||
modifiedStream = Buffer.concat([modifiedStream, chunk])
|
||||
while (modifiedStream.length >= 2048 * 3) {
|
||||
let decryptedChunks = Buffer.alloc(0)
|
||||
const decryptingChunks = modifiedStream.slice(0, 2048 * 3)
|
||||
modifiedStream = modifiedStream.slice(2048 * 3)
|
||||
if (decryptingChunks.length >= 2048) {
|
||||
decryptedChunks = decryptChunk(decryptingChunks.slice(0, 2048), blowfishKey)
|
||||
decryptedChunks = Buffer.concat([decryptedChunks, decryptingChunks.slice(2048)])
|
||||
}
|
||||
yield decryptedChunks
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isCryptedStream) {
|
||||
let decryptedChunks = Buffer.alloc(0)
|
||||
if (modifiedStream.length >= 2048) {
|
||||
decryptedChunks = decryptChunk(modifiedStream.slice(0, 2048), blowfishKey)
|
||||
decryptedChunks = Buffer.concat([decryptedChunks, modifiedStream.slice(2048)])
|
||||
yield decryptedChunks
|
||||
} else {
|
||||
yield modifiedStream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function * depadder (source) {
|
||||
let isStart = true
|
||||
for await (let chunk of source) {
|
||||
if (isStart && chunk[0] === 0 && chunk.slice(4, 8).toString() !== 'ftyp') {
|
||||
let i
|
||||
for (i = 0; i < chunk.length; i++) {
|
||||
const byte = chunk[i]
|
||||
if (byte !== 0) break
|
||||
}
|
||||
chunk = chunk.slice(i)
|
||||
}
|
||||
isStart = false
|
||||
yield chunk
|
||||
}
|
||||
}
|
||||
|
||||
const request = got.stream(track.downloadURL, {
|
||||
headers,
|
||||
https: { rejectUnauthorized: false }
|
||||
}).on('response', (response) => {
|
||||
clearTimeout(timeout)
|
||||
complete = parseInt(response.headers['content-length'])
|
||||
if (complete === 0) {
|
||||
error = 'DownloadEmpty'
|
||||
request.destroy()
|
||||
}
|
||||
if (listener) {
|
||||
listener.send('downloadInfo', {
|
||||
uuid: downloadObject.uuid,
|
||||
data: itemData,
|
||||
state: 'downloading',
|
||||
alreadyStarted: false,
|
||||
value: complete
|
||||
})
|
||||
}
|
||||
}).on('data', function (chunk) {
|
||||
if (downloadObject.isCanceled) {
|
||||
error = 'DownloadCanceled'
|
||||
request.destroy()
|
||||
}
|
||||
chunkLength += chunk.length
|
||||
|
||||
if (downloadObject) {
|
||||
downloadObject.progressNext += ((chunk.length / complete) / downloadObject.size) * 100
|
||||
downloadObject.updateProgress(listener)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
error = 'DownloadTimeout'
|
||||
request.destroy()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
error = 'DownloadTimeout'
|
||||
request.destroy()
|
||||
}, 5000)
|
||||
|
||||
try {
|
||||
await pipeline(request, decrypter, depadder, outputStream)
|
||||
} catch (e) {
|
||||
if (fs.existsSync(writepath)) fs.unlinkSync(writepath)
|
||||
if (
|
||||
e instanceof got.ReadError ||
|
||||
e instanceof got.TimeoutError ||
|
||||
['ESOCKETTIMEDOUT', 'ERR_STREAM_PREMATURE_CLOSE', 'ETIMEDOUT', 'ECONNRESET'].includes(e.code) ||
|
||||
(request.destroyed && error === 'DownloadTimeout')
|
||||
) {
|
||||
if (downloadObject && chunkLength !== 0) {
|
||||
downloadObject.progressNext -= ((chunkLength / complete) / downloadObject.size) * 100
|
||||
downloadObject.updateProgress(listener)
|
||||
}
|
||||
if (listener) {
|
||||
listener.send('downloadInfo', {
|
||||
uuid: downloadObject.uuid,
|
||||
data: itemData,
|
||||
state: 'downloadTimeout'
|
||||
})
|
||||
}
|
||||
return await streamTrack(writepath, track, downloadObject, listener)
|
||||
} else if (request.destroyed) {
|
||||
switch (error) {
|
||||
case 'DownloadEmpty': throw new DownloadEmpty()
|
||||
case 'DownloadCanceled': throw new DownloadCanceled()
|
||||
default: throw e
|
||||
}
|
||||
} else {
|
||||
console.trace(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateStreamPath,
|
||||
generateStreamURL,
|
||||
generateCryptedStreamURL,
|
||||
reverseStreamPath,
|
||||
reverseStreamURL,
|
||||
streamTrack
|
||||
}
|
||||
837
deemix/deemix/downloader.js
Normal file
837
deemix/deemix/downloader.js
Normal file
@@ -0,0 +1,837 @@
|
||||
const { Track } = require('./types/Track.js')
|
||||
const { StaticPicture } = require('./types/Picture.js')
|
||||
const { streamTrack, generateCryptedStreamURL } = require('./decryption.js')
|
||||
const { tagID3, tagFLAC } = require('./tagger.js')
|
||||
const { USER_AGENT_HEADER, pipeline, shellEscape } = require('./utils/index.js')
|
||||
const { DEFAULTS, OverwriteOption } = require('./settings.js')
|
||||
const { generatePath, generateAlbumName, generateArtistName, generateDownloadObjectName } = require('./utils/pathtemplates.js')
|
||||
const { PreferredBitrateNotFound, TrackNot360, DownloadFailed, ErrorMessages, DownloadCanceled } = require('./errors.js')
|
||||
const { TrackFormats } = require('deezer-js')
|
||||
const { WrongLicense, WrongGeolocation } = require('deezer-js').errors
|
||||
const { map_track } = require('deezer-js').utils
|
||||
const got = require('got')
|
||||
const fs = require('fs')
|
||||
const { tmpdir } = require('os')
|
||||
const { queue, each } = require('async')
|
||||
const { exec } = require('child_process')
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
const formatsName = {
|
||||
[TrackFormats.FLAC]: 'FLAC',
|
||||
[TrackFormats.LOCAL]: 'MP3_MISC',
|
||||
[TrackFormats.MP3_320]: 'MP3_320',
|
||||
[TrackFormats.MP3_128]: 'MP3_128',
|
||||
[TrackFormats.DEFAULT]: 'MP3_MISC',
|
||||
[TrackFormats.MP4_RA3]: 'MP4_RA3',
|
||||
[TrackFormats.MP4_RA2]: 'MP4_RA2',
|
||||
[TrackFormats.MP4_RA1]: 'MP4_RA1'
|
||||
}
|
||||
|
||||
const TEMPDIR = tmpdir() + '/deemix-imgs'
|
||||
fs.mkdirSync(TEMPDIR, { recursive: true })
|
||||
|
||||
async function downloadImage (url, path, overwrite = OverwriteOption.DONT_OVERWRITE) {
|
||||
if (fs.existsSync(path) && ![OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS, OverwriteOption.KEEP_BOTH].includes(overwrite)) {
|
||||
const file = fs.readFileSync(path)
|
||||
if (file.length !== 0) return path
|
||||
fs.unlinkSync(path)
|
||||
}
|
||||
let timeout = null
|
||||
let error = ''
|
||||
|
||||
const downloadStream = got.stream(url, { headers: { 'User-Agent': USER_AGENT_HEADER }, https: { rejectUnauthorized: false } })
|
||||
.on('data', function () {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
error = 'DownloadTimeout'
|
||||
downloadStream.destroy()
|
||||
}, 5000)
|
||||
})
|
||||
const fileWriterStream = fs.createWriteStream(path)
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
error = 'DownloadTimeout'
|
||||
downloadStream.destroy()
|
||||
}, 5000)
|
||||
|
||||
try {
|
||||
await pipeline(downloadStream, fileWriterStream)
|
||||
} catch (e) {
|
||||
fs.unlinkSync(path)
|
||||
if (e instanceof got.HTTPError) {
|
||||
if (url.includes('images.dzcdn.net')) {
|
||||
const urlBase = url.slice(0, url.lastIndexOf('/') + 1)
|
||||
const pictureURL = url.slice(urlBase.length)
|
||||
const pictureSize = parseInt(pictureURL.slice(0, pictureURL.indexOf('x')))
|
||||
if (pictureSize > 1200) { return downloadImage(urlBase + pictureURL.replace(`${pictureSize}x${pictureSize}`, '1200x1200'), path, overwrite) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (
|
||||
e instanceof got.ReadError ||
|
||||
e instanceof got.TimeoutError ||
|
||||
['ESOCKETTIMEDOUT', 'ERR_STREAM_PREMATURE_CLOSE', 'ETIMEDOUT', 'ECONNRESET'].includes(e.code) ||
|
||||
(downloadStream.destroyed && error === 'DownloadTimeout')
|
||||
) {
|
||||
return downloadImage(url, path, overwrite)
|
||||
}
|
||||
console.trace(e)
|
||||
throw e
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
async function getPreferredBitrate (dz, track, preferredBitrate, shouldFallback, feelingLucky, uuid, listener) {
|
||||
preferredBitrate = parseInt(preferredBitrate)
|
||||
|
||||
let falledBack = false
|
||||
let hasAlternative = track.fallbackID !== 0
|
||||
let isGeolocked = false
|
||||
let wrongLicense = false
|
||||
|
||||
async function testURL (track, url, formatName) {
|
||||
if (!url) return false
|
||||
let request
|
||||
try {
|
||||
request = got.get(
|
||||
url,
|
||||
{ headers: { 'User-Agent': USER_AGENT_HEADER }, https: { rejectUnauthorized: false }, timeout: 7000 }
|
||||
).on('response', (response) => {
|
||||
track.filesizes[`${formatName.toLowerCase()}`] = response.statusCode === 403 ? 0 : response.headers['content-length']
|
||||
request.cancel()
|
||||
})
|
||||
|
||||
await request
|
||||
} catch (e) {
|
||||
if (e.isCanceled) {
|
||||
if (track.filesizes[`${formatName.toLowerCase()}`] === 0) return false
|
||||
return true
|
||||
}
|
||||
if (e instanceof got.ReadError || e instanceof got.TimeoutError) {
|
||||
return await testURL(track, url, formatName)
|
||||
}
|
||||
if (e instanceof got.HTTPError) return false
|
||||
console.trace(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function getCorrectURL (track, formatName, formatNumber, feelingLucky) {
|
||||
// Check the track with the legit method
|
||||
let url
|
||||
wrongLicense = (
|
||||
((formatName === 'FLAC' || formatName.startsWith('MP4_RA')) && !dz.current_user.can_stream_lossless) ||
|
||||
(formatName === 'MP3_320' && !dz.current_user.can_stream_hq)
|
||||
)
|
||||
if (track.filesizes[`${formatName.toLowerCase()}`] && track.filesizes[`${formatName.toLowerCase()}`] !== '0') {
|
||||
try {
|
||||
url = await dz.get_track_url(track.trackToken, formatName)
|
||||
} catch (e) {
|
||||
wrongLicense = (e.name === 'WrongLicense')
|
||||
isGeolocked = (e.name === 'WrongGeolocation')
|
||||
}
|
||||
}
|
||||
// Fallback to old method
|
||||
if (!url && feelingLucky) {
|
||||
url = generateCryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber)
|
||||
if (await testURL(track, url, formatName, formatNumber)) return url
|
||||
url = undefined
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
if (track.local) {
|
||||
const url = await getCorrectURL(track, 'MP3_MISC', TrackFormats.LOCAL, feelingLucky)
|
||||
track.urls.MP3_MISC = url
|
||||
return TrackFormats.LOCAL
|
||||
}
|
||||
|
||||
const formats_non_360 = {
|
||||
[TrackFormats.FLAC]: 'FLAC',
|
||||
[TrackFormats.MP3_320]: 'MP3_320',
|
||||
[TrackFormats.MP3_128]: 'MP3_128'
|
||||
}
|
||||
const formats_360 = {
|
||||
[TrackFormats.MP4_RA3]: 'MP4_RA3',
|
||||
[TrackFormats.MP4_RA2]: 'MP4_RA2',
|
||||
[TrackFormats.MP4_RA1]: 'MP4_RA1'
|
||||
}
|
||||
|
||||
const is360Format = Object.keys(formats_360).includes(preferredBitrate)
|
||||
let formats
|
||||
if (!shouldFallback) {
|
||||
formats = { ...formats_360, ...formats_non_360 }
|
||||
} else if (is360Format) {
|
||||
formats = { ...formats_360 }
|
||||
} else {
|
||||
formats = { ...formats_non_360 }
|
||||
}
|
||||
|
||||
// Check and renew trackToken before starting the check
|
||||
await track.checkAndRenewTrackToken(dz)
|
||||
for (let i = 0; i < Object.keys(formats).length; i++) {
|
||||
// Check bitrates
|
||||
const formatNumber = Object.keys(formats).reverse()[i]
|
||||
const formatName = formats[formatNumber]
|
||||
|
||||
// Current bitrate is higher than preferred bitrate; skip
|
||||
if (formatNumber > preferredBitrate) { continue }
|
||||
|
||||
let currentTrack = track
|
||||
let url = await getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
|
||||
let newTrack
|
||||
do {
|
||||
if (!url && hasAlternative) {
|
||||
newTrack = await dz.gw.get_track_with_fallback(currentTrack.fallbackID)
|
||||
newTrack = map_track(newTrack)
|
||||
currentTrack = new Track()
|
||||
currentTrack.parseEssentialData(newTrack)
|
||||
hasAlternative = currentTrack.fallbackID !== 0
|
||||
}
|
||||
if (!url) url = await getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
|
||||
} while (!url && hasAlternative)
|
||||
|
||||
if (url) {
|
||||
if (newTrack) track.parseEssentialData(newTrack)
|
||||
track.urls[formatName] = url
|
||||
return formatNumber
|
||||
}
|
||||
|
||||
if (!shouldFallback) {
|
||||
if (wrongLicense) throw new WrongLicense(formatName)
|
||||
if (isGeolocked) throw new WrongGeolocation(dz.current_user.country)
|
||||
throw new PreferredBitrateNotFound()
|
||||
} else if (!falledBack) {
|
||||
falledBack = true
|
||||
if (listener && uuid) {
|
||||
listener.send('downloadInfo', {
|
||||
uuid,
|
||||
state: 'bitrateFallback',
|
||||
data: {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.mainArtist.name
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is360Format) throw new TrackNot360()
|
||||
const url = await getCorrectURL(track, 'MP3_MISC', TrackFormats.DEFAULT, feelingLucky)
|
||||
track.urls.MP3_MISC = url
|
||||
return TrackFormats.DEFAULT
|
||||
}
|
||||
|
||||
class Downloader {
|
||||
constructor (dz, downloadObject, settings, listener) {
|
||||
this.dz = dz
|
||||
this.downloadObject = downloadObject
|
||||
this.settings = settings || DEFAULTS
|
||||
this.bitrate = downloadObject.bitrate
|
||||
this.listener = listener
|
||||
|
||||
this.playlistCovername = null
|
||||
this.playlistURLs = []
|
||||
|
||||
this.coverQueue = {}
|
||||
}
|
||||
|
||||
log (data, state) {
|
||||
if (this.listener) { this.listener.send('downloadInfo', { uuid: this.downloadObject.uuid, data, state }) }
|
||||
}
|
||||
|
||||
warn (data, state, solution) {
|
||||
if (this.listener) { this.listener.send('downloadWarn', { uuid: this.downloadObject.uuid, data, state, solution }) }
|
||||
}
|
||||
|
||||
async start () {
|
||||
if (!this.downloadObject.isCanceled) {
|
||||
if (this.downloadObject.__type__ === 'Single') {
|
||||
const track = await this.downloadWrapper({
|
||||
trackAPI: this.downloadObject.single.trackAPI,
|
||||
albumAPI: this.downloadObject.single.albumAPI
|
||||
})
|
||||
if (track) await this.afterDownloadSingle(track)
|
||||
} else if (this.downloadObject.__type__ === 'Collection') {
|
||||
const tracks = []
|
||||
|
||||
const q = queue(async (data) => {
|
||||
const { track, pos } = data
|
||||
tracks[pos] = await this.downloadWrapper({
|
||||
trackAPI: track,
|
||||
albumAPI: this.downloadObject.collection.albumAPI,
|
||||
playlistAPI: this.downloadObject.collection.playlistAPI
|
||||
})
|
||||
}, this.settings.queueConcurrency)
|
||||
|
||||
if (this.downloadObject.collection.tracks.length) {
|
||||
this.downloadObject.collection.tracks.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async download (extraData, track) {
|
||||
const returnData = {}
|
||||
const { trackAPI, albumAPI, playlistAPI } = extraData
|
||||
trackAPI.size = this.downloadObject.size
|
||||
if (this.downloadObject.isCanceled) throw new DownloadCanceled()
|
||||
if (parseInt(trackAPI.id) === 0) throw new DownloadFailed('notOnDeezer')
|
||||
|
||||
let itemData = {
|
||||
id: trackAPI.id,
|
||||
title: trackAPI.title,
|
||||
artist: trackAPI.artist.name
|
||||
}
|
||||
|
||||
// Generate track object
|
||||
if (!track) {
|
||||
track = new Track()
|
||||
this.log(itemData, 'getTags')
|
||||
try {
|
||||
await track.parseData(
|
||||
this.dz,
|
||||
trackAPI.id,
|
||||
trackAPI,
|
||||
albumAPI,
|
||||
playlistAPI
|
||||
)
|
||||
} catch (e) {
|
||||
if (e.name === 'AlbumDoesntExists') { throw new DownloadFailed('albumDoesntExists') }
|
||||
if (e.name === 'MD5NotFound') { throw new DownloadFailed('notLoggedIn') }
|
||||
console.trace(e)
|
||||
throw e
|
||||
}
|
||||
this.log(itemData, 'gotTags')
|
||||
}
|
||||
if (this.downloadObject.isCanceled) throw new DownloadCanceled()
|
||||
|
||||
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(itemData, 'getBitrate')
|
||||
let selectedFormat
|
||||
try {
|
||||
selectedFormat = await getPreferredBitrate(
|
||||
this.dz,
|
||||
track,
|
||||
this.bitrate,
|
||||
this.settings.fallbackBitrate, this.settings.feelingLucky,
|
||||
this.downloadObject.uuid, this.listener
|
||||
)
|
||||
} catch (e) {
|
||||
if (e.name === 'WrongLicense') { throw new DownloadFailed('wrongLicense') }
|
||||
if (e.name === 'WrongGeolocation') { throw new DownloadFailed('wrongGeolocation', track) }
|
||||
if (e.name === 'PreferredBitrateNotFound') { throw new DownloadFailed('wrongBitrate', track) }
|
||||
if (e.name === 'TrackNot360') { throw new DownloadFailed('no360RA') }
|
||||
console.trace(e)
|
||||
throw e
|
||||
}
|
||||
track.bitrate = selectedFormat
|
||||
track.album.bitrate = selectedFormat
|
||||
this.log(itemData, 'gotBitrate')
|
||||
|
||||
// Apply Settings
|
||||
track.applySettings(this.settings)
|
||||
|
||||
// Generate filename and filepath from metadata
|
||||
const {
|
||||
filename,
|
||||
filepath,
|
||||
artistPath,
|
||||
coverPath,
|
||||
extrasPath
|
||||
} = generatePath(track, this.downloadObject, this.settings)
|
||||
if (this.downloadObject.isCanceled) throw new DownloadCanceled()
|
||||
|
||||
// Make sure the filepath exsists
|
||||
fs.mkdirSync(filepath, { recursive: true })
|
||||
const extension = extensions[track.bitrate]
|
||||
let writepath = `${filepath}/${filename}${extension}`
|
||||
|
||||
// Save extrasPath
|
||||
if (extrasPath && !this.downloadObject.extrasPath) {
|
||||
this.downloadObject.extrasPath = extrasPath
|
||||
}
|
||||
|
||||
// Generate covers URLs
|
||||
let embeddedImageFormat = `jpg-${this.settings.jpegImageQuality}`
|
||||
if (this.settings.embeddedArtworkPNG) embeddedImageFormat = 'png'
|
||||
|
||||
track.album.embeddedCoverURL = track.album.pic.getURL(this.settings.embeddedArtworkSize, embeddedImageFormat)
|
||||
let ext = track.album.embeddedCoverURL.slice(-4)
|
||||
if (ext.charAt(0) !== '.') ext = '.jpg'
|
||||
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(itemData, 'getAlbumArt')
|
||||
if (!this.coverQueue[track.album.embeddedCoverPath]) { this.coverQueue[track.album.embeddedCoverPath] = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath) }
|
||||
track.album.embeddedCoverPath = await this.coverQueue[track.album.embeddedCoverPath]
|
||||
if (this.coverQueue[track.album.embeddedCoverPath]) delete this.coverQueue[track.album.embeddedCoverPath]
|
||||
this.log(itemData, 'gotAlbumArt')
|
||||
|
||||
// Save local album art
|
||||
if (coverPath) {
|
||||
returnData.albumURLs = []
|
||||
this.settings.localArtworkFormat.split(',').forEach((picFormat) => {
|
||||
if (['png', 'jpg'].includes(picFormat)) {
|
||||
let extendedFormat = picFormat
|
||||
if (extendedFormat === 'jpg') extendedFormat += `-${this.settings.jpegImageQuality}`
|
||||
const url = track.album.pic.getURL(this.settings.localArtworkSize, extendedFormat)
|
||||
// Skip non deezer pictures at the wrong format
|
||||
if (track.album.pic instanceof StaticPicture && picFormat !== 'jpg') return
|
||||
returnData.albumURLs.push({ url, ext: picFormat })
|
||||
}
|
||||
})
|
||||
returnData.albumPath = coverPath
|
||||
returnData.albumFilename = generateAlbumName(this.settings.coverImageTemplate, track.album, this.settings, track.playlist)
|
||||
}
|
||||
|
||||
// Save artist art
|
||||
if (artistPath) {
|
||||
returnData.artistURLs = []
|
||||
this.settings.localArtworkFormat.split(',').forEach((picFormat) => {
|
||||
// Deezer doesn't support png artist images
|
||||
if (picFormat === 'jpg') {
|
||||
const extendedFormat = `${picFormat}-${this.settings.jpegImageQuality}`
|
||||
const 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 })
|
||||
}
|
||||
})
|
||||
returnData.artistPath = artistPath
|
||||
returnData.artistFilename = generateArtistName(this.settings.artistImageTemplate, track.album.mainArtist, this.settings, track.album.rootArtist)
|
||||
}
|
||||
|
||||
// Save playlist art
|
||||
if (track.playlist) {
|
||||
if (this.playlistURLs.length === 0) {
|
||||
this.settings.localArtworkFormat.split(',').forEach((picFormat) => {
|
||||
if (['png', 'jpg'].includes(picFormat)) {
|
||||
let extendedFormat = picFormat
|
||||
if (extendedFormat === 'jpg') extendedFormat += `-${this.settings.jpegImageQuality}`
|
||||
const url = track.playlist.pic.getURL(this.settings.localArtworkSize, extendedFormat)
|
||||
// Skip non deezer pictures at the wrong format
|
||||
if (track.playlist.pic instanceof StaticPicture && picFormat !== 'jpg') return
|
||||
this.playlistURLs.push({ url, ext: picFormat })
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!this.playlistCovername) {
|
||||
track.playlist.bitrate = track.bitrate
|
||||
track.playlist.dateString = track.playlist.date.format(this.settings.dateFormat)
|
||||
this.playlistCovername = generateAlbumName(this.settings.coverImageTemplate, track.playlist, this.settings, track.playlist)
|
||||
}
|
||||
}
|
||||
|
||||
// Save lyrics in lrc file
|
||||
if (this.settings.syncedLyrics && track.lyrics.sync) {
|
||||
if (!fs.existsSync(`${filepath}/${filename}.lrc`) || [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS].includes(this.settings.overwriteFile)) { fs.writeFileSync(`${filepath}/${filename}.lrc`, track.lyrics.sync) }
|
||||
}
|
||||
|
||||
// Check for overwrite settings
|
||||
let trackAlreadyDownloaded = fs.existsSync(writepath)
|
||||
|
||||
// Don't overwrite and don't mind extension
|
||||
if (!trackAlreadyDownloaded && this.settings.overwriteFile === OverwriteOption.DONT_CHECK_EXT) {
|
||||
const extensions = ['.mp3', '.flac', '.opus', '.m4a']
|
||||
const baseFilename = `${filepath}/${filename}`
|
||||
for (let i = 0; i < extensions.length; i++) {
|
||||
const ext = extensions[i]
|
||||
trackAlreadyDownloaded = fs.existsSync(baseFilename + ext)
|
||||
if (trackAlreadyDownloaded) break
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite only lower bitrates
|
||||
if (trackAlreadyDownloaded && this.settings.overwriteFile === OverwriteOption.ONLY_LOWER_BITRATES && extension === '.mp3') {
|
||||
const stats = fs.statSync(writepath)
|
||||
const fileSizeKb = stats.size * 8 / 1024
|
||||
const bitrateAprox = fileSizeKb / track.duration
|
||||
if (selectedFormat !== 0 && bitrateAprox < 310 && selectedFormat === 3) { trackAlreadyDownloaded = false }
|
||||
}
|
||||
|
||||
// Don't overwrite and keep both files
|
||||
if (trackAlreadyDownloaded && this.settings.overwriteFile === OverwriteOption.KEEP_BOTH) {
|
||||
const baseFilename = `${filepath}/${filename}`
|
||||
let currentFilename
|
||||
let c = 0
|
||||
do {
|
||||
c++
|
||||
currentFilename = `${baseFilename} (${c})${extension}`
|
||||
} while (fs.existsSync(currentFilename))
|
||||
trackAlreadyDownloaded = false
|
||||
writepath = currentFilename
|
||||
}
|
||||
|
||||
// Download the track
|
||||
if (!trackAlreadyDownloaded || this.settings.overwriteFile === OverwriteOption.OVERWRITE) {
|
||||
track.downloadURL = track.urls[formatsName[track.bitrate]]
|
||||
if (!track.downloadURL) throw new DownloadFailed('notAvailable', track)
|
||||
try {
|
||||
await streamTrack(writepath, track, this.downloadObject, this.listener)
|
||||
} catch (e) {
|
||||
if (e instanceof got.HTTPError) throw new DownloadFailed('notAvailable', track)
|
||||
throw e
|
||||
}
|
||||
this.log(itemData, 'downloaded')
|
||||
} else {
|
||||
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(itemData, 'tagging')
|
||||
if (extension === '.mp3') {
|
||||
tagID3(writepath, track, this.settings.tags)
|
||||
} else if (extension === '.flac') {
|
||||
tagFLAC(writepath, track, this.settings.tags)
|
||||
}
|
||||
this.log(itemData, 'tagged')
|
||||
}
|
||||
|
||||
if (track.searched) returnData.searched = true
|
||||
this.downloadObject.downloaded += 1
|
||||
|
||||
if (this.listener) {
|
||||
this.listener.send('updateQueue', {
|
||||
uuid: this.downloadObject.uuid,
|
||||
downloaded: true,
|
||||
downloadPath: String(writepath),
|
||||
extrasPath: String(this.downloadObject.extrasPath)
|
||||
})
|
||||
}
|
||||
returnData.filename = writepath.slice(extrasPath.length + 1)
|
||||
returnData.data = itemData
|
||||
returnData.path = String(writepath)
|
||||
this.downloadObject.files.push(returnData)
|
||||
return returnData
|
||||
}
|
||||
|
||||
async downloadWrapper (extraData, track) {
|
||||
const { trackAPI } = extraData
|
||||
// Temp metadata to generate logs
|
||||
const itemData = {
|
||||
id: trackAPI.id,
|
||||
title: trackAPI.title,
|
||||
artist: trackAPI.artist.name
|
||||
}
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await this.download(extraData, track)
|
||||
} catch (e) {
|
||||
if (e instanceof DownloadFailed) {
|
||||
if (e.track) {
|
||||
const track = e.track
|
||||
if (track.fallbackID !== 0) {
|
||||
this.warn(itemData, e.errid, 'fallback')
|
||||
let newTrack = await this.dz.gw.get_track_with_fallback(track.fallbackID)
|
||||
newTrack = map_track(newTrack)
|
||||
track.parseEssentialData(newTrack)
|
||||
return await this.downloadWrapper(extraData, track)
|
||||
}
|
||||
if (track.albumsFallback.length && this.settings.fallbackISRC) {
|
||||
const newAlbumID = track.albumsFallback.pop()
|
||||
const newAlbum = await this.dz.gw.get_album_page(newAlbumID)
|
||||
let fallbackID = 0
|
||||
for (const newTrack of newAlbum.SONGS.data) {
|
||||
if (newTrack.ISRC === track.ISRC) {
|
||||
fallbackID = newTrack.SNG_ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if (fallbackID !== 0) {
|
||||
this.warn(itemData, e.errid, 'fallback')
|
||||
let newTrack = await this.dz.gw.get_track_with_fallback(fallbackID)
|
||||
newTrack = map_track(newTrack)
|
||||
track.parseEssentialData(newTrack)
|
||||
return await this.downloadWrapper(extraData, track)
|
||||
}
|
||||
}
|
||||
if (!track.searched && this.settings.fallbackSearch) {
|
||||
this.warn(itemData, e.errid, 'search')
|
||||
const 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)
|
||||
newTrack = map_track(newTrack)
|
||||
track.parseEssentialData(newTrack)
|
||||
track.searched = true
|
||||
this.log(itemData, 'searchFallback')
|
||||
return await this.downloadWrapper(extraData, track)
|
||||
}
|
||||
}
|
||||
e.errid += 'NoAlternative'
|
||||
e.message = ErrorMessages[e.errid]
|
||||
}
|
||||
result = {
|
||||
error: {
|
||||
message: e.message,
|
||||
errid: e.errid,
|
||||
data: itemData,
|
||||
type: 'track'
|
||||
}
|
||||
}
|
||||
} else if (e instanceof DownloadCanceled) {
|
||||
return
|
||||
} else {
|
||||
console.trace(e)
|
||||
result = {
|
||||
error: {
|
||||
message: e.message,
|
||||
data: itemData,
|
||||
stack: String(e.stack),
|
||||
type: 'track'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
this.downloadObject.completeTrackProgress(this.listener)
|
||||
this.downloadObject.failed += 1
|
||||
this.downloadObject.errors.push(result.error)
|
||||
if (this.listener) {
|
||||
const error = result.error
|
||||
this.listener.send('updateQueue', {
|
||||
uuid: this.downloadObject.uuid,
|
||||
failed: true,
|
||||
data: error.data,
|
||||
error: error.message,
|
||||
errid: error.errid || null,
|
||||
stack: error.stack || null,
|
||||
type: error.type
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
afterDownloadErrorReport (position, error, itemData = {}) {
|
||||
console.trace(error)
|
||||
this.downloadObject.errors.push({
|
||||
message: error.message,
|
||||
stack: String(error.stack),
|
||||
data: { position, ...itemData },
|
||||
type: 'post'
|
||||
})
|
||||
if (this.listener) {
|
||||
this.listener.send('updateQueue', {
|
||||
uuid: this.downloadObject.uuid,
|
||||
postFailed: true,
|
||||
error: error.message,
|
||||
data: { position, ...itemData },
|
||||
stack: error.stack,
|
||||
type: 'post'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async afterDownloadSingle (track) {
|
||||
if (!track) return
|
||||
if (!this.downloadObject.extrasPath) {
|
||||
this.downloadObject.extrasPath = this.settings.downloadLocation
|
||||
}
|
||||
|
||||
// Save local album artwork
|
||||
try {
|
||||
if (this.settings.saveArtwork && track.albumPath) {
|
||||
await each(track.albumURLs, async (image) => {
|
||||
await downloadImage(image.url, `${track.albumPath}/${track.albumFilename}.${image.ext}`, this.settings.overwriteFile)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('SaveLocalAlbumArt', e)
|
||||
}
|
||||
|
||||
// Save local artist artwork
|
||||
try {
|
||||
if (this.settings.saveArtworkArtist && track.artistPath) {
|
||||
await each(track.artistURLs, async (image) => {
|
||||
await downloadImage(image.url, `${track.artistPath}/${track.artistFilename}.${image.ext}`, this.settings.overwriteFile)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('SaveLocalArtistArt', e)
|
||||
}
|
||||
|
||||
// Create searched logfile
|
||||
try {
|
||||
if (this.settings.logSearched && track.searched) {
|
||||
const filename = `${track.data.artist} - ${track.data.title}`
|
||||
let searchedFile
|
||||
try {
|
||||
searchedFile = fs.readFileSync(`${this.downloadObject.extrasPath}/searched.txt`).toString()
|
||||
} catch { searchedFile = '' }
|
||||
if (searchedFile.indexOf(filename) === -1) {
|
||||
if (searchedFile !== '') searchedFile += '\r\n'
|
||||
searchedFile += filename + '\r\n'
|
||||
fs.writeFileSync(`${this.downloadObject.extrasPath}/searched.txt`, searchedFile)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('CreateSearchedLog', e)
|
||||
}
|
||||
|
||||
// Execute command after download
|
||||
try {
|
||||
if (this.settings.executeCommand !== '') {
|
||||
const child = exec(this.settings.executeCommand.replaceAll('%folder%', shellEscape(this.downloadObject.extrasPath)).replaceAll('%filename%', shellEscape(track.filename)),
|
||||
(error, stdout, stderr) => {
|
||||
if (error) this.afterDownloadErrorReport('ExecuteCommand', error)
|
||||
const itemData = { stderr, stdout }
|
||||
if (stderr) this.log(itemData, 'stderr')
|
||||
if (stdout) this.log(itemData, 'stdout')
|
||||
}
|
||||
)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
child.on('close', resolve)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('ExecuteCommand', e)
|
||||
}
|
||||
}
|
||||
|
||||
async afterDownloadCollection (tracks) {
|
||||
if (!this.downloadObject.extrasPath) {
|
||||
this.downloadObject.extrasPath = this.settings.downloadLocation
|
||||
}
|
||||
|
||||
const playlist = []
|
||||
let errors = ''
|
||||
let searched = ''
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i]
|
||||
if (!track) return
|
||||
|
||||
if (track.error) {
|
||||
if (!track.error.data) track.error.data = { id: '0', title: 'Unknown', artist: 'Unknown' }
|
||||
errors += `${track.error.data.id} | ${track.error.data.artist} - ${track.error.data.title} | ${track.error.message}\r\n`
|
||||
}
|
||||
|
||||
if (track.searched) searched += `${track.data.artist} - ${track.data.title}\r\n`
|
||||
|
||||
// Save local album artwork
|
||||
try {
|
||||
if (this.settings.saveArtwork && track.albumPath) {
|
||||
await each(track.albumURLs, async (image) => {
|
||||
await downloadImage(image.url, `${track.albumPath}/${track.albumFilename}.${image.ext}`, this.settings.overwriteFile)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('SaveLocalAlbumArt', e, track.data)
|
||||
}
|
||||
|
||||
// Save local artist artwork
|
||||
try {
|
||||
if (this.settings.saveArtworkArtist && track.artistPath) {
|
||||
await each(track.artistURLs, async (image) => {
|
||||
await downloadImage(image.url, `${track.artistPath}/${track.artistFilename}.${image.ext}`, this.settings.overwriteFile)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('SaveLocalArtistArt', e, track.data)
|
||||
}
|
||||
|
||||
// Save filename for playlist file
|
||||
playlist[i] = track.filename || ''
|
||||
}
|
||||
|
||||
// Create errors logfile
|
||||
try {
|
||||
if (this.settings.logErrors && errors !== '') { fs.writeFileSync(`${this.downloadObject.extrasPath}/errors.txt`, errors) }
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('CreateErrorLog', e)
|
||||
}
|
||||
|
||||
// Create searched logfile
|
||||
try {
|
||||
if (this.settings.logSearched && searched !== '') { fs.writeFileSync(`${this.downloadObject.extrasPath}/searched.txt`, searched) }
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('CreateSearchedLog', e)
|
||||
}
|
||||
|
||||
// Save Playlist Artwork
|
||||
try {
|
||||
if (this.settings.saveArtwork && this.playlistCovername && !this.settings.tags.savePlaylistAsCompilation) {
|
||||
await each(this.playlistURLs, async (image) => {
|
||||
await downloadImage(image.url, `${this.downloadObject.extrasPath}/${this.playlistCovername}.${image.ext}`, this.settings.overwriteFile)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('SavePlaylistArt', e)
|
||||
}
|
||||
|
||||
// Create M3U8 File
|
||||
try {
|
||||
if (this.settings.createM3U8File) {
|
||||
const filename = generateDownloadObjectName(this.settings.playlistFilenameTemplate, this.downloadObject, this.settings) || 'playlist'
|
||||
fs.writeFileSync(`${this.downloadObject.extrasPath}/${filename}.m3u8`, playlist.join('\n'))
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('CreatePlaylistFile', e)
|
||||
}
|
||||
|
||||
// Execute command after download
|
||||
try {
|
||||
if (this.settings.executeCommand !== '') {
|
||||
const child = exec(this.settings.executeCommand.replaceAll('%folder%', shellEscape(this.downloadObject.extrasPath)).replaceAll('%filename%', ''),
|
||||
(error, stdout, stderr) => {
|
||||
if (error) this.afterDownloadErrorReport('ExecuteCommand', error)
|
||||
const itemData = { stderr, stdout }
|
||||
if (stderr) this.log(itemData, 'stderr')
|
||||
if (stdout) this.log(itemData, 'stdout')
|
||||
}
|
||||
)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
child.on('close', resolve)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.afterDownloadErrorReport('ExecuteCommand', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Downloader,
|
||||
downloadImage,
|
||||
getPreferredBitrate
|
||||
}
|
||||
183
deemix/deemix/errors.js
Normal file
183
deemix/deemix/errors.js
Normal file
@@ -0,0 +1,183 @@
|
||||
class DeemixError extends Error {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'DeemixError'
|
||||
}
|
||||
}
|
||||
|
||||
class GenerationError extends DeemixError {
|
||||
constructor (link, message) {
|
||||
super(message)
|
||||
this.link = link
|
||||
this.name = 'GenerationError'
|
||||
}
|
||||
}
|
||||
|
||||
class ISRCnotOnDeezer extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, 'Track ISRC is not available on deezer')
|
||||
this.name = 'ISRCnotOnDeezer'
|
||||
this.errid = 'ISRCnotOnDeezer'
|
||||
}
|
||||
}
|
||||
|
||||
class NotYourPrivatePlaylist extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, "You can't download others private playlists.")
|
||||
this.name = 'NotYourPrivatePlaylist'
|
||||
this.errid = 'notYourPrivatePlaylist'
|
||||
}
|
||||
}
|
||||
|
||||
class TrackNotOnDeezer extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, 'Track not found on deezer!')
|
||||
this.name = 'TrackNotOnDeezer'
|
||||
this.errid = 'trackNotOnDeezer'
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumNotOnDeezer extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, 'Album not found on deezer!')
|
||||
this.name = 'AlbumNotOnDeezer'
|
||||
this.errid = 'albumNotOnDeezer'
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidID extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, 'Link ID is invalid!')
|
||||
this.name = 'InvalidID'
|
||||
this.errid = 'invalidID'
|
||||
}
|
||||
}
|
||||
|
||||
class LinkNotSupported extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, 'Link is not supported.')
|
||||
this.name = 'LinkNotSupported'
|
||||
this.errid = 'unsupportedURL'
|
||||
}
|
||||
}
|
||||
|
||||
class LinkNotRecognized extends GenerationError {
|
||||
constructor (link) {
|
||||
super(link, 'Link is not recognized.')
|
||||
this.name = 'LinkNotRecognized'
|
||||
this.errid = 'invalidURL'
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadError extends DeemixError {
|
||||
constructor () {
|
||||
super()
|
||||
this.name = 'DownloadError'
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorMessages = {
|
||||
notOnDeezer: 'Track not available on Deezer!',
|
||||
notEncoded: 'Track not yet encoded!',
|
||||
notEncodedNoAlternative: 'Track not yet encoded and no alternative found!',
|
||||
wrongBitrate: 'Track not found at desired bitrate.',
|
||||
wrongBitrateNoAlternative: 'Track not found at desired bitrate and no alternative found!',
|
||||
wrongLicense: "Your account can't stream the track at the desired bitrate.",
|
||||
no360RA: 'Track is not available in Reality Audio 360.',
|
||||
notAvailable: "Track not available on deezer's servers!",
|
||||
notAvailableNoAlternative: "Track not available on deezer's servers and no alternative found!",
|
||||
noSpaceLeft: 'No space left on target drive, clean up some space for the tracks.',
|
||||
albumDoesntExists: "Track's album does not exsist, failed to gather info.",
|
||||
notLoggedIn: 'You need to login to download tracks.',
|
||||
wrongGeolocation: "Your account can't stream the track from your current country.",
|
||||
wrongGeolocationNoAlternative: "Your account can't stream the track from your current country and no alternative found."
|
||||
}
|
||||
|
||||
class DownloadFailed extends DownloadError {
|
||||
constructor (errid, track) {
|
||||
super()
|
||||
this.errid = errid
|
||||
this.message = ErrorMessages[errid]
|
||||
this.name = 'DownloadFailed'
|
||||
this.track = track
|
||||
}
|
||||
}
|
||||
|
||||
class TrackNot360 extends DownloadError {
|
||||
constructor () {
|
||||
super()
|
||||
this.name = 'TrackNot360'
|
||||
}
|
||||
}
|
||||
|
||||
class PreferredBitrateNotFound extends DownloadError {
|
||||
constructor () {
|
||||
super()
|
||||
this.name = 'PreferredBitrateNotFound'
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadEmpty extends DeemixError {
|
||||
constructor () {
|
||||
super()
|
||||
this.name = 'DownloadEmpty'
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadCanceled extends DeemixError {
|
||||
constructor () {
|
||||
super()
|
||||
this.name = 'DownloadCanceled'
|
||||
}
|
||||
}
|
||||
|
||||
class TrackError extends DeemixError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'TrackError'
|
||||
}
|
||||
}
|
||||
|
||||
class MD5NotFound extends TrackError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'MD5NotFound'
|
||||
}
|
||||
}
|
||||
|
||||
class NoDataToParse extends TrackError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'NoDataToParse'
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumDoesntExists extends TrackError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AlbumDoesntExists'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DeemixError,
|
||||
GenerationError,
|
||||
ISRCnotOnDeezer,
|
||||
NotYourPrivatePlaylist,
|
||||
TrackNotOnDeezer,
|
||||
AlbumNotOnDeezer,
|
||||
InvalidID,
|
||||
LinkNotSupported,
|
||||
LinkNotRecognized,
|
||||
ErrorMessages,
|
||||
DownloadError,
|
||||
DownloadFailed,
|
||||
TrackNot360,
|
||||
PreferredBitrateNotFound,
|
||||
DownloadEmpty,
|
||||
DownloadCanceled,
|
||||
TrackError,
|
||||
MD5NotFound,
|
||||
NoDataToParse,
|
||||
AlbumDoesntExists
|
||||
}
|
||||
116
deemix/deemix/index.js
Normal file
116
deemix/deemix/index.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const got = require('got')
|
||||
const {
|
||||
generateTrackItem,
|
||||
generateAlbumItem,
|
||||
generatePlaylistItem,
|
||||
generateArtistItem,
|
||||
generateArtistTopItem
|
||||
} = require('./itemgen.js')
|
||||
const {
|
||||
LinkNotSupported,
|
||||
LinkNotRecognized
|
||||
} = require('./errors.js')
|
||||
|
||||
async function parseLink (link) {
|
||||
if (link.includes('deezer.page.link')) {
|
||||
link = await got.get(link, { https: { rejectUnauthorized: false } }) // Resolve URL shortner
|
||||
link = link.url
|
||||
}
|
||||
// Remove extra stuff
|
||||
if (link.includes('?')) link = link.slice(0, link.indexOf('?'))
|
||||
if (link.includes('&')) link = link.slice(0, link.indexOf('&'))
|
||||
if (link.endsWith('/')) link = link.slice(0, -1) // Remove last slash if present
|
||||
|
||||
let link_type, link_id
|
||||
let link_data
|
||||
|
||||
if (!link.includes('deezer')) return [link, link_type, link_id] // return if not a deezer link
|
||||
|
||||
if (link.search(/\/track\/(.+)/g) !== -1) {
|
||||
link_type = 'track'
|
||||
link_id = /\/track\/(.+)/g.exec(link)[1]
|
||||
} else if (link.search(/\/playlist\/(\d+)/g) !== -1) {
|
||||
link_type = 'playlist'
|
||||
link_id = /\/playlist\/(\d+)/g.exec(link)[1]
|
||||
} else if (link.search(/\/album\/(.+)/g) !== -1) {
|
||||
link_type = 'album'
|
||||
link_id = /\/album\/(.+)/g.exec(link)[1]
|
||||
} else if (link.search(/\/artist\/(\d+)\/top_track/g) !== -1) {
|
||||
link_type = 'artist_top'
|
||||
link_id = /\/artist\/(\d+)\/top_track/g.exec(link)[1]
|
||||
} else if (link.search(/\/artist\/(\d+)\/(.+)/g) !== -1) {
|
||||
link_data = /\/artist\/(\d+)\/(.+)/g.exec(link)
|
||||
link_type = `artist_${link_data[2]}`
|
||||
link_id = link_data[1]
|
||||
} else if (link.search(/\/artist\/(\d+)/g) !== -1) {
|
||||
link_type = 'artist'
|
||||
link_id = /\/artist\/(\d+)/g.exec(link)[1]
|
||||
}
|
||||
|
||||
return [link, link_type, link_id]
|
||||
}
|
||||
|
||||
async function generateDownloadObject (dz, link, bitrate, plugins = {}, listener) {
|
||||
let link_type, link_id
|
||||
[link, link_type, link_id] = await parseLink(link)
|
||||
|
||||
if (link_type == null || link_id == null) {
|
||||
const pluginNames = Object.keys(plugins)
|
||||
let currentPlugin
|
||||
let item = null
|
||||
for (let i = 0; i < pluginNames.length; i++) {
|
||||
currentPlugin = plugins[pluginNames[i]]
|
||||
item = await currentPlugin.generateDownloadObject(dz, link, bitrate, listener)
|
||||
if (item) break
|
||||
}
|
||||
if (item) return item
|
||||
throw new LinkNotRecognized(link)
|
||||
}
|
||||
|
||||
if (link_type === 'track') return generateTrackItem(dz, link_id, bitrate)
|
||||
if (link_type === 'album') return generateAlbumItem(dz, link_id, bitrate)
|
||||
if (link_type === 'playlist') return generatePlaylistItem(dz, link_id, bitrate)
|
||||
if (link_type === 'artist') return generateArtistItem(dz, link_id, bitrate, listener, 'all')
|
||||
if (link_type === 'artist_top') return generateArtistTopItem(dz, link_id, bitrate)
|
||||
if (link_type.startsWith('artist_')) {
|
||||
const tab = link_type.slice(7)
|
||||
return generateArtistItem(dz, link_id, bitrate, listener, tab)
|
||||
}
|
||||
throw new LinkNotSupported(link)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseLink,
|
||||
generateDownloadObject,
|
||||
types: {
|
||||
...require('./types/index.js'),
|
||||
...require('./types/Album.js'),
|
||||
...require('./types/Artist.js'),
|
||||
...require('./types/Date.js'),
|
||||
...require('./types/Lyrics.js'),
|
||||
...require('./types/Picture.js'),
|
||||
...require('./types/Playlist.js'),
|
||||
...require('./types/Track.js'),
|
||||
downloadObjects: require('./types/DownloadObjects.js')
|
||||
},
|
||||
itemgen: {
|
||||
generateTrackItem,
|
||||
generateAlbumItem,
|
||||
generatePlaylistItem,
|
||||
generateArtistItem,
|
||||
generateArtistTopItem
|
||||
},
|
||||
settings: require('./settings.js'),
|
||||
downloader: require('./downloader.js'),
|
||||
decryption: require('./decryption.js'),
|
||||
tagger: require('./tagger.js'),
|
||||
utils: {
|
||||
...require('./utils/index.js'),
|
||||
localpaths: require('./utils/localpaths.js'),
|
||||
pathtemplates: require('./utils/pathtemplates.js'),
|
||||
deezer: require('./utils/deezer.js')
|
||||
},
|
||||
plugins: {
|
||||
spotify: require('./plugins/spotify.js')
|
||||
}
|
||||
}
|
||||
304
deemix/deemix/itemgen.js
Normal file
304
deemix/deemix/itemgen.js
Normal file
@@ -0,0 +1,304 @@
|
||||
const {
|
||||
Single,
|
||||
Collection
|
||||
} = require('./types/DownloadObjects.js')
|
||||
const { GenerationError, ISRCnotOnDeezer, InvalidID, NotYourPrivatePlaylist } = require('./errors.js')
|
||||
const { map_user_playlist, map_track, map_album } = require('deezer-js').utils
|
||||
const { each } = require('async')
|
||||
|
||||
async function generateTrackItem (dz, id, bitrate, trackAPI, albumAPI) {
|
||||
// Get essential track info
|
||||
if (!trackAPI) {
|
||||
if (String(id).startsWith('isrc') || parseInt(id) > 0) {
|
||||
try {
|
||||
trackAPI = await dz.api.get_track(id)
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
throw new GenerationError(`https://deezer.com/track/${id}`, e.message)
|
||||
}
|
||||
// Check if is an isrc: url
|
||||
if (String(id).startsWith('isrc')) {
|
||||
if (trackAPI.id && trackAPI.title) id = trackAPI.id
|
||||
else throw new ISRCnotOnDeezer(`https://deezer.com/track/${id}`)
|
||||
}
|
||||
} else {
|
||||
const trackAPI_gw = await dz.gw.get_track(id)
|
||||
trackAPI = map_track(trackAPI_gw)
|
||||
}
|
||||
} else {
|
||||
id = trackAPI.id
|
||||
}
|
||||
if (!(/^-?\d+$/.test(id))) throw new InvalidID(`https://deezer.com/track/${id}`)
|
||||
|
||||
let cover
|
||||
if (trackAPI.album.cover_small) {
|
||||
cover = trackAPI.album.cover_small.slice(0, -24) + '/75x75-000000-80-0-0.jpg'
|
||||
} else {
|
||||
cover = `https://e-cdns-images.dzcdn.net/images/cover/${trackAPI.md5_image}/75x75-000000-80-0-0.jpg`
|
||||
}
|
||||
|
||||
delete trackAPI.track_token
|
||||
|
||||
return new Single({
|
||||
type: 'track',
|
||||
id,
|
||||
bitrate,
|
||||
title: trackAPI.title,
|
||||
artist: trackAPI.artist.name,
|
||||
cover,
|
||||
explicit: trackAPI.explicit_lyrics,
|
||||
single: {
|
||||
trackAPI,
|
||||
albumAPI
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function generateAlbumItem (dz, id, bitrate, rootArtist) {
|
||||
// Get essential album info
|
||||
let albumAPI
|
||||
if (String(id).startsWith('upc')) {
|
||||
const upcs = [id.slice(4)]
|
||||
upcs.push(parseInt(upcs[0])) // Try UPC without leading zeros as well
|
||||
let lastError
|
||||
await each(upcs, async (upc) => {
|
||||
try {
|
||||
albumAPI = await dz.api.get_album(`upc:${upc}`)
|
||||
} catch (e) {
|
||||
lastError = e
|
||||
albumAPI = null
|
||||
}
|
||||
})
|
||||
if (!albumAPI) {
|
||||
console.trace(lastError)
|
||||
throw new GenerationError(`https://deezer.com/album/${id}`, lastError.message)
|
||||
}
|
||||
id = albumAPI.id
|
||||
} else {
|
||||
try {
|
||||
const albumAPI_gw_page = await dz.gw.get_album_page(id)
|
||||
if (albumAPI_gw_page.DATA) {
|
||||
albumAPI = map_album(albumAPI_gw_page.DATA)
|
||||
id = albumAPI_gw_page.DATA.ALB_ID
|
||||
const albumAPI_new = await dz.api.get_album(id)
|
||||
albumAPI = { ...albumAPI, ...albumAPI_new }
|
||||
} else {
|
||||
throw new GenerationError(`https://deezer.com/album/${id}`, "Can't find the album")
|
||||
}
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
throw new GenerationError(`https://deezer.com/album/${id}`, e.message)
|
||||
}
|
||||
}
|
||||
if (!(/^\d+$/.test(id))) throw new InvalidID(`https://deezer.com/album/${id}`)
|
||||
|
||||
// Get extra info about album
|
||||
// This saves extra api calls when downloading
|
||||
let albumAPI_gw = await dz.gw.get_album(id)
|
||||
albumAPI_gw = map_album(albumAPI_gw)
|
||||
albumAPI = { ...albumAPI_gw, ...albumAPI }
|
||||
albumAPI.root_artist = rootArtist
|
||||
|
||||
// If the album is a single download as a track
|
||||
if (albumAPI.nb_tracks === 1) {
|
||||
if (albumAPI.tracks.data.length) { return generateTrackItem(dz, albumAPI.tracks.data[0].id, bitrate, null, albumAPI) }
|
||||
throw new GenerationError(`https://deezer.com/album/${id}`, 'Single has no tracks.')
|
||||
}
|
||||
|
||||
const tracksArray = await dz.gw.get_album_tracks(id)
|
||||
|
||||
let cover
|
||||
if (albumAPI.cover_small) {
|
||||
cover = albumAPI.cover_small.slice(0, -24) + '/75x75-000000-80-0-0.jpg'
|
||||
} else {
|
||||
cover = `https://e-cdns-images.dzcdn.net/images/cover/${albumAPI.md5_image}/75x75-000000-80-0-0.jpg`
|
||||
}
|
||||
|
||||
const totalSize = tracksArray.length
|
||||
albumAPI.nb_tracks = totalSize
|
||||
const collection = []
|
||||
tracksArray.forEach((trackAPI, pos) => {
|
||||
trackAPI = map_track(trackAPI)
|
||||
delete trackAPI.track_token
|
||||
trackAPI.position = pos + 1
|
||||
collection.push(trackAPI)
|
||||
})
|
||||
|
||||
return new Collection({
|
||||
type: 'album',
|
||||
id,
|
||||
bitrate,
|
||||
title: albumAPI.title,
|
||||
artist: albumAPI.artist.name,
|
||||
cover,
|
||||
explicit: albumAPI.explicit_lyrics,
|
||||
size: totalSize,
|
||||
collection: {
|
||||
tracks: collection,
|
||||
albumAPI
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function generatePlaylistItem (dz, id, bitrate, playlistAPI, playlistTracksAPI) {
|
||||
if (!playlistAPI) {
|
||||
if (!(/^\d+$/.test(id))) throw new InvalidID(`https://deezer.com/playlist/${id}`)
|
||||
// Get essential playlist info
|
||||
try {
|
||||
playlistAPI = await dz.api.get_playlist(id)
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
playlistAPI = null
|
||||
}
|
||||
// Fallback to gw api if the playlist is private
|
||||
if (!playlistAPI) {
|
||||
try {
|
||||
const userPlaylist = await dz.gw.get_playlist_page(id)
|
||||
playlistAPI = map_user_playlist(userPlaylist.DATA)
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
throw new GenerationError(`https://deezer.com/playlist/${id}`, e.message)
|
||||
}
|
||||
}
|
||||
// Check if private playlist and owner
|
||||
if (!playlistAPI.public && playlistAPI.creator.id !== dz.current_user.id) {
|
||||
throw new NotYourPrivatePlaylist(`https://deezer.com/playlist/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!playlistTracksAPI) {
|
||||
playlistTracksAPI = await dz.gw.get_playlist_tracks(id)
|
||||
}
|
||||
playlistAPI.various_artist = await dz.api.get_artist(5080) // Useful for save as compilation
|
||||
|
||||
const totalSize = playlistTracksAPI.length
|
||||
playlistAPI.nb_tracks = totalSize
|
||||
const collection = []
|
||||
playlistTracksAPI.forEach((trackAPI, pos) => {
|
||||
trackAPI = map_track(trackAPI)
|
||||
if (trackAPI.explicit_lyrics) { playlistAPI.explicit = true }
|
||||
delete trackAPI.track_token
|
||||
trackAPI.position = pos + 1
|
||||
collection.push(trackAPI)
|
||||
})
|
||||
|
||||
if (!playlistAPI.explicit) playlistAPI.explicit = false
|
||||
|
||||
return new Collection({
|
||||
type: 'playlist',
|
||||
id,
|
||||
bitrate,
|
||||
title: playlistAPI.title,
|
||||
artist: playlistAPI.creator.name,
|
||||
cover: playlistAPI.picture_small.slice(0, -24) + '/75x75-000000-80-0-0.jpg',
|
||||
explicit: playlistAPI.explicit,
|
||||
size: totalSize,
|
||||
collection: {
|
||||
tracks: collection,
|
||||
playlistAPI
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function generateArtistItem (dz, id, bitrate, listener, tab = 'all') {
|
||||
let path = ''
|
||||
if (tab !== 'all') path = '/' + tab
|
||||
|
||||
if (!(/^\d+$/.test(id))) throw new InvalidID(`https://deezer.com/artist/${id}${path}`)
|
||||
// Get essential artist info
|
||||
let artistAPI
|
||||
try {
|
||||
artistAPI = await dz.api.get_artist(id)
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
throw new GenerationError(`https://deezer.com/artist/${id}${path}`, e.message)
|
||||
}
|
||||
|
||||
const rootArtist = {
|
||||
id: artistAPI.id,
|
||||
name: artistAPI.name,
|
||||
picture_small: artistAPI.picture_small
|
||||
}
|
||||
if (listener) { listener.send('startAddingArtist', rootArtist) }
|
||||
const artistDiscographyAPI = await dz.gw.get_artist_discography_tabs(id, 100)
|
||||
const albumList = []
|
||||
if (tab === 'discography') {
|
||||
delete artistDiscographyAPI.all
|
||||
await each(artistDiscographyAPI, async (type) => {
|
||||
await each(type, async (album) => {
|
||||
try {
|
||||
const albumData = await generateAlbumItem(dz, album.id, bitrate, rootArtist)
|
||||
albumList.push(albumData)
|
||||
} catch (e) {
|
||||
console.warn(album.id, 'No Data', e)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const tabReleases = artistDiscographyAPI[tab] || []
|
||||
await each(tabReleases, async (album) => {
|
||||
try {
|
||||
const albumData = await generateAlbumItem(dz, album.id, bitrate, rootArtist)
|
||||
albumList.push(albumData)
|
||||
} catch (e) {
|
||||
console.warn(album.id, 'No Data', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (listener) { listener.send('finishAddingArtist', rootArtist) }
|
||||
return albumList
|
||||
}
|
||||
|
||||
async function generateArtistTopItem (dz, id, bitrate) {
|
||||
if (!(/^\d+$/.test(id))) throw new InvalidID(`https://deezer.com/artist/${id}/top_track`)
|
||||
// Get essential artist info
|
||||
let artistAPI
|
||||
try {
|
||||
artistAPI = await dz.api.get_artist(id)
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
throw new GenerationError(`https://deezer.com/artist/${id}/top_track`, e.message)
|
||||
}
|
||||
|
||||
// Emulate the creation of a playlist
|
||||
// Can't use generatePlaylistItem directly as this is not a real playlist
|
||||
const playlistAPI = {
|
||||
id: artistAPI.id + '_top_track',
|
||||
title: artistAPI.name + ' - Top Tracks',
|
||||
description: 'Top Tracks for ' + artistAPI.name,
|
||||
duration: 0,
|
||||
public: true,
|
||||
is_loved_track: false,
|
||||
collaborative: false,
|
||||
nb_tracks: 0,
|
||||
fans: artistAPI.nb_fan,
|
||||
link: 'https://www.deezer.com/artist/' + artistAPI.id + '/top_track',
|
||||
share: null,
|
||||
picture: artistAPI.picture,
|
||||
picture_small: artistAPI.picture_small,
|
||||
picture_medium: artistAPI.picture_medium,
|
||||
picture_big: artistAPI.picture_big,
|
||||
picture_xl: artistAPI.picture_xl,
|
||||
checksum: null,
|
||||
tracklist: 'https://api.deezer.com/artist/' + artistAPI.id + '/top',
|
||||
creation_date: 'XXXX-00-00',
|
||||
creator: {
|
||||
id: 'art_' + artistAPI.id,
|
||||
name: artistAPI.name,
|
||||
type: 'user'
|
||||
},
|
||||
type: 'playlist'
|
||||
}
|
||||
|
||||
const artistTopTracksAPI_gw = await dz.gw.get_artist_top_tracks(id)
|
||||
return generatePlaylistItem(dz, playlistAPI.id, bitrate, playlistAPI, artistTopTracksAPI_gw)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateTrackItem,
|
||||
generateAlbumItem,
|
||||
generatePlaylistItem,
|
||||
generateArtistItem,
|
||||
generateArtistTopItem
|
||||
}
|
||||
15
deemix/deemix/plugins/index.js
Normal file
15
deemix/deemix/plugins/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
class Plugin {
|
||||
/* constructor () {} */
|
||||
async setup () {}
|
||||
|
||||
async parseLink (link) {
|
||||
return [link, undefined, undefined]
|
||||
}
|
||||
|
||||
/* eslint no-unused-vars: ["error", { "args": "none" }] */
|
||||
async generateDownloadObject (dz, link, bitrate, listener) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plugin
|
||||
455
deemix/deemix/plugins/spotify.js
Normal file
455
deemix/deemix/plugins/spotify.js
Normal file
@@ -0,0 +1,455 @@
|
||||
const Plugin = require('./index.js')
|
||||
const { getConfigFolder } = require('../utils/localpaths.js')
|
||||
const {
|
||||
generateTrackItem,
|
||||
generateAlbumItem,
|
||||
TrackNotOnDeezer,
|
||||
AlbumNotOnDeezer,
|
||||
InvalidID
|
||||
} = require('../itemgen.js')
|
||||
const { Convertable, Collection } = require('../types/DownloadObjects.js')
|
||||
const { sep } = require('path')
|
||||
const fs = require('fs')
|
||||
const SpotifyWebApi = require('spotify-web-api-node')
|
||||
const got = require('got')
|
||||
const { queue } = require('async')
|
||||
|
||||
class Spotify extends Plugin {
|
||||
constructor (configFolder = undefined) {
|
||||
super()
|
||||
this.credentials = { clientId: '', clientSecret: '' }
|
||||
this.settings = {
|
||||
fallbackSearch: false
|
||||
}
|
||||
this.enabled = false
|
||||
/* this.sp */
|
||||
this.configFolder = configFolder || getConfigFolder()
|
||||
this.configFolder += `spotify${sep}`
|
||||
return this
|
||||
}
|
||||
|
||||
setup () {
|
||||
fs.mkdirSync(this.configFolder, { recursive: true })
|
||||
|
||||
this.loadSettings()
|
||||
return this
|
||||
}
|
||||
|
||||
async parseLink (link) {
|
||||
if (link.includes('link.tospotify.com')) {
|
||||
link = await got.get(link, { https: { rejectUnauthorized: false } }) // Resolve URL shortner
|
||||
link = link.url
|
||||
}
|
||||
// Remove extra stuff
|
||||
if (link.includes('?')) link = link.slice(0, link.indexOf('?'))
|
||||
if (link.includes('&')) link = link.slice(0, link.indexOf('&'))
|
||||
if (link.endsWith('/')) link = link.slice(0, -1) // Remove last slash if present
|
||||
|
||||
let link_type, link_id
|
||||
|
||||
if (!link.includes('spotify')) return [link, link_type, link_id] // return if not a spotify link
|
||||
|
||||
if (link.search(/[/:]track[/:](.+)/g) !== -1) {
|
||||
link_type = 'track'
|
||||
link_id = /[/:]track[/:](.+)/g.exec(link)[1]
|
||||
} else if (link.search(/[/:]album[/:](.+)/g) !== -1) {
|
||||
link_type = 'album'
|
||||
link_id = /[/:]album[/:](.+)/g.exec(link)[1]
|
||||
} else if (link.search(/[/:]playlist[/:](\d+)/g) !== -1) {
|
||||
link_type = 'playlist'
|
||||
link_id = /[/:]playlist[/:](.+)/g.exec(link)[1]
|
||||
}
|
||||
|
||||
return [link, link_type, link_id]
|
||||
}
|
||||
|
||||
async generateDownloadObject (dz, link, bitrate) {
|
||||
let link_type, link_id
|
||||
[link, link_type, link_id] = await this.parseLink(link)
|
||||
|
||||
if (link_type == null || link_id == null) return null
|
||||
|
||||
switch (link_type) {
|
||||
case 'track':
|
||||
return this.generateTrackItem(dz, link_id, bitrate)
|
||||
case 'album':
|
||||
return this.generateAlbumItem(dz, link_id, bitrate)
|
||||
case 'playlist':
|
||||
return this.generatePlaylistItem(dz, link_id, bitrate)
|
||||
}
|
||||
}
|
||||
|
||||
async generateTrackItem (dz, link_id, bitrate) {
|
||||
const cache = this.loadCache()
|
||||
|
||||
let cachedTrack
|
||||
if (cache.tracks[link_id]) {
|
||||
cachedTrack = cache.tracks[link_id]
|
||||
} else {
|
||||
cachedTrack = await this.getTrack(link_id)
|
||||
cache.tracks[link_id] = cachedTrack
|
||||
this.saveCache(cache)
|
||||
}
|
||||
|
||||
if (cachedTrack.isrc) {
|
||||
try { return generateTrackItem(dz, `isrc:${cachedTrack.isrc}`, bitrate) } catch (e) { /* empty */ }
|
||||
}
|
||||
if (this.settings.fallbackSearch) {
|
||||
if (!cachedTrack.id || cachedTrack.id === '0') {
|
||||
const trackID = await dz.api.get_track_id_from_metadata(
|
||||
cachedTrack.data.artist,
|
||||
cachedTrack.data.title,
|
||||
cachedTrack.data.album
|
||||
)
|
||||
if (trackID !== '0') {
|
||||
cachedTrack.id = trackID
|
||||
cache.tracks[link_id] = cachedTrack
|
||||
this.saveCache(cache)
|
||||
}
|
||||
}
|
||||
if (cachedTrack.id !== '0') return generateTrackItem(dz, cachedTrack.id, bitrate)
|
||||
}
|
||||
throw new TrackNotOnDeezer(`https://open.spotify.com/track/${link_id}`)
|
||||
}
|
||||
|
||||
async generateAlbumItem (dz, link_id, bitrate) {
|
||||
const cache = this.loadCache()
|
||||
|
||||
let cachedAlbum
|
||||
if (cache.albums[link_id]) {
|
||||
cachedAlbum = cache.albums[link_id]
|
||||
} else {
|
||||
cachedAlbum = await this.getAlbum(link_id)
|
||||
cache.albums[link_id] = cachedAlbum
|
||||
this.saveCache(cache)
|
||||
}
|
||||
|
||||
try {
|
||||
return generateAlbumItem(dz, `upc:${cachedAlbum.upc}`, bitrate)
|
||||
} catch (e) {
|
||||
throw new AlbumNotOnDeezer(`https://open.spotify.com/album/${link_id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async generatePlaylistItem (dz, link_id, bitrate) {
|
||||
if (!this.enabled) throw new Error('Spotify plugin not enabled')
|
||||
let spotifyPlaylist = await this.sp.getPlaylist(link_id)
|
||||
spotifyPlaylist = spotifyPlaylist.body
|
||||
|
||||
const playlistAPI = this._convertPlaylistStructure(spotifyPlaylist)
|
||||
playlistAPI.various_artist = await dz.api.get_artist(5080) // Useful for save as compilation
|
||||
|
||||
let tracklistTemp = spotifyPlaylist.tracks.items
|
||||
while (spotifyPlaylist.tracks.next) {
|
||||
const regExec = /offset=(\d+)&limit=(\d+)/g.exec(spotifyPlaylist.tracks.next)
|
||||
const offset = regExec[1]
|
||||
const limit = regExec[2]
|
||||
const playlistTracks = await this.sp.getPlaylistTracks(link_id, { offset, limit })
|
||||
spotifyPlaylist.tracks = playlistTracks.body
|
||||
tracklistTemp = tracklistTemp.concat(spotifyPlaylist.tracks.items)
|
||||
}
|
||||
|
||||
const tracklist = []
|
||||
tracklistTemp.forEach((item) => {
|
||||
if (!item.track) return // Skip everything that isn't a track
|
||||
if (item.track.explicit && !playlistAPI.explicit) playlistAPI.explicit = true
|
||||
tracklist.push(item.track)
|
||||
})
|
||||
if (!playlistAPI.explicit) playlistAPI.explicit = false
|
||||
|
||||
return new Convertable({
|
||||
type: 'spotify_playlist',
|
||||
id: link_id,
|
||||
bitrate,
|
||||
title: spotifyPlaylist.name,
|
||||
artist: spotifyPlaylist.owner.display_name,
|
||||
cover: playlistAPI.picture_thumbnail,
|
||||
explicit: playlistAPI.explicit,
|
||||
size: tracklist.length,
|
||||
collection: {
|
||||
tracks: [],
|
||||
playlistAPI
|
||||
},
|
||||
plugin: 'spotify',
|
||||
conversion_data: tracklist
|
||||
})
|
||||
}
|
||||
|
||||
async getTrack (track_id, spotifyTrack = null) {
|
||||
if (!this.enabled) throw new Error('Spotify plugin not enabled')
|
||||
const cachedTrack = {
|
||||
isrc: null,
|
||||
data: null
|
||||
}
|
||||
|
||||
if (!spotifyTrack) {
|
||||
try {
|
||||
spotifyTrack = await this.sp.getTrack(track_id)
|
||||
} catch (e) {
|
||||
if (e.body.error.message === 'invalid id') throw new InvalidID(`https://open.spotify.com/track/${track_id}`)
|
||||
throw e
|
||||
}
|
||||
spotifyTrack = spotifyTrack.body
|
||||
}
|
||||
if (spotifyTrack.external_ids && spotifyTrack.external_ids.isrc) cachedTrack.isrc = spotifyTrack.external_ids.isrc
|
||||
cachedTrack.data = {
|
||||
title: spotifyTrack.name,
|
||||
artist: spotifyTrack.artists[0].name,
|
||||
album: spotifyTrack.album.name
|
||||
}
|
||||
return cachedTrack
|
||||
}
|
||||
|
||||
async getAlbum (album_id, spotifyAlbum = null) {
|
||||
if (!this.enabled) throw new Error('Spotify plugin not enabled')
|
||||
const cachedAlbum = {
|
||||
upc: null,
|
||||
data: null
|
||||
}
|
||||
|
||||
if (!spotifyAlbum) {
|
||||
try {
|
||||
spotifyAlbum = await this.sp.getAlbum(album_id)
|
||||
} catch (e) {
|
||||
if (e.body.error.message === 'invalid id') throw new InvalidID(`https://open.spotify.com/album/${album_id}`)
|
||||
throw e
|
||||
}
|
||||
spotifyAlbum = spotifyAlbum.body
|
||||
}
|
||||
if (spotifyAlbum.external_ids && spotifyAlbum.external_ids.upc) cachedAlbum.upc = spotifyAlbum.external_ids.upc
|
||||
cachedAlbum.data = {
|
||||
title: spotifyAlbum.name,
|
||||
artist: spotifyAlbum.artists[0].name
|
||||
}
|
||||
return cachedAlbum
|
||||
}
|
||||
|
||||
async convert (dz, downloadObject, settings, listener = null) {
|
||||
const cache = this.loadCache()
|
||||
|
||||
let conversion = 0
|
||||
let conversionNext = 0
|
||||
|
||||
const collection = []
|
||||
if (listener) listener.send('startConversion', downloadObject.uuid)
|
||||
const q = queue(async (data) => {
|
||||
const { track, pos } = data
|
||||
if (downloadObject.isCanceled) return
|
||||
|
||||
let cachedTrack, trackAPI
|
||||
if (cache.tracks[track.id]) {
|
||||
cachedTrack = cache.tracks[track.id]
|
||||
} else {
|
||||
cachedTrack = await this.getTrack(track.id, track)
|
||||
cache.tracks[track.id] = cachedTrack
|
||||
this.saveCache(cache)
|
||||
}
|
||||
|
||||
if (cachedTrack.isrc) {
|
||||
try {
|
||||
trackAPI = await dz.api.get_track_by_ISRC(cachedTrack.isrc)
|
||||
if (!trackAPI.id || !trackAPI.title) trackAPI = null
|
||||
} catch { /* Empty */ }
|
||||
}
|
||||
if (this.settings.fallbackSearch && !trackAPI) {
|
||||
if (!cachedTrack.id || cachedTrack.id === '0') {
|
||||
const trackID = await dz.api.get_track_id_from_metadata(
|
||||
cachedTrack.data.artist,
|
||||
cachedTrack.data.title,
|
||||
cachedTrack.data.album
|
||||
)
|
||||
if (trackID !== '0') {
|
||||
cachedTrack.id = trackID
|
||||
cache.tracks[track.id] = cachedTrack
|
||||
this.saveCache(cache)
|
||||
}
|
||||
}
|
||||
if (cachedTrack.id !== '0') trackAPI = await dz.api.get_track(cachedTrack.id)
|
||||
}
|
||||
if (!trackAPI) {
|
||||
trackAPI = {
|
||||
id: '0',
|
||||
title: track.name,
|
||||
duration: 0,
|
||||
md5_origin: 0,
|
||||
media_version: 0,
|
||||
filesizes: {},
|
||||
album: {
|
||||
title: track.album.name,
|
||||
md5_image: ''
|
||||
},
|
||||
artist: {
|
||||
id: 0,
|
||||
name: track.artists[0].name,
|
||||
md5_image: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
trackAPI.position = pos + 1
|
||||
collection[pos] = trackAPI
|
||||
|
||||
conversionNext += (1 / downloadObject.size) * 100
|
||||
if (Math.round(conversionNext) !== conversion && Math.round(conversionNext) % 2 === 0) {
|
||||
conversion = Math.round(conversionNext)
|
||||
if (listener) listener.send('updateQueue', { uuid: downloadObject.uuid, conversion })
|
||||
}
|
||||
}, settings.queueConcurrency)
|
||||
|
||||
downloadObject.conversion_data.forEach((track, pos) => {
|
||||
q.push({ track, pos })
|
||||
})
|
||||
|
||||
await q.drain()
|
||||
|
||||
downloadObject.collection.tracks = collection
|
||||
downloadObject.size = collection.length
|
||||
downloadObject = new Collection(downloadObject.toDict())
|
||||
if (listener) listener.send('finishConversion', downloadObject.getSlimmedDict())
|
||||
|
||||
fs.writeFileSync(this.configFolder + 'cache.json', JSON.stringify(cache))
|
||||
return downloadObject
|
||||
}
|
||||
|
||||
_convertPlaylistStructure (spotifyPlaylist) {
|
||||
let cover = null
|
||||
if (spotifyPlaylist.images.length) cover = spotifyPlaylist.images[0].url
|
||||
|
||||
const deezerPlaylist = {
|
||||
checksum: spotifyPlaylist.snapshot_id,
|
||||
collaborative: spotifyPlaylist.collaborative,
|
||||
creation_date: 'XXXX-00-00',
|
||||
creator: {
|
||||
id: spotifyPlaylist.owner.id,
|
||||
name: spotifyPlaylist.owner.display_name,
|
||||
tracklist: spotifyPlaylist.owner.href,
|
||||
type: 'user'
|
||||
},
|
||||
description: spotifyPlaylist.description,
|
||||
duration: 0,
|
||||
fans: spotifyPlaylist.followers ? spotifyPlaylist.followers.total : 0,
|
||||
id: spotifyPlaylist.id,
|
||||
is_loved_track: false,
|
||||
link: spotifyPlaylist.external_urls.spotify,
|
||||
nb_tracks: spotifyPlaylist.tracks.total,
|
||||
picture: cover,
|
||||
picture_small: cover || 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/56x56-000000-80-0-0.jpg',
|
||||
picture_medium: cover || 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/250x250-000000-80-0-0.jpg',
|
||||
picture_big: cover || 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/500x500-000000-80-0-0.jpg',
|
||||
picture_xl: cover || 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg',
|
||||
picture_thumbnail: cover || 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg',
|
||||
public: spotifyPlaylist.public,
|
||||
share: spotifyPlaylist.external_urls.spotify,
|
||||
title: spotifyPlaylist.name,
|
||||
tracklist: spotifyPlaylist.tracks.href,
|
||||
type: 'playlist'
|
||||
}
|
||||
|
||||
return deezerPlaylist
|
||||
}
|
||||
|
||||
loadSettings () {
|
||||
if (!fs.existsSync(this.configFolder + 'config.json')) {
|
||||
fs.writeFileSync(this.configFolder + 'config.json', JSON.stringify({
|
||||
...this.credentials,
|
||||
...this.settings
|
||||
}, null, 2))
|
||||
}
|
||||
let settings
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(this.configFolder + 'config.json'))
|
||||
} catch (e) {
|
||||
if (e.name === 'SyntaxError') {
|
||||
fs.writeFileSync(this.configFolder + 'config.json', JSON.stringify({
|
||||
...this.credentials,
|
||||
...this.settings
|
||||
}, null, 2))
|
||||
}
|
||||
settings = JSON.parse(JSON.stringify({
|
||||
...this.credentials,
|
||||
...this.settings
|
||||
}))
|
||||
}
|
||||
this.setSettings(settings)
|
||||
this.checkCredentials()
|
||||
}
|
||||
|
||||
saveSettings (newSettings) {
|
||||
if (newSettings) this.setSettings(newSettings)
|
||||
this.checkCredentials()
|
||||
fs.writeFileSync(this.configFolder + 'config.json', JSON.stringify({
|
||||
...this.credentials,
|
||||
...this.settings
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
getSettings () {
|
||||
return {
|
||||
...this.credentials,
|
||||
...this.settings
|
||||
}
|
||||
}
|
||||
|
||||
setSettings (newSettings) {
|
||||
this.credentials = { clientId: newSettings.clientId, clientSecret: newSettings.clientSecret }
|
||||
const settings = { ...newSettings }
|
||||
delete settings.clientId
|
||||
delete settings.clientSecret
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
loadCache () {
|
||||
let cache
|
||||
try {
|
||||
cache = JSON.parse(fs.readFileSync(this.configFolder + 'cache.json'))
|
||||
} catch (e) {
|
||||
if (e.name === 'SyntaxError') {
|
||||
fs.writeFileSync(this.configFolder + 'cache.json', JSON.stringify(
|
||||
{ tracks: {}, albums: {} },
|
||||
null, 2
|
||||
))
|
||||
}
|
||||
cache = { tracks: {}, albums: {} }
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
saveCache (newCache) {
|
||||
fs.writeFileSync(this.configFolder + 'cache.json', JSON.stringify(newCache))
|
||||
}
|
||||
|
||||
checkCredentials () {
|
||||
if (this.credentials.clientId === '' || this.credentials.clientSecret === '') {
|
||||
this.enabled = false
|
||||
return
|
||||
}
|
||||
this.sp = new SpotifyWebApi(this.credentials)
|
||||
this.sp.clientCredentialsGrant().then(
|
||||
(creds) => {
|
||||
this.sp.setAccessToken(creds.body.access_token)
|
||||
// Need to get a new access_token when it expires
|
||||
setTimeout(() => {
|
||||
this.checkCredentials()
|
||||
}, creds.body.expires_in * 1000 - 10)
|
||||
this.enabled = true
|
||||
},
|
||||
() => {
|
||||
this.enabled = false
|
||||
this.sp = undefined
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getCredentials () {
|
||||
return this.credentials
|
||||
}
|
||||
|
||||
setCredentials (clientId, clientSecret) {
|
||||
clientId = clientId.trim()
|
||||
clientSecret = clientSecret.trim()
|
||||
|
||||
this.credentials = { clientId, clientSecret }
|
||||
this.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Spotify
|
||||
175
deemix/deemix/settings.js
Normal file
175
deemix/deemix/settings.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const { TrackFormats } = require('deezer-js')
|
||||
const { getMusicFolder, getConfigFolder } = require('./utils/localpaths.js')
|
||||
const fs = require('fs')
|
||||
|
||||
// Should the lib overwrite files?
|
||||
const OverwriteOption = {
|
||||
OVERWRITE: 'y', // Yes, overwrite the file
|
||||
DONT_OVERWRITE: 'n', // No, don't overwrite the file
|
||||
DONT_CHECK_EXT: 'e', // No, and don't check for extensions
|
||||
KEEP_BOTH: 'b', // No, and keep both files
|
||||
ONLY_TAGS: 't', // Overwrite only the tags
|
||||
ONLY_LOWER_BITRATES: 'l' // Overwrite only lower bitrates
|
||||
}
|
||||
|
||||
// What should I do with featured artists?
|
||||
const FeaturesOption = {
|
||||
NO_CHANGE: '0', // Do nothing
|
||||
REMOVE_TITLE: '1', // Remove from track title
|
||||
REMOVE_TITLE_ALBUM: '3', // Remove from track title and album title
|
||||
MOVE_TITLE: '2' // Move to track title
|
||||
}
|
||||
|
||||
const DEFAULTS = {
|
||||
downloadLocation: getMusicFolder(),
|
||||
tracknameTemplate: '%artist% - %title%',
|
||||
albumTracknameTemplate: '%tracknumber% - %title%',
|
||||
playlistTracknameTemplate: '%position% - %artist% - %title%',
|
||||
createPlaylistFolder: true,
|
||||
playlistNameTemplate: '%playlist%',
|
||||
createArtistFolder: false,
|
||||
artistNameTemplate: '%artist%',
|
||||
createAlbumFolder: true,
|
||||
albumNameTemplate: '%artist% - %album%',
|
||||
createCDFolder: true,
|
||||
createStructurePlaylist: false,
|
||||
createSingleFolder: false,
|
||||
padTracks: true,
|
||||
padSingleDigit: true,
|
||||
paddingSize: '0',
|
||||
illegalCharacterReplacer: '_',
|
||||
queueConcurrency: 3,
|
||||
maxBitrate: String(TrackFormats.MP3_128),
|
||||
feelingLucky: false,
|
||||
fallbackBitrate: false,
|
||||
fallbackSearch: false,
|
||||
fallbackISRC: false,
|
||||
logErrors: true,
|
||||
logSearched: false,
|
||||
overwriteFile: OverwriteOption.DONT_OVERWRITE,
|
||||
createM3U8File: false,
|
||||
playlistFilenameTemplate: 'playlist',
|
||||
syncedLyrics: false,
|
||||
embeddedArtworkSize: 800,
|
||||
embeddedArtworkPNG: false,
|
||||
localArtworkSize: 1200,
|
||||
localArtworkFormat: 'jpg',
|
||||
saveArtwork: true,
|
||||
coverImageTemplate: 'cover',
|
||||
saveArtworkArtist: false,
|
||||
artistImageTemplate: 'folder',
|
||||
jpegImageQuality: 90,
|
||||
dateFormat: 'Y-M-D',
|
||||
albumVariousArtists: true,
|
||||
removeAlbumVersion: false,
|
||||
removeDuplicateArtists: true,
|
||||
featuredToTitle: FeaturesOption.NO_CHANGE,
|
||||
titleCasing: 'nothing',
|
||||
artistCasing: 'nothing',
|
||||
executeCommand: '',
|
||||
tags: {
|
||||
title: true,
|
||||
artist: true,
|
||||
artists: true,
|
||||
album: true,
|
||||
cover: true,
|
||||
trackNumber: true,
|
||||
trackTotal: false,
|
||||
discNumber: true,
|
||||
discTotal: false,
|
||||
albumArtist: true,
|
||||
genre: true,
|
||||
year: true,
|
||||
date: true,
|
||||
explicit: false,
|
||||
isrc: true,
|
||||
length: true,
|
||||
barcode: true,
|
||||
bpm: true,
|
||||
replayGain: false,
|
||||
label: true,
|
||||
lyrics: false,
|
||||
syncedLyrics: false,
|
||||
copyright: false,
|
||||
composer: false,
|
||||
involvedPeople: false,
|
||||
source: false,
|
||||
rating: false,
|
||||
savePlaylistAsCompilation: false,
|
||||
useNullSeparator: false,
|
||||
saveID3v1: true,
|
||||
multiArtistSeparator: 'default',
|
||||
singleAlbumArtist: false,
|
||||
coverDescriptionUTF8: false
|
||||
}
|
||||
}
|
||||
|
||||
function save (settings, configFolder) {
|
||||
configFolder = configFolder || getConfigFolder()
|
||||
if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder)
|
||||
|
||||
fs.writeFileSync(configFolder + 'config.json', JSON.stringify(settings, null, 2))
|
||||
}
|
||||
|
||||
function load (configFolder) {
|
||||
configFolder = configFolder || getConfigFolder()
|
||||
if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder)
|
||||
|
||||
if (!fs.existsSync(configFolder + 'config.json')) save(DEFAULTS, configFolder)
|
||||
|
||||
let settings
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(configFolder + 'config.json'))
|
||||
} catch (e) {
|
||||
if (e.name === 'SyntaxError') save(DEFAULTS, configFolder)
|
||||
settings = JSON.parse(JSON.stringify(DEFAULTS))
|
||||
}
|
||||
if (check(settings) > 0) save(settings, configFolder)
|
||||
return settings
|
||||
}
|
||||
|
||||
function check (settings) {
|
||||
let changes = 0
|
||||
Object.keys(DEFAULTS).forEach(_iSet => {
|
||||
if (settings[_iSet] === undefined || typeof settings[_iSet] !== typeof DEFAULTS[_iSet]) {
|
||||
settings[_iSet] = DEFAULTS[_iSet]
|
||||
changes++
|
||||
}
|
||||
})
|
||||
Object.keys(DEFAULTS.tags).forEach(_iSet => {
|
||||
if (settings.tags[_iSet] === undefined || typeof settings.tags[_iSet] !== typeof DEFAULTS.tags[_iSet]) {
|
||||
settings.tags[_iSet] = DEFAULTS.tags[_iSet]
|
||||
changes++
|
||||
}
|
||||
})
|
||||
if (settings.downloadLocation === '') {
|
||||
settings.downloadLocation = DEFAULTS.downloadLocation
|
||||
changes++
|
||||
}
|
||||
[
|
||||
'tracknameTemplate',
|
||||
'albumTracknameTemplate',
|
||||
'playlistTracknameTemplate',
|
||||
'playlistNameTemplate',
|
||||
'artistNameTemplate',
|
||||
'albumNameTemplate',
|
||||
'playlistFilenameTemplate',
|
||||
'coverImageTemplate',
|
||||
'artistImageTemplate',
|
||||
'paddingSize'
|
||||
].forEach(template => {
|
||||
if (settings[template] === '') {
|
||||
settings[template] = DEFAULTS[template]
|
||||
changes++
|
||||
}
|
||||
})
|
||||
return changes
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OverwriteOption,
|
||||
FeaturesOption,
|
||||
DEFAULTS,
|
||||
save,
|
||||
load
|
||||
}
|
||||
318
deemix/deemix/tagger.js
Normal file
318
deemix/deemix/tagger.js
Normal file
@@ -0,0 +1,318 @@
|
||||
const ID3Writer = require('./utils/id3-writer.js')
|
||||
const Metaflac = require('metaflac-js2')
|
||||
const fs = require('fs')
|
||||
|
||||
function tagID3 (path, track, save) {
|
||||
const songBuffer = fs.readFileSync(path)
|
||||
const tag = new ID3Writer(songBuffer)
|
||||
tag.separateWithNull = String.fromCharCode(0)
|
||||
|
||||
if (save.title) tag.setFrame('TIT2', track.title)
|
||||
|
||||
if (save.artist && track.artists.length) {
|
||||
if (save.multiArtistSeparator === 'default') {
|
||||
tag.setFrame('TPE1', track.artists)
|
||||
} else {
|
||||
if (save.multiArtistSeparator === 'nothing') {
|
||||
tag.setFrame('TPE1', [track.mainArtist.name])
|
||||
} else {
|
||||
tag.setFrame('TPE1', [track.artistsString])
|
||||
}
|
||||
// Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
|
||||
// https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists
|
||||
if (save.artists) {
|
||||
tag.setFrame('TXXX', {
|
||||
description: 'ARTISTS',
|
||||
value: track.artists
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (save.album) tag.setFrame('TALB', track.album.title)
|
||||
|
||||
if (save.albumArtist && track.album.artists.length) {
|
||||
if (save.singleAlbumArtist && track.album.mainArtist.save) {
|
||||
tag.setFrame('TPE2', [track.album.mainArtist.name])
|
||||
} else {
|
||||
tag.setFrame('TPE2', track.album.artists)
|
||||
}
|
||||
}
|
||||
|
||||
if (save.trackNumber) {
|
||||
let trackNumber = String(track.trackNumber)
|
||||
if (save.trackTotal) trackNumber += `/${track.album.trackTotal}`
|
||||
tag.setFrame('TRCK', trackNumber)
|
||||
}
|
||||
if (save.discNumber) {
|
||||
let discNumber = String(track.discNumber)
|
||||
if (save.discTotal) discNumber += `/${track.album.discTotal}`
|
||||
tag.setFrame('TPOS', discNumber)
|
||||
}
|
||||
|
||||
if (save.genre) tag.setFrame('TCON', track.album.genre)
|
||||
if (save.year) tag.setFrame('TYER', track.date.year)
|
||||
|
||||
// Referencing ID3 standard
|
||||
// https://id3.org/id3v2.3.0#TDAT
|
||||
// The 'Date' frame is a numeric string in the DDMM format.
|
||||
if (save.date) tag.setFrame('TDAT', '' + track.date.day + track.date.month)
|
||||
|
||||
if (save.length) tag.setFrame('TLEN', parseInt(track.duration) * 1000)
|
||||
if (save.bpm && track.bpm) tag.setFrame('TBPM', track.bpm)
|
||||
if (save.label) tag.setFrame('TPUB', track.album.label)
|
||||
if (save.isrc) tag.setFrame('TSRC', track.ISRC)
|
||||
if (save.barcode) {
|
||||
tag.setFrame('TXXX', {
|
||||
description: 'BARCODE',
|
||||
value: track.album.barcode
|
||||
})
|
||||
}
|
||||
if (save.explicit) {
|
||||
tag.setFrame('TXXX', {
|
||||
description: 'ITUNESADVISORY',
|
||||
value: track.explicit ? '1' : '0'
|
||||
})
|
||||
}
|
||||
if (save.replayGain) {
|
||||
tag.setFrame('TXXX', {
|
||||
description: 'REPLAYGAIN_TRACK_GAIN',
|
||||
value: track.replayGain
|
||||
})
|
||||
}
|
||||
if (save.lyrics && track.lyrics.unsync) {
|
||||
tag.setFrame('USLT', {
|
||||
description: '',
|
||||
lyrics: track.lyrics.unsync,
|
||||
language: 'XXX'
|
||||
})
|
||||
}
|
||||
|
||||
if (save.syncedLyrics && track.lyrics.syncID3.length !== 0) {
|
||||
tag.setFrame('SYLT', {
|
||||
text: track.lyrics.syncID3,
|
||||
type: 1,
|
||||
timestampFormat: 2,
|
||||
useUnicodeEncoding: true
|
||||
})
|
||||
}
|
||||
|
||||
const involvedPeople = []
|
||||
Object.keys(track.contributors).forEach(role => {
|
||||
if (['author', 'engineer', 'mixer', 'producer', 'writer'].includes(role)) {
|
||||
track.contributors[role].forEach(person => {
|
||||
involvedPeople.push([role, person])
|
||||
})
|
||||
} else if (role === 'composer' && save.composer) {
|
||||
tag.setFrame('TCOM', track.contributors.composer)
|
||||
}
|
||||
})
|
||||
if (involvedPeople.length && save.involvedPeople) tag.setFrame('IPLS', involvedPeople)
|
||||
|
||||
if (save.copyright && track.copyright) tag.setFrame('TCOP', track.copyright)
|
||||
if ((save.savePlaylistAsCompilation && track.playlist) || track.album.recordType === 'compile') { tag.setFrame('TCMP', '1') }
|
||||
|
||||
if (save.source) {
|
||||
tag.setFrame('TXXX', {
|
||||
description: 'SOURCE',
|
||||
value: 'Deezer'
|
||||
})
|
||||
tag.setFrame('TXXX', {
|
||||
description: 'SOURCEID',
|
||||
value: track.id
|
||||
})
|
||||
}
|
||||
|
||||
if (save.rating) {
|
||||
let rank = (track.rank / 10000) * 2.55
|
||||
rank = rank > 255 ? 255 : Math.round(rank)
|
||||
tag.setFrame('POPM', {
|
||||
rating: rank
|
||||
})
|
||||
}
|
||||
|
||||
if (save.cover && track.album.embeddedCoverPath) {
|
||||
const coverArrayBuffer = fs.readFileSync(track.album.embeddedCoverPath)
|
||||
if (coverArrayBuffer.length !== 0) {
|
||||
tag.setFrame('APIC', {
|
||||
type: 3,
|
||||
data: coverArrayBuffer,
|
||||
description: 'cover',
|
||||
useUnicodeEncoding: save.coverDescriptionUTF8
|
||||
})
|
||||
}
|
||||
}
|
||||
tag.addTag()
|
||||
|
||||
let taggedSongBuffer = Buffer.from(tag.arrayBuffer)
|
||||
if (taggedSongBuffer.slice(-128, -125).toString() === 'TAG') { taggedSongBuffer = taggedSongBuffer.slice(0, -128) }
|
||||
if (save.saveID3v1) { taggedSongBuffer = tagID3v1(taggedSongBuffer, track, save) }
|
||||
|
||||
fs.writeFileSync(path, taggedSongBuffer)
|
||||
}
|
||||
|
||||
function tagFLAC (path, track, save) {
|
||||
const flac = new Metaflac(path)
|
||||
flac.removeAllTags()
|
||||
|
||||
if (save.title) flac.setTag(`TITLE=${track.title}`)
|
||||
|
||||
if (save.artist && track.artists.length) {
|
||||
if (save.multiArtistSeparator === 'default') {
|
||||
track.artists.forEach(artist => {
|
||||
flac.setTag(`ARTIST=${artist}`)
|
||||
})
|
||||
} else {
|
||||
if (save.multiArtistSeparator === 'nothing') {
|
||||
flac.setTag(`ARTIST=${track.mainArtist.name}`)
|
||||
} else {
|
||||
flac.setTag(`ARTIST=${track.artistsString}`)
|
||||
}
|
||||
// Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
|
||||
// https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists
|
||||
if (save.artists) {
|
||||
track.artists.forEach(artist => {
|
||||
flac.setTag(`ARTISTS=${artist}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (save.album) flac.setTag(`ALBUM=${track.album.title}`)
|
||||
|
||||
if (save.albumArtist && track.album.artists.length) {
|
||||
if (save.singleAlbumArtist && track.album.mainArtist.save) {
|
||||
flac.setTag(`ALBUMARTIST=${track.album.mainArtist.name}`)
|
||||
} else {
|
||||
track.album.artists.forEach(artist => {
|
||||
flac.setTag(`ALBUMARTIST=${artist}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (save.trackNumber) flac.setTag(`TRACKNUMBER=${track.trackNumber}`)
|
||||
if (save.trackTotal) flac.setTag(`TRACKTOTAL=${track.album.trackTotal}`)
|
||||
if (save.discNumber) flac.setTag(`DISCNUMBER=${track.discNumber}`)
|
||||
if (save.discTotal) flac.setTag(`DISCTOTAL=${track.album.discTotal}`)
|
||||
if (save.genre) {
|
||||
track.album.genre.forEach(genre => {
|
||||
flac.setTag(`GENRE=${genre}`)
|
||||
})
|
||||
}
|
||||
|
||||
// YEAR tag is not suggested as a standard tag
|
||||
// Being YEAR already contained in DATE will only use DATE instead
|
||||
// Reference: https://www.xiph.org/vorbis/doc/v-comment.html#fieldnames
|
||||
if (save.date) flac.setTag(`DATE=${track.dateString}`)
|
||||
else if (save.year) flac.setTag(`DATE=${track.date.year}`)
|
||||
|
||||
if (save.length) flac.setTag(`LENGTH=${parseInt(track.duration) * 1000}`)
|
||||
if (save.bpm && track.bpm) flac.setTag(`BPM=${track.bpm}`)
|
||||
if (save.label) flac.setTag(`PUBLISHER=${track.album.label}`)
|
||||
if (save.isrc) flac.setTag(`ISRC=${track.ISRC}`)
|
||||
if (save.barcode) flac.setTag(`BARCODE=${track.album.barcode}`)
|
||||
if (save.explicit) flac.setTag(`ITUNESADVISORY=${track.explicit ? '1' : '0'}`)
|
||||
if (save.replayGain) flac.setTag(`REPLAYGAIN_TRACK_GAIN=${track.replayGain}`)
|
||||
if (save.lyrics && track.lyrics.unsync) flac.setTag(`LYRICS=${track.lyrics.unsync}`)
|
||||
|
||||
Object.keys(track.contributors).forEach(role => {
|
||||
if (['author', 'engineer', 'mixer', 'producer', 'writer', 'composer'].includes(role)) {
|
||||
if ((save.involvedPeople && role !== 'composer') || (save.composer && role === 'composer')) {
|
||||
track.contributors[role].forEach(person => {
|
||||
flac.setTag(`${role.toUpperCase()}=${person}`)
|
||||
})
|
||||
}
|
||||
} else if (role === 'musicpublisher' && save.involvedPeople) {
|
||||
track.contributors.musicpublisher.forEach(person => {
|
||||
flac.setTag(`ORGANIZATION=${person}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (save.copyright && track.copyright) flac.setTag(`COPYRIGHT=${track.copyright}`)
|
||||
if ((save.savePlaylistAsCompilation && track.playlist) || (track.album.recordType === 'compile')) { flac.setTag('COMPILATION=1') }
|
||||
|
||||
if (save.source) {
|
||||
flac.setTag('SOURCE=Deezer')
|
||||
flac.setTag(`SOURCEID=${track.id}`)
|
||||
}
|
||||
|
||||
if (save.rating) {
|
||||
const rank = Math.round(track.rank / 10000)
|
||||
flac.setTag(`RATING=${rank}`)
|
||||
}
|
||||
|
||||
if (save.cover && track.album.embeddedCoverPath) {
|
||||
const picture = fs.readFileSync(track.album.embeddedCoverPath)
|
||||
if (picture.length !== 0) flac.importPicture(picture)
|
||||
}
|
||||
|
||||
flac.save()
|
||||
}
|
||||
|
||||
const id3v1Genres = ['Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop', 'Rhythm and Blues', 'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', 'Jazz & Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', 'Game', 'Sound clip', 'Gospel', 'Noise', 'Alternative Rock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle music', 'Native US', 'Cabaret', 'New Wave', 'Psychedelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock ’n’ Roll', 'Hard Rock', 'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebop', 'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'A cappella', 'Euro-House', 'Dance Hall', 'Goa music', 'Drum & Bass', 'Club-House', 'Hardcore Techno', 'Terror', 'Indie', 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', 'Heavy Metal', 'Black Metal', 'Crossover', 'Contemporary Christian', 'Christian Rock', 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'Jpop', 'Synthpop', 'Abstract', 'Art Rock', 'Baroque', 'Bhangra', 'Big beat', 'Breakbeat', 'Chillout', 'Downtempo', 'Dub', 'EBM', 'Eclectic', 'Electro', 'Electroclash', 'Emo', 'Experimental', 'Garage', 'Global', 'IDM', 'Illbient', 'Industro-Goth', 'Jam Band', 'Krautrock', 'Leftfield', 'Lounge', 'Math Rock', 'New Romantic', 'Nu-Breakz', 'Post-Punk', 'Post-Rock', 'Psytrance', 'Shoegaze', 'Space Rock', 'Trop Rock', 'World Music', 'Neoclassical', 'Audiobook', 'Audio Theatre', 'Neue Deutsche Welle', 'Podcast', 'Indie-Rock', 'G-Funk', 'Dubstep', 'Garage Rock', 'Psybient']
|
||||
|
||||
// Filters only Extended Ascii characters
|
||||
function extAsciiFilter (string) {
|
||||
let output = ''
|
||||
string.split('').forEach((x) => {
|
||||
if (x.charCodeAt(0) > 255) { output += '?' } else { output += x }
|
||||
})
|
||||
return output
|
||||
}
|
||||
|
||||
function tagID3v1 (taggedSongBuffer, track, save) {
|
||||
const tagBuffer = Buffer.alloc(128)
|
||||
|
||||
tagBuffer.write('TAG', 0) // Header
|
||||
if (save.title) {
|
||||
const trimmedTitle = extAsciiFilter(track.title.substring(0, 30))
|
||||
tagBuffer.write(trimmedTitle, 3)
|
||||
}
|
||||
if (save.artist) {
|
||||
let selectedArtist
|
||||
if (track.artistsString) selectedArtist = track.artistsString
|
||||
else selectedArtist = track.mainArtist.name
|
||||
|
||||
const trimmedArtist = extAsciiFilter(selectedArtist.substring(0, 30))
|
||||
tagBuffer.write(trimmedArtist, 33)
|
||||
}
|
||||
if (save.album) {
|
||||
const trimmedAlbum = extAsciiFilter(track.album.title.substring(0, 30))
|
||||
tagBuffer.write(trimmedAlbum, 63)
|
||||
}
|
||||
if (save.year) {
|
||||
const trimmedYear = track.date.year.substring(0, 4)
|
||||
tagBuffer.write(trimmedYear, 93)
|
||||
}
|
||||
if (save.trackNumber) {
|
||||
if (track.trackNumber <= 65535) {
|
||||
if (track.trackNumber > 255) {
|
||||
tagBuffer.writeUInt8(parseInt(track.trackNumber >> 8), 125)
|
||||
tagBuffer.writeUInt8(parseInt(track.trackNumber & 255), 126)
|
||||
} else {
|
||||
tagBuffer.writeUInt8(parseInt(track.trackNumber), 126)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (save.genre) {
|
||||
const selectedGenre = track.album.genre[0]
|
||||
if (id3v1Genres.includes(selectedGenre)) tagBuffer.writeUInt8(id3v1Genres.indexOf(selectedGenre), 127)
|
||||
else tagBuffer.writeUInt8(255, 127)
|
||||
} else {
|
||||
tagBuffer.writeUInt8(255, 127)
|
||||
}
|
||||
|
||||
// Save tags
|
||||
const buffer = new ArrayBuffer(taggedSongBuffer.byteLength + 128)
|
||||
const bufferWriter = new Uint8Array(buffer)
|
||||
bufferWriter.set(new Uint8Array(taggedSongBuffer), 0)
|
||||
bufferWriter.set(new Uint8Array(tagBuffer), taggedSongBuffer.byteLength)
|
||||
return Buffer.from(buffer)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
tagID3,
|
||||
tagFLAC,
|
||||
tagID3v1
|
||||
}
|
||||
151
deemix/deemix/types/Album.js
Normal file
151
deemix/deemix/types/Album.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const { removeDuplicateArtists, removeFeatures } = require('../utils/index.js')
|
||||
const { Artist } = require('./Artist.js')
|
||||
const { Date } = require('./Date.js')
|
||||
const { Picture } = require('./Picture.js')
|
||||
const { VARIOUS_ARTISTS } = require('./index.js')
|
||||
|
||||
class Album {
|
||||
constructor (alb_id = '0', title = '', pic_md5 = '') {
|
||||
this.id = alb_id
|
||||
this.title = title
|
||||
this.pic = new Picture(pic_md5, 'cover')
|
||||
this.artist = { Main: [] }
|
||||
this.artists = []
|
||||
this.mainArtist = null
|
||||
this.date = new Date()
|
||||
this.dateString = ''
|
||||
this.trackTotal = '0'
|
||||
this.discTotal = '0'
|
||||
this.embeddedCoverPath = ''
|
||||
this.embeddedCoverURL = ''
|
||||
this.explicit = false
|
||||
this.genre = []
|
||||
this.barcode = 'Unknown'
|
||||
this.label = 'Unknown'
|
||||
this.copyright = ''
|
||||
this.recordType = 'album'
|
||||
this.bitrate = 0
|
||||
this.rootArtist = null
|
||||
this.variousArtists = null
|
||||
|
||||
this.playlistId = null
|
||||
this.owner = null
|
||||
this.isPlaylist = false
|
||||
}
|
||||
|
||||
parseAlbum (albumAPI) {
|
||||
this.title = albumAPI.title
|
||||
|
||||
// Getting artist image ID
|
||||
// ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
|
||||
let art_pic = albumAPI.artist.picture_small
|
||||
if (art_pic) art_pic = art_pic.slice(art_pic.indexOf('artist/') + 7, -24)
|
||||
else art_pic = ''
|
||||
this.mainArtist = new Artist(
|
||||
albumAPI.artist.id,
|
||||
albumAPI.artist.name,
|
||||
'Main',
|
||||
art_pic
|
||||
)
|
||||
if (albumAPI.root_artist) {
|
||||
let art_pic = albumAPI.root_artist.picture_small
|
||||
art_pic = art_pic.slice(art_pic.indexOf('artist/') + 7, -24)
|
||||
this.rootArtist = new Artist(
|
||||
albumAPI.root_artist.id,
|
||||
albumAPI.root_artist.name,
|
||||
'Root',
|
||||
art_pic
|
||||
)
|
||||
}
|
||||
|
||||
albumAPI.contributors.forEach(artist => {
|
||||
const isVariousArtists = String(artist.id) === VARIOUS_ARTISTS
|
||||
const isMainArtist = artist.role === 'Main'
|
||||
|
||||
if (isVariousArtists) {
|
||||
this.variousArtists = new Artist(
|
||||
artist.id,
|
||||
artist.name,
|
||||
artist.role
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.artists.includes(artist.name)) {
|
||||
this.artists.push(artist.name)
|
||||
}
|
||||
|
||||
if (isMainArtist || (!this.artist.Main.includes(artist.name) && !isMainArtist)) {
|
||||
if (!this.artist[artist.role]) this.artist[artist.role] = []
|
||||
this.artist[artist.role].push(artist.name)
|
||||
}
|
||||
})
|
||||
|
||||
this.trackTotal = albumAPI.nb_tracks
|
||||
this.recordType = albumAPI.record_type || this.recordType
|
||||
|
||||
this.barcode = albumAPI.upc || this.barcode
|
||||
this.label = albumAPI.label || this.label
|
||||
this.explicit = Boolean(albumAPI.explicit_lyrics || false)
|
||||
let release_date = albumAPI.release_date
|
||||
if (albumAPI.original_release_date) release_date = albumAPI.original_release_date
|
||||
if (release_date) {
|
||||
this.date.year = release_date.slice(0, 4)
|
||||
this.date.month = release_date.slice(5, 7)
|
||||
this.date.day = release_date.slice(8, 10)
|
||||
this.date.fixDayMonth()
|
||||
}
|
||||
|
||||
this.discTotal = albumAPI.nb_disk || '1'
|
||||
this.copyright = albumAPI.copyright || ''
|
||||
|
||||
if (this.pic.md5 === '') {
|
||||
if (albumAPI.md5_image) {
|
||||
this.pic.md5 = albumAPI.md5_image
|
||||
} else if (albumAPI.cover_small) {
|
||||
// Getting album cover MD5
|
||||
// ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg
|
||||
const alb_pic = albumAPI.cover_small
|
||||
this.pic.md5 = alb_pic.slice(alb_pic.indexOf('cover/') + 6, -24)
|
||||
}
|
||||
}
|
||||
|
||||
if (albumAPI.genres && albumAPI.genres.data && albumAPI.genres.data.length > 0) {
|
||||
albumAPI.genres.data.forEach(genre => {
|
||||
this.genre.push(genre.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
makePlaylistCompilation (playlist) {
|
||||
this.variousArtists = playlist.variousArtists
|
||||
this.mainArtist = playlist.mainArtist
|
||||
this.title = playlist.title
|
||||
this.rootArtist = playlist.rootArtist
|
||||
this.artist = playlist.artist
|
||||
this.artists = playlist.artists
|
||||
this.trackTotal = playlist.trackTotal
|
||||
this.recordType = playlist.recordType
|
||||
this.barcode = playlist.barcode
|
||||
this.label = playlist.label
|
||||
this.explicit = playlist.explicit
|
||||
this.date = playlist.date
|
||||
this.discTotal = playlist.discTotal
|
||||
this.playlistID = playlist.playlistID
|
||||
this.owner = playlist.owner
|
||||
this.pic = playlist.pic
|
||||
this.isPlaylist = true
|
||||
}
|
||||
|
||||
removeDuplicateArtists () {
|
||||
[this.artist, this.artists] = removeDuplicateArtists(this.artist, this.artists)
|
||||
}
|
||||
|
||||
getCleanTitle () {
|
||||
return removeFeatures(this.title)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Album
|
||||
}
|
||||
20
deemix/deemix/types/Artist.js
Normal file
20
deemix/deemix/types/Artist.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { Picture } = require('./Picture.js')
|
||||
const { VARIOUS_ARTISTS } = require('./index.js')
|
||||
|
||||
class Artist {
|
||||
constructor (art_id = '0', name = '', role = '', pic_md5 = '') {
|
||||
this.id = String(art_id)
|
||||
this.name = name
|
||||
this.pic = new Picture(pic_md5, 'artist')
|
||||
this.role = role
|
||||
this.save = true
|
||||
}
|
||||
|
||||
isVariousArtists () {
|
||||
return this.id === VARIOUS_ARTISTS
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Artist
|
||||
}
|
||||
27
deemix/deemix/types/Date.js
Normal file
27
deemix/deemix/types/Date.js
Normal file
@@ -0,0 +1,27 @@
|
||||
class Date {
|
||||
constructor (day = '00', month = '00', year = 'XXXX') {
|
||||
this.day = day
|
||||
this.month = month
|
||||
this.year = year
|
||||
this.fixDayMonth()
|
||||
}
|
||||
|
||||
fixDayMonth () {
|
||||
if (parseInt(this.month) > 12) {
|
||||
const monthTemp = this.month
|
||||
this.month = this.day
|
||||
this.day = monthTemp
|
||||
}
|
||||
}
|
||||
|
||||
format (template) {
|
||||
template = template.replaceAll(/D+/g, this.day)
|
||||
template = template.replaceAll(/M+/g, this.month)
|
||||
template = template.replaceAll(/Y+/g, this.year)
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Date
|
||||
}
|
||||
158
deemix/deemix/types/DownloadObjects.js
Normal file
158
deemix/deemix/types/DownloadObjects.js
Normal file
@@ -0,0 +1,158 @@
|
||||
class IDownloadObject {
|
||||
constructor (obj) {
|
||||
this.type = obj.type
|
||||
this.id = obj.id
|
||||
this.bitrate = obj.bitrate
|
||||
this.title = obj.title
|
||||
this.artist = obj.artist
|
||||
this.cover = obj.cover
|
||||
this.explicit = obj.explicit || false
|
||||
this.size = obj.size
|
||||
this.downloaded = obj.downloaded || 0
|
||||
this.failed = obj.failed || 0
|
||||
this.progress = obj.progress || 0
|
||||
this.errors = obj.errors || []
|
||||
this.files = obj.files || []
|
||||
this.extrasPath = obj.extrasPath || ''
|
||||
this.progressNext = 0
|
||||
this.uuid = `${this.type}_${this.id}_${this.bitrate}`
|
||||
this.isCanceled = false
|
||||
this.__type__ = null
|
||||
}
|
||||
|
||||
toDict () {
|
||||
return {
|
||||
type: this.type,
|
||||
id: this.id,
|
||||
bitrate: this.bitrate,
|
||||
uuid: this.uuid,
|
||||
title: this.title,
|
||||
artist: this.artist,
|
||||
cover: this.cover,
|
||||
explicit: this.explicit,
|
||||
size: this.size,
|
||||
downloaded: this.downloaded,
|
||||
failed: this.failed,
|
||||
progress: this.progress,
|
||||
errors: this.errors,
|
||||
files: this.files,
|
||||
extrasPath: this.extrasPath,
|
||||
__type__: this.__type__
|
||||
}
|
||||
}
|
||||
|
||||
getResettedDict () {
|
||||
const item = this.toDict()
|
||||
item.downloaded = 0
|
||||
item.failed = 0
|
||||
item.progress = 0
|
||||
item.errors = []
|
||||
item.files = []
|
||||
return item
|
||||
}
|
||||
|
||||
getSlimmedDict () {
|
||||
const light = this.toDict()
|
||||
const propertiesToDelete = ['single', 'collection', 'plugin', 'conversion_data']
|
||||
propertiesToDelete.forEach((property) => {
|
||||
if (Object.keys(light).includes(property)) {
|
||||
delete light[property]
|
||||
}
|
||||
})
|
||||
return light
|
||||
}
|
||||
|
||||
getEssentialDict () {
|
||||
return {
|
||||
type: this.type,
|
||||
id: this.id,
|
||||
bitrate: this.bitrate,
|
||||
uuid: this.uuid,
|
||||
title: this.title,
|
||||
artist: this.artist,
|
||||
cover: this.cover,
|
||||
explicit: this.explicit,
|
||||
size: this.size,
|
||||
extrasPath: this.extrasPath
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress (listener) {
|
||||
if (Math.floor(this.progressNext) !== this.progress && Math.floor(this.progressNext) % 2 === 0) {
|
||||
this.progress = Math.floor(this.progressNext)
|
||||
if (listener) listener.send('updateQueue', { uuid: this.uuid, progress: this.progress })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Single extends IDownloadObject {
|
||||
constructor (obj) {
|
||||
super(obj)
|
||||
this.size = 1
|
||||
this.single = obj.single
|
||||
this.__type__ = 'Single'
|
||||
}
|
||||
|
||||
toDict () {
|
||||
const item = super.toDict()
|
||||
item.single = this.single
|
||||
return item
|
||||
}
|
||||
|
||||
completeTrackProgress (listener) {
|
||||
this.progressNext = 100
|
||||
this.updateProgress(listener)
|
||||
}
|
||||
|
||||
removeTrackProgress (listener) {
|
||||
this.progressNext = 0
|
||||
this.updateProgress(listener)
|
||||
}
|
||||
}
|
||||
|
||||
class Collection extends IDownloadObject {
|
||||
constructor (obj) {
|
||||
super(obj)
|
||||
this.collection = obj.collection
|
||||
this.__type__ = 'Collection'
|
||||
}
|
||||
|
||||
toDict () {
|
||||
const item = super.toDict()
|
||||
item.collection = this.collection
|
||||
return item
|
||||
}
|
||||
|
||||
completeTrackProgress (listener) {
|
||||
this.progressNext += (1 / this.size) * 100
|
||||
this.updateProgress(listener)
|
||||
}
|
||||
|
||||
removeTrackProgress (listener) {
|
||||
this.progressNext -= (1 / this.size) * 100
|
||||
this.updateProgress(listener)
|
||||
}
|
||||
}
|
||||
|
||||
class Convertable extends Collection {
|
||||
constructor (obj) {
|
||||
super(obj)
|
||||
this.plugin = obj.plugin
|
||||
this.conversion_data = obj.conversion_data
|
||||
this.__type__ = 'Convertable'
|
||||
}
|
||||
|
||||
toDict () {
|
||||
const item = super.toDict()
|
||||
item.plugin = this.plugin
|
||||
item.conversion_data = this.conversion_data
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
IDownloadObject,
|
||||
Single,
|
||||
Collection,
|
||||
Convertable
|
||||
}
|
||||
36
deemix/deemix/types/Lyrics.js
Normal file
36
deemix/deemix/types/Lyrics.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const { decode } = require('html-entities')
|
||||
|
||||
class Lyrics {
|
||||
constructor (lyr_id = '0') {
|
||||
this.id = lyr_id
|
||||
this.sync = ''
|
||||
this.unsync = ''
|
||||
this.syncID3 = []
|
||||
}
|
||||
|
||||
parseLyrics (lyricsAPI) {
|
||||
this.unsync = lyricsAPI.LYRICS_TEXT || ''
|
||||
if (lyricsAPI.LYRICS_SYNC_JSON) {
|
||||
const syncLyricsJson = lyricsAPI.LYRICS_SYNC_JSON
|
||||
let timestamp = ''
|
||||
let milliseconds = 0
|
||||
for (let line = 0; line < syncLyricsJson.length; line++) {
|
||||
const currentLine = decode(syncLyricsJson[line].line)
|
||||
if (currentLine !== '') {
|
||||
timestamp = syncLyricsJson[line].lrc_timestamp
|
||||
milliseconds = parseInt(syncLyricsJson[line].milliseconds)
|
||||
this.syncID3.push([currentLine, milliseconds])
|
||||
} else {
|
||||
let notEmptyLine = line + 1
|
||||
while (syncLyricsJson[notEmptyLine].line === '') notEmptyLine += 1
|
||||
timestamp = syncLyricsJson[notEmptyLine].lrc_timestamp
|
||||
}
|
||||
this.sync += timestamp + currentLine + '\r\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Lyrics
|
||||
}
|
||||
37
deemix/deemix/types/Picture.js
Normal file
37
deemix/deemix/types/Picture.js
Normal file
@@ -0,0 +1,37 @@
|
||||
class Picture {
|
||||
constructor (md5 = '', pic_type = '') {
|
||||
this.md5 = md5
|
||||
this.type = pic_type
|
||||
}
|
||||
|
||||
getURL (size, format) {
|
||||
const url = `https://e-cdns-images.dzcdn.net/images/${this.type}/${this.md5}/${size}x${size}`
|
||||
|
||||
if (format.startsWith('jpg')) {
|
||||
let quality = 80
|
||||
if (format.includes('-')) quality = parseInt(format.substr(4))
|
||||
format = 'jpg'
|
||||
return url + `-000000-${quality}-0-0.jpg`
|
||||
}
|
||||
if (format === 'png') {
|
||||
return url + '-none-100-0-0.png'
|
||||
}
|
||||
|
||||
return url + '.jpg'
|
||||
}
|
||||
}
|
||||
|
||||
class StaticPicture {
|
||||
constructor (url) {
|
||||
this.staticURL = url
|
||||
}
|
||||
|
||||
getURL () {
|
||||
return this.staticURL
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Picture,
|
||||
StaticPicture
|
||||
}
|
||||
53
deemix/deemix/types/Playlist.js
Normal file
53
deemix/deemix/types/Playlist.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { Artist } = require('./Artist.js')
|
||||
const { Date } = require('./Date.js')
|
||||
const { Picture, StaticPicture } = require('./Picture.js')
|
||||
|
||||
class Playlist {
|
||||
constructor (playlistAPI) {
|
||||
this.id = `pl_${playlistAPI.id}`
|
||||
this.title = playlistAPI.title
|
||||
this.artist = { Main: [] }
|
||||
this.artists = []
|
||||
this.trackTotal = playlistAPI.nb_tracks
|
||||
this.recordType = 'compile'
|
||||
this.barcode = ''
|
||||
this.label = ''
|
||||
this.explicit = playlistAPI.explicit
|
||||
this.genre = ['Compilation']
|
||||
|
||||
const year = playlistAPI.creation_date.slice(0, 4)
|
||||
const month = playlistAPI.creation_date.slice(5, 7)
|
||||
const day = playlistAPI.creation_date.slice(8, 10)
|
||||
this.date = new Date(day, month, year)
|
||||
|
||||
this.discTotal = '1'
|
||||
this.playlistID = playlistAPI.id
|
||||
this.owner = playlistAPI.creator
|
||||
|
||||
if (playlistAPI.picture_small.includes('dzcdn.net')) {
|
||||
const url = playlistAPI.picture_small
|
||||
let picType = url.slice(url.indexOf('images/') + 7)
|
||||
picType = picType.slice(0, picType.indexOf('/'))
|
||||
const md5 = url.slice(url.indexOf(picType + '/') + picType.length + 1, -24)
|
||||
this.pic = new Picture(md5, picType)
|
||||
} else {
|
||||
this.pic = new StaticPicture(playlistAPI.picture_xl)
|
||||
}
|
||||
|
||||
if (playlistAPI.various_artist) {
|
||||
let pic_md5 = playlistAPI.various_artist.picture_small
|
||||
pic_md5 = pic_md5.slice(pic_md5.indexOf('artist/') + 7, -24)
|
||||
this.variousArtists = new Artist(
|
||||
playlistAPI.various_artist.id,
|
||||
playlistAPI.various_artist.name,
|
||||
playlistAPI.various_artist.role,
|
||||
pic_md5
|
||||
)
|
||||
this.mainArtist = this.variousArtists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Playlist
|
||||
}
|
||||
354
deemix/deemix/types/Track.js
Normal file
354
deemix/deemix/types/Track.js
Normal file
@@ -0,0 +1,354 @@
|
||||
const { map_track, map_album } = require('deezer-js').utils
|
||||
const { Artist } = require('./Artist.js')
|
||||
const { Album } = require('./Album.js')
|
||||
const { Playlist } = require('./Playlist.js')
|
||||
const { Picture } = require('./Picture.js')
|
||||
const { Lyrics } = require('./Lyrics.js')
|
||||
const { Date: dzDate } = require('./Date.js')
|
||||
const { VARIOUS_ARTISTS } = require('./index.js')
|
||||
const { changeCase } = require('../utils/index.js')
|
||||
const { FeaturesOption } = require('../settings.js')
|
||||
const { NoDataToParse, AlbumDoesntExists } = require('../errors.js')
|
||||
|
||||
const {
|
||||
generateReplayGainString,
|
||||
removeDuplicateArtists,
|
||||
removeFeatures,
|
||||
andCommaConcat
|
||||
} = require('../utils/index.js')
|
||||
|
||||
class Track {
|
||||
constructor () {
|
||||
this.id = '0'
|
||||
this.title = ''
|
||||
this.MD5 = ''
|
||||
this.mediaVersion = ''
|
||||
this.trackToken = ''
|
||||
this.trackTokenExpiration = 0
|
||||
this.duration = 0
|
||||
this.fallbackID = '0'
|
||||
this.albumsFallback = []
|
||||
this.filesizes = {}
|
||||
this.local = false
|
||||
this.mainArtist = null
|
||||
this.artist = { Main: [] }
|
||||
this.artists = []
|
||||
this.album = null
|
||||
this.trackNumber = '0'
|
||||
this.discNumber = '0'
|
||||
this.date = new dzDate()
|
||||
this.lyrics = null
|
||||
this.bpm = 0
|
||||
this.contributors = {}
|
||||
this.copyright = ''
|
||||
this.explicit = false
|
||||
this.ISRC = ''
|
||||
this.replayGain = ''
|
||||
this.playlist = null
|
||||
this.position = null
|
||||
this.searched = false
|
||||
this.bitrate = 0
|
||||
this.dateString = ''
|
||||
this.artistsString = ''
|
||||
this.mainArtistsString = ''
|
||||
this.featArtistsString = ''
|
||||
this.fullArtistsString = ''
|
||||
this.urls = {}
|
||||
}
|
||||
|
||||
parseEssentialData (trackAPI) {
|
||||
this.id = String(trackAPI.id)
|
||||
this.duration = trackAPI.duration
|
||||
this.trackToken = trackAPI.track_token
|
||||
this.trackTokenExpiration = trackAPI.track_token_expire
|
||||
this.MD5 = trackAPI.md5_origin
|
||||
this.mediaVersion = trackAPI.media_version
|
||||
this.filesizes = trackAPI.filesizes
|
||||
this.fallbackID = '0'
|
||||
if (trackAPI.fallback_id) this.fallbackID = trackAPI.fallback_id
|
||||
this.local = parseInt(this.id) < 0
|
||||
this.urls = {}
|
||||
}
|
||||
|
||||
async parseData (dz, id, trackAPI, albumAPI, playlistAPI) {
|
||||
/* if (id && (!trackAPI || trackAPI && !trackAPI.track_token)) { */ // mickey
|
||||
if (id && (!trackAPI || (trackAPI && !trackAPI.md5_origin))) {
|
||||
let trackAPI_new = await dz.gw.get_track_with_fallback(id)
|
||||
trackAPI_new = map_track(trackAPI_new)
|
||||
if (!trackAPI) trackAPI = {}
|
||||
/* trackAPI = {...trackAPI_new, ...trackAPI} */ // mickey
|
||||
trackAPI = { ...trackAPI, ...trackAPI_new }
|
||||
} else if (!trackAPI) { throw new NoDataToParse() }
|
||||
|
||||
this.parseEssentialData(trackAPI)
|
||||
|
||||
// only public api has bpm
|
||||
if (!trackAPI.bpm && !this.local) {
|
||||
try {
|
||||
const trackAPI_new = await dz.api.get_track(trackAPI.id)
|
||||
trackAPI_new.release_date = trackAPI.release_date
|
||||
trackAPI = { ...trackAPI, ...trackAPI_new }
|
||||
} catch { /* empty */ }
|
||||
}
|
||||
|
||||
if (this.local) {
|
||||
this.parseLocalTrackData(trackAPI)
|
||||
} else {
|
||||
this.parseTrack(trackAPI)
|
||||
|
||||
// Get Lyrics Data
|
||||
if (!trackAPI.lyrics && this.lyrics.id !== '0') {
|
||||
try { trackAPI.lyrics = await dz.gw.get_track_lyrics(this.id) } catch { this.lyrics.id = '0' }
|
||||
}
|
||||
if (this.lyrics.id !== '0') { this.lyrics.parseLyrics(trackAPI.lyrics) }
|
||||
|
||||
// Parse Album Data
|
||||
this.album = new Album(trackAPI.album.id, trackAPI.album.title, trackAPI.album.md5_origin || '')
|
||||
|
||||
// Get album Data
|
||||
if (!albumAPI) {
|
||||
try { albumAPI = await dz.api.get_album(this.album.id) } catch { albumAPI = null }
|
||||
}
|
||||
|
||||
// Get album_gw Data
|
||||
// Only gw has disk number
|
||||
if (!albumAPI || (albumAPI && !albumAPI.nb_disk)) {
|
||||
let albumAPI_gw
|
||||
try {
|
||||
albumAPI_gw = await dz.gw.get_album(this.album.id)
|
||||
albumAPI_gw = map_album(albumAPI_gw)
|
||||
} catch { albumAPI_gw = {} }
|
||||
if (!albumAPI) albumAPI = {}
|
||||
albumAPI = { ...albumAPI_gw, ...albumAPI }
|
||||
}
|
||||
|
||||
if (!albumAPI) throw new AlbumDoesntExists()
|
||||
this.album.parseAlbum(albumAPI)
|
||||
|
||||
// albumAPI_gw doesn't contain the artist cover
|
||||
// Getting artist image ID
|
||||
// ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
|
||||
if (!this.album.mainArtist.pic.md5) {
|
||||
const artistAPI = await dz.api.get_artist(this.album.mainArtist.id)
|
||||
this.album.mainArtist.pic.md5 = artistAPI.picture_small.slice(artistAPI.picture_small.search('artist/') + 7, -24)
|
||||
}
|
||||
|
||||
// Fill missing data
|
||||
if (this.album.date && !this.date) this.date = this.album.date
|
||||
if (trackAPI.genres) {
|
||||
trackAPI.genres.forEach((genre) => {
|
||||
if (!this.album.genre.includes(genre)) this.album.genre.push(genre)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unwanted charaters in track name
|
||||
// Example: track/127793
|
||||
this.title = this.title.replace(/\s\s+/g, ' ')
|
||||
|
||||
// Make sure there is at least one artist
|
||||
if (!this.artist.Main.length) {
|
||||
this.artist.Main = [this.mainArtist.name]
|
||||
}
|
||||
this.position = trackAPI.position
|
||||
|
||||
if (playlistAPI) { this.playlist = new Playlist(playlistAPI) }
|
||||
|
||||
this.generateMainFeatStrings()
|
||||
return this
|
||||
}
|
||||
|
||||
parseLocalTrackData (trackAPI) {
|
||||
// Local tracks has only the trackAPI_gw page and
|
||||
// contains only the tags provided by the file
|
||||
this.title = trackAPI.title
|
||||
this.album = new Album(trackAPI.album.title)
|
||||
this.album.pic = new Picture(
|
||||
trackAPI.md5_image || '',
|
||||
'cover'
|
||||
)
|
||||
this.mainArtist = new Artist('0', trackAPI.artist.name, 'Main')
|
||||
this.artists = [trackAPI.artist.name]
|
||||
this.artist = {
|
||||
Main: [trackAPI.artist.name]
|
||||
}
|
||||
this.album.artist = this.artist
|
||||
this.album.artists = this.artists
|
||||
this.album.date = this.date
|
||||
this.album.mainArtist = this.mainArtist
|
||||
}
|
||||
|
||||
parseTrack (trackAPI) {
|
||||
this.title = trackAPI.title
|
||||
|
||||
this.discNumber = trackAPI.disk_number
|
||||
this.explicit = trackAPI.explicit_lyrics
|
||||
this.copyright = trackAPI.copyright
|
||||
if (trackAPI.gain) this.replayGain = generateReplayGainString(trackAPI.gain)
|
||||
this.ISRC = trackAPI.isrc
|
||||
this.trackNumber = trackAPI.track_position
|
||||
this.contributors = trackAPI.song_contributors
|
||||
this.rank = trackAPI.rank
|
||||
this.bpm = trackAPI.bpm
|
||||
|
||||
this.lyrics = new Lyrics(trackAPI.lyrics_id || '0')
|
||||
|
||||
this.mainArtist = new Artist(
|
||||
trackAPI.artist.id,
|
||||
trackAPI.artist.name,
|
||||
'Main',
|
||||
trackAPI.artist.md5_image
|
||||
)
|
||||
|
||||
if (trackAPI.physical_release_date) {
|
||||
this.date.day = trackAPI.physical_release_date.slice(8, 10)
|
||||
this.date.month = trackAPI.physical_release_date.slice(5, 7)
|
||||
this.date.year = trackAPI.physical_release_date.slice(0, 4)
|
||||
this.date.fixDayMonth()
|
||||
}
|
||||
|
||||
trackAPI.contributors.forEach(artist => {
|
||||
const isVariousArtists = String(artist.id) === VARIOUS_ARTISTS
|
||||
const isMainArtist = artist.role === 'Main'
|
||||
|
||||
if (trackAPI.contributors.length > 1 && isVariousArtists) return
|
||||
|
||||
if (!this.artists.includes(artist.name)) { this.artists.push(artist.name) }
|
||||
|
||||
if (isMainArtist || (!this.artist.Main.includes(artist.name) && !isMainArtist)) {
|
||||
if (!this.artist[artist.role]) { this.artist[artist.role] = [] }
|
||||
this.artist[artist.role].push(artist.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (trackAPI.alternative_albums) {
|
||||
trackAPI.alternative_albums.data.forEach(album => {
|
||||
if (album.RIGHTS.STREAM_ADS_AVAILABLE || album.RIGHTS.STREAM_SUB_AVAILABLE) { this.albumsFallback.push(album.ALB_ID) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
removeDuplicateArtists () {
|
||||
[this.artist, this.artists] = removeDuplicateArtists(this.artist, this.artists)
|
||||
}
|
||||
|
||||
getCleanTitle () {
|
||||
return removeFeatures(this.title)
|
||||
}
|
||||
|
||||
getFeatTitle () {
|
||||
if (this.featArtistsString && !this.title.toLowerCase().includes('feat.')) {
|
||||
return `${this.title} (${this.featArtistsString})`
|
||||
}
|
||||
return this.title
|
||||
}
|
||||
|
||||
generateMainFeatStrings () {
|
||||
this.mainArtistsString = andCommaConcat(this.artist.Main)
|
||||
this.fullArtistsString = `${this.mainArtistsString}`
|
||||
this.featArtistsString = ''
|
||||
if (this.artist.Featured) {
|
||||
this.featArtistsString = `feat. ${andCommaConcat(this.artist.Featured)}`
|
||||
this.fullArtistsString += ` ${this.featArtistsString}`
|
||||
}
|
||||
}
|
||||
|
||||
async checkAndRenewTrackToken (dz) {
|
||||
const now = new Date()
|
||||
const expiration = new Date(this.trackTokenExpiration * 1000)
|
||||
if (now > expiration) {
|
||||
const newTrack = await dz.gw.get_track_with_fallback(this.id)
|
||||
this.trackToken = newTrack.TRACK_TOKEN
|
||||
this.trackTokenExpiration = newTrack.TRACK_TOKEN_EXPIRE
|
||||
}
|
||||
}
|
||||
|
||||
applySettings (settings) {
|
||||
// Check if should save the playlist as a compilation
|
||||
if (settings.tags.savePlaylistAsCompilation && this.playlist) {
|
||||
this.trackNumber = this.position
|
||||
this.discNumber = '1'
|
||||
this.album.makePlaylistCompilation(this.playlist)
|
||||
} else {
|
||||
if (this.album.date) this.date = this.album.date
|
||||
}
|
||||
this.dateString = this.date.format(settings.dateFormat)
|
||||
this.album.dateString = this.album.date.format(settings.dateFormat)
|
||||
if (this.playlist) this.playlist.dateString = this.playlist.date.format(settings.dateFormat)
|
||||
|
||||
// Check various artist option
|
||||
if (settings.albumVariousArtists && this.album.variousArtists) {
|
||||
const artist = this.album.variousArtists
|
||||
const isMainArtist = artist.role === 'Main'
|
||||
|
||||
if (!this.album.artists.includes(artist.name)) { this.album.artists.push(artist.name) }
|
||||
|
||||
if (isMainArtist || (!this.album.artist.Main.includes(artist.name) && !isMainArtist)) {
|
||||
if (!this.album.artist[artist.role]) { this.album.artist[artist.role] = [] }
|
||||
this.album.artist[artist.role].push(artist.name)
|
||||
}
|
||||
}
|
||||
this.album.mainArtist.save = (!this.album.mainArtist.isVariousArtists() || (settings.albumVariousArtists && this.album.mainArtist.isVariousArtists()))
|
||||
|
||||
// Check removeDuplicateArtists
|
||||
if (settings.removeDuplicateArtists) {
|
||||
this.removeDuplicateArtists()
|
||||
this.generateMainFeatStrings()
|
||||
}
|
||||
|
||||
// Check if user wants the feat in the title
|
||||
if (settings.featuredToTitle === FeaturesOption.REMOVE_TITLE) {
|
||||
this.title = this.getCleanTitle()
|
||||
} else if (settings.featuredToTitle === FeaturesOption.MOVE_TITLE) {
|
||||
this.title = this.getFeatTitle()
|
||||
} else if (settings.featuredToTitle === FeaturesOption.REMOVE_TITLE_ALBUM) {
|
||||
this.title = this.getCleanTitle()
|
||||
this.album.title = this.album.getCleanTitle()
|
||||
}
|
||||
|
||||
// Remove (Album Version) from tracks that have that
|
||||
if (settings.removeAlbumVersion && this.title.includes('Album Version')) {
|
||||
this.title = this.title.replace(/ ?\(Album Version\)/g, '').trim()
|
||||
}
|
||||
|
||||
// Change title and artist casing if needed
|
||||
if (settings.titleCasing !== 'nothing') {
|
||||
this.title = changeCase(this.title, settings.titleCasing)
|
||||
}
|
||||
if (settings.artistCasing !== 'nothing') {
|
||||
this.mainArtist.name = changeCase(this.mainArtist.name, settings.artistCasing)
|
||||
this.artists.forEach((artist, i) => {
|
||||
this.artists[i] = changeCase(artist, settings.artistCasing)
|
||||
})
|
||||
Object.keys(this.artist).forEach((art_type) => {
|
||||
this.artist[art_type].forEach((artist, i) => {
|
||||
this.artist[art_type][i] = changeCase(artist, settings.artistCasing)
|
||||
})
|
||||
})
|
||||
this.generateMainFeatStrings()
|
||||
}
|
||||
|
||||
// Generate artist tag
|
||||
if (settings.tags.multiArtistSeparator === 'default') {
|
||||
if (settings.featuredToTitle === FeaturesOption.MOVE_TITLE) {
|
||||
this.artistsString = this.artist.Main.join(', ')
|
||||
} else {
|
||||
this.artistString = this.artists.join(', ')
|
||||
}
|
||||
} else if (settings.tags.multiArtistSeparator === 'andFeat') {
|
||||
this.artistsString = this.mainArtistsString
|
||||
if (this.featArtistsString && settings.featuredToTitle !== FeaturesOption.MOVE_TITLE) { this.artistsString += ` ${this.featArtistsString}` }
|
||||
} else {
|
||||
const separator = settings.tags.multiArtistSeparator
|
||||
if (settings.featuredToTitle === FeaturesOption.MOVE_TITLE) {
|
||||
this.artistsString = this.artist.Main.join(separator)
|
||||
} else {
|
||||
this.artistsString = this.artists.join(separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Track
|
||||
}
|
||||
5
deemix/deemix/types/index.js
Normal file
5
deemix/deemix/types/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const VARIOUS_ARTISTS = '5080'
|
||||
|
||||
module.exports = {
|
||||
VARIOUS_ARTISTS
|
||||
}
|
||||
1
deemix/deemix/utils/blowfish.js
Normal file
1
deemix/deemix/utils/blowfish.js
Normal file
File diff suppressed because one or more lines are too long
56
deemix/deemix/utils/crypto.js
Normal file
56
deemix/deemix/utils/crypto.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const crypto = require('crypto')
|
||||
let Blowfish
|
||||
try {
|
||||
Blowfish = require('./blowfish.js')
|
||||
} catch (e) { /* empty */ }
|
||||
|
||||
function _md5 (data, type = 'binary') {
|
||||
const md5sum = crypto.createHash('md5')
|
||||
md5sum.update(Buffer.from(data, type))
|
||||
return md5sum.digest('hex')
|
||||
}
|
||||
|
||||
function _ecbCrypt (key, data) {
|
||||
const cipher = crypto.createCipheriv('aes-128-ecb', Buffer.from(key), Buffer.from(''))
|
||||
cipher.setAutoPadding(false)
|
||||
return Buffer.concat([cipher.update(data, 'binary'), cipher.final()]).toString('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function _ecbDecrypt (key, data) {
|
||||
const cipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(key), Buffer.from(''))
|
||||
cipher.setAutoPadding(false)
|
||||
return Buffer.concat([cipher.update(data, 'binary'), cipher.final()]).toString('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function generateBlowfishKey (trackId) {
|
||||
const SECRET = 'g4el58wc0zvf9na1'
|
||||
const idMd5 = _md5(trackId.toString(), 'ascii')
|
||||
let bfKey = ''
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i))
|
||||
}
|
||||
return String(bfKey)
|
||||
}
|
||||
|
||||
function decryptChunk (chunk, blowFishKey) {
|
||||
const ciphers = crypto.getCiphers()
|
||||
if (ciphers.includes('bf-cbc')) {
|
||||
const cipher = crypto.createDecipheriv('bf-cbc', blowFishKey, Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]))
|
||||
cipher.setAutoPadding(false)
|
||||
return Buffer.concat([cipher.update(chunk), cipher.final()])
|
||||
}
|
||||
if (Blowfish) {
|
||||
const cipher = new Blowfish(blowFishKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL)
|
||||
cipher.setIv(Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]))
|
||||
return Buffer.from(cipher.decode(chunk, Blowfish.TYPE.UINT8_ARRAY))
|
||||
}
|
||||
throw new Error("Can't find a way to decrypt chunks")
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_md5,
|
||||
_ecbCrypt,
|
||||
_ecbDecrypt,
|
||||
generateBlowfishKey,
|
||||
decryptChunk
|
||||
}
|
||||
53
deemix/deemix/utils/deezer.js
Normal file
53
deemix/deemix/utils/deezer.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const got = require('got')
|
||||
const {CookieJar} = require('tough-cookie')
|
||||
const {_md5} = require('./crypto.js')
|
||||
const { USER_AGENT_HEADER } = require('./index.js')
|
||||
|
||||
const CLIENT_ID = "172365"
|
||||
const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"
|
||||
|
||||
async function getAccessToken(email, password){
|
||||
let accessToken = null
|
||||
password = _md5(password, 'utf8')
|
||||
const hash = _md5([CLIENT_ID, email, password, CLIENT_SECRET].join(''), 'utf8')
|
||||
try {
|
||||
let response = await got.get("https://api.deezer.com/auth/token",{
|
||||
searchParams: {
|
||||
app_id: CLIENT_ID,
|
||||
login: email,
|
||||
password: password,
|
||||
hash
|
||||
},
|
||||
https: {rejectUnauthorized: false},
|
||||
headers: {"User-Agent": USER_AGENT_HEADER}
|
||||
}).json()
|
||||
accessToken = response.access_token
|
||||
if (accessToken == "undefined") accessToken = null
|
||||
} catch { /*empty*/ }
|
||||
return accessToken
|
||||
}
|
||||
|
||||
async function getArlFromAccessToken(accessToken){
|
||||
if (!accessToken) return null
|
||||
let arl = null
|
||||
let cookieJar = new CookieJar()
|
||||
try {
|
||||
await got.get("https://api.deezer.com/platform/generic/track/3135556", {
|
||||
headers: {"Authorization": `Bearer ${accessToken}`, "User-Agent": USER_AGENT_HEADER},
|
||||
https: {rejectUnauthorized: false},
|
||||
cookieJar
|
||||
})
|
||||
let response = await got.get('https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null', {
|
||||
headers: {"User-Agent": USER_AGENT_HEADER},
|
||||
https: {rejectUnauthorized: false},
|
||||
cookieJar
|
||||
}).json()
|
||||
arl = response.results
|
||||
} catch { /*empty*/ }
|
||||
return arl
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAccessToken,
|
||||
getArlFromAccessToken
|
||||
}
|
||||
1
deemix/deemix/utils/id3-writer.js
Normal file
1
deemix/deemix/utils/id3-writer.js
Normal file
File diff suppressed because one or more lines are too long
175
deemix/deemix/utils/index.js
Normal file
175
deemix/deemix/utils/index.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const stream = require('stream')
|
||||
const {promisify} = require('util')
|
||||
const pipeline = promisify(stream.pipeline)
|
||||
const { accessSync, constants } = require('fs')
|
||||
const { ErrorMessages } = require('../errors.js')
|
||||
|
||||
const USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"
|
||||
|
||||
function canWrite(path){
|
||||
try{
|
||||
accessSync(path, constants.R_OK | constants.W_OK)
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function generateReplayGainString(trackGain){
|
||||
return `${Math.round((parseFloat(trackGain) + 18.4)*-100)/100} dB`
|
||||
}
|
||||
|
||||
function changeCase(txt, type){
|
||||
switch (type) {
|
||||
case 'lower': return txt.toLowerCase()
|
||||
case 'upper': return txt.toUpperCase()
|
||||
case 'start':
|
||||
txt = txt.trim().split(" ")
|
||||
for (let i = 0; i < txt.length; i++) {
|
||||
if (['(', '{', '[', "'", '"'].some(bracket => ( txt[i].length > 1 && txt[i].startsWith(bracket) ) )) {
|
||||
txt[i] = txt[i][0] + txt[i][1].toUpperCase() + txt[i].substr(2).toLowerCase()
|
||||
} else if (txt[i].length > 1) {
|
||||
txt[i] = txt[i][0].toUpperCase() + txt[i].substr(1).toLowerCase()
|
||||
} else {
|
||||
txt[i] = txt[i][0].toUpperCase()
|
||||
}
|
||||
}
|
||||
return txt.join(" ")
|
||||
case 'sentence': return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase()
|
||||
default: return txt
|
||||
}
|
||||
}
|
||||
|
||||
function removeFeatures(title){
|
||||
let clean = title
|
||||
let found = false
|
||||
let pos
|
||||
if (clean.search(/[\s(]\(?\s?feat\.?\s/gi) != -1){
|
||||
pos = clean.search(/[\s(]\(?\s?feat\.?\s/gi)
|
||||
found = true
|
||||
}
|
||||
if (clean.search(/[\s(]\(?\s?ft\.?\s/gi) != -1){
|
||||
pos = clean.search(/[\s(]\(?\s?ft\.?\s/gi)
|
||||
found = true
|
||||
}
|
||||
const openBracket = clean[pos] == '(' || clean[pos+1] == '('
|
||||
const otherBracket = clean.indexOf('(', pos+2)
|
||||
if (found) {
|
||||
let tempTrack = clean.slice(0, pos)
|
||||
if (clean.includes(')') && openBracket)
|
||||
tempTrack += clean.slice(clean.indexOf(')', pos+2)+1)
|
||||
if (!openBracket && otherBracket != -1)
|
||||
tempTrack += ` ${clean.slice(otherBracket)}`
|
||||
clean = tempTrack.trim()
|
||||
clean = clean.replace(/\s\s+/g, ' ') // remove extra spaces
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
function andCommaConcat(lst){
|
||||
const tot = lst.length
|
||||
let result = ""
|
||||
lst.forEach((art, i) => {
|
||||
result += art
|
||||
if (tot != i+1){
|
||||
if (tot - 1 == i+1){
|
||||
result += " & "
|
||||
} else {
|
||||
result += ", "
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function uniqueArray(arr){
|
||||
arr.forEach((namePrinc, iPrinc) => {
|
||||
arr.forEach((nameRest, iRest) => {
|
||||
if (iPrinc != iRest && nameRest.toLowerCase().includes(namePrinc.toLowerCase())){
|
||||
arr.splice(iRest, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
function shellEscape(s){
|
||||
if (typeof s !== 'string') return ''
|
||||
if (!(/[^\w@%+=:,./-]/g.test(s))) return s
|
||||
return "'" + s.replaceAll("'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
function removeDuplicateArtists(artist, artists){
|
||||
artists = uniqueArray(artists)
|
||||
Object.keys(artist).forEach((role) => {
|
||||
artist[role] = uniqueArray(artist[role])
|
||||
})
|
||||
return [artist, artists]
|
||||
}
|
||||
|
||||
function formatListener(key, data){
|
||||
let message = ""
|
||||
switch (key) {
|
||||
case "startAddingArtist": return `Started gathering ${data.name}'s albums (${data.id})`
|
||||
case "finishAddingArtist": return `Finished gathering ${data.name}'s albums (${data.id})`
|
||||
case "updateQueue":
|
||||
message = `[${data['uuid']}]`
|
||||
if (data.downloaded) message += ` Completed download of ${data.downloadPath.slice(data.extrasPath.length+1)}`
|
||||
if (data.failed) message += ` ${data.data.artist} - ${data.data.title} :: ${data.error}`
|
||||
if (data.progress) message += ` Download at ${data.progress}%`
|
||||
if (data.conversion) message += ` Conversion at ${data.conversion}%`
|
||||
return message
|
||||
case "downloadInfo":
|
||||
message = data.state
|
||||
switch (data.state) {
|
||||
case "getTags": message = "Getting tags."; break;
|
||||
case "gotTags": message = "Tags got."; break;
|
||||
case "getBitrate": message = "Getting download URL."; break;
|
||||
case "bitrateFallback": message = "Desired bitrate not found, falling back to lower bitrate."; break;
|
||||
case "searchFallback": message = "This track has been searched for, result might not be 100% exact."; break;
|
||||
case "gotBitrate": message = "Download URL got."; break;
|
||||
case "getAlbumArt": message = "Downloading album art."; break;
|
||||
case "gotAlbumArt": message = "Album art downloaded."; break;
|
||||
case "downloading":
|
||||
message = "Downloading track.";
|
||||
if (data.alreadyStarted) message += ` Recovering download from ${data.value}.`
|
||||
else message += ` Downloading ${data.value} bytes.`
|
||||
break;
|
||||
case "downloadTimeout": message = "Deezer timedout when downloading track, retrying..."; break;
|
||||
case "downloaded": message = "Track downloaded."; break;
|
||||
case "alreadyDownloaded": message = "Track already downloaded."; break;
|
||||
case "tagging": message = "Tagging track."; break;
|
||||
case "tagged": message = "Track tagged."; break;
|
||||
case "stderr": return `ExecuteCommand Error: ${data.data.stderr}`
|
||||
case "stdout": return `ExecuteCommand Output: ${data.data.stdout}`
|
||||
}
|
||||
return `[${data.uuid}] ${data.data.artist} - ${data.data.title} :: ${message}`
|
||||
case "downloadWarn":
|
||||
message = `[${data.uuid}] ${data.data.artist} - ${data.data.title} :: ${ErrorMessages[data.state]} `
|
||||
switch (data.solution) {
|
||||
case 'fallback': message += "Using fallback id."; break;
|
||||
case 'search': message += "Searching for alternative."; break;
|
||||
}
|
||||
return message
|
||||
case "currentItemCancelled": return `Current item cancelled (${data})`
|
||||
case "removedFromQueue": return `[${data}] Removed from the queue`
|
||||
case "finishDownload": return `[${data}] Finished downloading`
|
||||
case "startConversion": return `[${data}] Started converting`
|
||||
case "finishConversion": return `[${data.uuid}] Finished converting`
|
||||
default: return message
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
USER_AGENT_HEADER,
|
||||
generateReplayGainString,
|
||||
removeFeatures,
|
||||
andCommaConcat,
|
||||
uniqueArray,
|
||||
removeDuplicateArtists,
|
||||
pipeline,
|
||||
canWrite,
|
||||
changeCase,
|
||||
shellEscape,
|
||||
formatListener
|
||||
}
|
||||
89
deemix/deemix/utils/localpaths.js
Normal file
89
deemix/deemix/utils/localpaths.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { sep } = require('path')
|
||||
const { homedir } = require('os')
|
||||
const fs = require('fs')
|
||||
const { canWrite } = require('./index.js')
|
||||
|
||||
let homedata = homedir()
|
||||
let userdata = ""
|
||||
let musicdata = ""
|
||||
|
||||
function checkPath(path){
|
||||
if (path === "") return ""
|
||||
if (!fs.existsSync(path)) return ""
|
||||
if (!canWrite(path)) return ""
|
||||
return path
|
||||
}
|
||||
|
||||
function getConfigFolder(){
|
||||
if (userdata != "") return userdata
|
||||
if (process.env.XDG_CONFIG_HOME && userdata === ""){
|
||||
userdata = `${process.env.XDG_CONFIG_HOME}${sep}`
|
||||
userdata = checkPath(userdata)
|
||||
}
|
||||
if (process.env.APPDATA && userdata === ""){
|
||||
userdata = `${process.env.APPDATA}${sep}`
|
||||
userdata = checkPath(userdata)
|
||||
}
|
||||
if (process.platform == "darwin" && userdata === ""){
|
||||
userdata = `${homedata}/Library/Application Support/`
|
||||
userdata = checkPath(userdata)
|
||||
}
|
||||
if (userdata === ""){
|
||||
userdata = `${homedata}${sep}.config${sep}`
|
||||
userdata = checkPath(userdata)
|
||||
}
|
||||
|
||||
if (userdata === "") userdata = `${process.cwd()}${sep}config${sep}`
|
||||
else userdata += `deemix${sep}`
|
||||
|
||||
if (process.env.DEEMIX_DATA_DIR) userdata = process.env.DEEMIX_DATA_DIR
|
||||
return userdata
|
||||
}
|
||||
|
||||
function getMusicFolder(){
|
||||
if (musicdata != "") return musicdata
|
||||
if (process.env.XDG_MUSIC_DIR && musicdata === ""){
|
||||
musicdata = `${process.env.XDG_MUSIC_DIR}${sep}`
|
||||
musicdata = checkPath(musicdata)
|
||||
}
|
||||
if (fs.existsSync(`${homedata}${sep}.config${sep}user-dirs.dirs`)){
|
||||
const userDirs = fs.readFileSync(`${homedata}${sep}.config${sep}user-dirs.dirs`).toString()
|
||||
musicdata = userDirs.match(/XDG_MUSIC_DIR="(.*)"/)[1]
|
||||
musicdata = musicdata.replace(/\$([A-Z_]+[A-Z0-9_]*)/ig, (_, envName) => process.env[envName])
|
||||
musicdata += sep
|
||||
musicdata = checkPath(musicdata)
|
||||
}
|
||||
if (process.platform == 'win32' && musicdata === ""){
|
||||
try {
|
||||
const { execSync } = require('child_process')
|
||||
const musicKeys = ["My Music", "{4BD8D571-6D19-48D3-BE97-422220080E43}"]
|
||||
let regData = execSync('reg.exe query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"').toString().split('\r\n')
|
||||
for (let i = 0; i < regData.length; i++){
|
||||
let line = regData[i]
|
||||
if (line === "") continue
|
||||
if (i == 1) continue
|
||||
line = line.split(' ')
|
||||
if (musicKeys.includes(line[1])){
|
||||
musicdata = line[3] + sep
|
||||
break;
|
||||
}
|
||||
}
|
||||
musicdata = checkPath(musicdata)
|
||||
} catch {/* empty */}
|
||||
}
|
||||
if (musicdata === ""){
|
||||
musicdata = `${homedata}${sep}Music${sep}`
|
||||
musicdata = checkPath(musicdata)
|
||||
}
|
||||
|
||||
if (musicdata === "") musicdata = `${process.cwd()}${sep}music${sep}`
|
||||
else musicdata += `deemix Music${sep}`
|
||||
|
||||
if (process.env.DEEMIX_MUSIC_DIR) musicdata = process.env.DEEMIX_MUSIC_DIR
|
||||
return musicdata
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfigFolder,
|
||||
getMusicFolder
|
||||
}
|
||||
259
deemix/deemix/utils/pathtemplates.js
Normal file
259
deemix/deemix/utils/pathtemplates.js
Normal file
@@ -0,0 +1,259 @@
|
||||
const { TrackFormats } = require('deezer-js')
|
||||
const { Date: dzDate } = require('../types/Date.js')
|
||||
|
||||
const bitrateLabels = {
|
||||
[TrackFormats.MP4_RA3]: "360 HQ",
|
||||
[TrackFormats.MP4_RA2]: "360 MQ",
|
||||
[TrackFormats.MP4_RA1]: "360 LQ",
|
||||
[TrackFormats.FLAC] : "FLAC",
|
||||
[TrackFormats.MP3_320]: "320",
|
||||
[TrackFormats.MP3_128]: "128",
|
||||
[TrackFormats.DEFAULT]: "128",
|
||||
[TrackFormats.LOCAL] : "MP3"
|
||||
}
|
||||
|
||||
function fixName(txt, char='_'){
|
||||
txt = txt+""
|
||||
txt = txt.replace(/[\0/\\:*?"<>|]/g, char)
|
||||
return txt.normalize('NFC')
|
||||
}
|
||||
|
||||
function fixLongName(name){
|
||||
if (name.includes('/')){
|
||||
let sepName = name.split('/')
|
||||
name = ""
|
||||
sepName.forEach((txt) => {
|
||||
txt = fixLongName(txt)
|
||||
name += `${txt}/`
|
||||
})
|
||||
name = name.slice(0, -1)
|
||||
} else {
|
||||
name = name.slice(0, 200)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
function antiDot(str){
|
||||
while(str[str.length-1] == "." || str[str.length-1] == " " || str[str.length-1] == "\n"){
|
||||
str = str.slice(0,-1)
|
||||
}
|
||||
if(str.length < 1){
|
||||
str = "dot"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function pad(num, max_val, settings) {
|
||||
let paddingSize;
|
||||
if (parseInt(settings.paddingSize) == 0) {
|
||||
paddingSize = (max_val+"").length
|
||||
} else{
|
||||
paddingSize = ((10 ** (parseInt(settings.paddingSize) - 1))+"").length
|
||||
}
|
||||
if (settings.padSingleDigit && paddingSize == 1) paddingSize = 2
|
||||
|
||||
if (settings.padTracks) return (num+"").padStart(paddingSize, "0")
|
||||
return (num+"")
|
||||
}
|
||||
|
||||
function generatePath(track, downloadObject, settings){
|
||||
let filenameTemplate = "%artist% - %title%";
|
||||
let singleTrack = false
|
||||
if (downloadObject.type === "track"){
|
||||
if (settings.createSingleFolder) filenameTemplate = settings.albumTracknameTemplate
|
||||
else filenameTemplate = settings.tracknameTemplate
|
||||
singleTrack = true
|
||||
} else if (downloadObject.type === "album") {
|
||||
filenameTemplate = settings.albumTracknameTemplate
|
||||
} else {
|
||||
filenameTemplate = settings.playlistTracknameTemplate
|
||||
}
|
||||
|
||||
let filename = generateTrackName(filenameTemplate, track, settings)
|
||||
let filepath, artistPath, coverPath, extrasPath
|
||||
|
||||
filepath = settings.downloadLocation || "."
|
||||
|
||||
if (settings.createPlaylistFolder && track.playlist && !settings.tags.savePlaylistAsCompilation)
|
||||
filepath += `/${generatePlaylistName(settings.playlistNameTemplate, track.playlist, settings)}`
|
||||
|
||||
if (track.playlist && !settings.tags.savePlaylistAsCompilation)
|
||||
extrasPath = filepath
|
||||
|
||||
if (
|
||||
(settings.createArtistFolder && !track.playlist) ||
|
||||
(settings.createArtistFolder && track.playlist && settings.tags.savePlaylistAsCompilation) ||
|
||||
(settings.createArtistFolder && track.playlist && settings.createStructurePlaylist)
|
||||
){
|
||||
filepath += `/${generateArtistName(settings.artistNameTemplate, track.album.mainArtist, settings, track.album.rootArtist)}`
|
||||
artistPath = filepath
|
||||
}
|
||||
|
||||
|
||||
if (settings.createAlbumFolder &&
|
||||
(!singleTrack || singleTrack && settings.createSingleFolder) &&
|
||||
(!track.playlist ||
|
||||
(track.playlist && settings.tags.savePlaylistAsCompilation) ||
|
||||
(track.playlist && settings.createStructurePlaylist)
|
||||
)
|
||||
){
|
||||
filepath += `/${generateAlbumName(settings.albumNameTemplate, track.album, settings, track.playlist)}`
|
||||
coverPath = filepath
|
||||
}
|
||||
|
||||
if (!extrasPath) extrasPath = filepath
|
||||
|
||||
if (
|
||||
parseInt(track.album.discTotal) > 1 && (
|
||||
(settings.createAlbumFolder && settings.createCDFolder) &&
|
||||
(!singleTrack || (singleTrack && settings.createSingleFolder)) &&
|
||||
(!track.playlist ||
|
||||
(track.playlist && settings.tags.savePlaylistAsCompilation)) ||
|
||||
(track.playlist && settings.createStructurePlaylist)
|
||||
)
|
||||
)
|
||||
filepath += `/CD${track.discNumber}`
|
||||
|
||||
// Remove Subfolders from filename and add it to filepath
|
||||
if (filename.includes('/')){
|
||||
let tempPath = filename.slice(0, filename.indexOf('/'))
|
||||
filepath += `/${tempPath}`
|
||||
filename = filename.slice(tempPath.length+1)
|
||||
}
|
||||
|
||||
return {
|
||||
filename,
|
||||
filepath,
|
||||
artistPath,
|
||||
coverPath,
|
||||
extrasPath
|
||||
}
|
||||
}
|
||||
|
||||
function generateTrackName(filename, track, settings){
|
||||
let c = settings.illegalCharacterReplacer
|
||||
filename = filename.replaceAll("%title%", fixName(track.title, c))
|
||||
filename = filename.replaceAll("%artist%", fixName(track.mainArtist.name, c))
|
||||
filename = filename.replaceAll("%artists%", fixName(track.artists.join(", "), c))
|
||||
filename = filename.replaceAll("%tagsartists%", fixName(track.artistsString, c))
|
||||
filename = filename.replaceAll("%allartists%", fixName(track.fullArtistsString, c))
|
||||
filename = filename.replaceAll("%mainartists%", fixName(track.mainArtistsString, c))
|
||||
if (track.featArtistsString) filename = filename.replaceAll("%featartists%", fixName('('+track.featArtistsString+')', c))
|
||||
else filename = filename.replaceAll(" %featartists%", '').replaceAll("%featartists%", '')
|
||||
filename = filename.replaceAll("%album%", fixName(track.album.title, c))
|
||||
filename = filename.replaceAll("%albumartist%", fixName(track.album.mainArtist.name, c))
|
||||
filename = filename.replaceAll("%tracknumber%", pad(track.trackNumber, track.album.trackTotal, settings))
|
||||
filename = filename.replaceAll("%tracktotal%", track.album.trackTotal)
|
||||
filename = filename.replaceAll("%discnumber%", track.discNumber)
|
||||
filename = filename.replaceAll("%disctotal%", track.album.discTotal)
|
||||
if (track.album.genre.length) filename = filename.replaceAll("%genre%", fixName(track.album.genre[0], c))
|
||||
else filename = filename.replaceAll("%genre%", "Unknown")
|
||||
filename = filename.replaceAll("%year%", track.date.year)
|
||||
filename = filename.replaceAll("%date%", track.dateString)
|
||||
filename = filename.replaceAll("%bpm%", track.bpm)
|
||||
filename = filename.replaceAll("%label%", fixName(track.album.label, c))
|
||||
filename = filename.replaceAll("%isrc%", track.ISRC)
|
||||
filename = filename.replaceAll("%upc%", track.album.barcode)
|
||||
if (track.explicit) filename = filename.replaceAll("%explicit%", "(Explicit)")
|
||||
else filename = filename.replaceAll(" %explicit%", "").replaceAll("%explicit%", "")
|
||||
|
||||
filename = filename.replaceAll("%track_id%", track.id)
|
||||
filename = filename.replaceAll("%album_id%", track.album.id)
|
||||
filename = filename.replaceAll("%artist_id%", track.mainArtist.id)
|
||||
if (track.playlist){
|
||||
filename = filename.replaceAll("%playlist_id%", track.playlist.playlistID)
|
||||
filename = filename.replaceAll("%position%", pad(track.position, track.playlist.trackTotal, settings))
|
||||
} else {
|
||||
filename = filename.replaceAll("%playlist_id%", '')
|
||||
filename = filename.replaceAll("%position%", pad(track.trackNumber, track.album.trackTotal, settings))
|
||||
}
|
||||
filename = filename.replaceAll('\\', '/')
|
||||
return antiDot(fixLongName(filename))
|
||||
}
|
||||
|
||||
function generateAlbumName(foldername, album, settings, playlist){
|
||||
let c = settings.illegalCharacterReplacer
|
||||
if (playlist && settings.tags.savePlaylistAsCompilation){
|
||||
foldername = foldername.replaceAll("%album_id%", "pl_" + playlist.playlistID)
|
||||
foldername = foldername.replaceAll("%genre%", "Compile")
|
||||
} else {
|
||||
foldername = foldername.replaceAll("%album_id%", album.id)
|
||||
if (album.genre.length) foldername = foldername.replaceAll("%genre%", fixName(album.genre[0], c))
|
||||
else foldername = foldername.replaceAll("%genre%", "Unknown")
|
||||
}
|
||||
foldername = foldername.replaceAll("%album%", fixName(album.title, c))
|
||||
foldername = foldername.replaceAll("%artist%", fixName(album.mainArtist.name, c))
|
||||
foldername = foldername.replaceAll("%artists%", fixName(album.artists.join(", "), c))
|
||||
foldername = foldername.replaceAll("%artist_id%", album.mainArtist.id)
|
||||
if (album.rootArtist){
|
||||
foldername = foldername.replaceAll("%root_artist%", fixName(album.rootArtist.name, c))
|
||||
foldername = foldername.replaceAll("%root_artist_id%", album.rootArtist.id)
|
||||
} else {
|
||||
foldername = foldername.replaceAll("%root_artist%", fixName(album.mainArtist.name, c))
|
||||
foldername = foldername.replaceAll("%root_artist_id%", album.mainArtist.id)
|
||||
}
|
||||
foldername = foldername.replaceAll("%tracktotal%", album.trackTotal)
|
||||
foldername = foldername.replaceAll("%disctotal%", album.discTotal)
|
||||
foldername = foldername.replaceAll("%type%", fixName(album.recordType.charAt(0).toUpperCase() + album.recordType.slice(1), c))
|
||||
foldername = foldername.replaceAll("%upc%", album.barcode)
|
||||
foldername = foldername.replaceAll("%explicit%", album.explicit ? "(Explicit)" : "")
|
||||
foldername = foldername.replaceAll("%label%", fixName(album.label, c))
|
||||
foldername = foldername.replaceAll("%year%", album.date.year)
|
||||
foldername = foldername.replaceAll("%date%", album.dateString)
|
||||
foldername = foldername.replaceAll("%bitrate%", bitrateLabels[parseInt(album.bitrate)])
|
||||
|
||||
foldername = foldername.replaceAll('\\', '/')
|
||||
return antiDot(fixLongName(foldername))
|
||||
}
|
||||
|
||||
function generateArtistName(foldername, artist, settings, rootArtist){
|
||||
let c = settings['illegalCharacterReplacer']
|
||||
foldername = foldername.replaceAll("%artist%", fixName(artist.name, c))
|
||||
foldername = foldername.replaceAll("%artist_id%", artist.id)
|
||||
if (rootArtist){
|
||||
foldername = foldername.replaceAll("%root_artist%", fixName(rootArtist.name, c))
|
||||
foldername = foldername.replaceAll("%root_artist_id%", rootArtist.id)
|
||||
} else {
|
||||
foldername = foldername.replaceAll("%root_artist%", fixName(artist.name, c))
|
||||
foldername = foldername.replaceAll("%root_artist_id%", artist.id)
|
||||
}
|
||||
foldername = foldername.replaceAll('\\', '/')
|
||||
return antiDot(fixLongName(foldername))
|
||||
}
|
||||
|
||||
function generatePlaylistName(foldername, playlist, settings){
|
||||
let c = settings['illegalCharacterReplacer']
|
||||
let today = new Date()
|
||||
let today_dz = new dzDate(String(today.getDate()).padStart(2, '0'), String(today.getMonth()+1).padStart(2, '0'), String(today.getFullYear()))
|
||||
foldername = foldername.replaceAll("%playlist%", fixName(playlist.title, c))
|
||||
foldername = foldername.replaceAll("%playlist_id%", fixName(playlist.playlistID, c))
|
||||
foldername = foldername.replaceAll("%owner%", fixName(playlist.owner['name'], c))
|
||||
foldername = foldername.replaceAll("%owner_id%", playlist.owner['id'])
|
||||
foldername = foldername.replaceAll("%year%", playlist.date.year)
|
||||
foldername = foldername.replaceAll("%date%", playlist.dateString)
|
||||
foldername = foldername.replaceAll("%explicit%", playlist.explicit ? "(Explicit)" : "")
|
||||
foldername = foldername.replaceAll("%today%", today_dz.format(settings['dateFormat']))
|
||||
foldername = foldername.replaceAll('\\', '/')
|
||||
return antiDot(fixLongName(foldername))
|
||||
}
|
||||
|
||||
function generateDownloadObjectName(foldername, queueItem, settings){
|
||||
let c = settings['illegalCharacterReplacer']
|
||||
foldername = foldername.replaceAll("%title%", fixName(queueItem.title, c))
|
||||
foldername = foldername.replaceAll("%artist%", fixName(queueItem.artist, c))
|
||||
foldername = foldername.replaceAll("%size%", queueItem.size)
|
||||
foldername = foldername.replaceAll("%type%", fixName(queueItem.type, c))
|
||||
foldername = foldername.replaceAll("%id%", fixName(queueItem.id, c))
|
||||
foldername = foldername.replaceAll("%bitrate%", bitrateLabels[parseInt(queueItem.bitrate)])
|
||||
foldername = foldername.replaceAll('\\', '/').replace('/', c)
|
||||
return antiDot(fixLongName(foldername))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generatePath,
|
||||
generateTrackName,
|
||||
generateAlbumName,
|
||||
generateArtistName,
|
||||
generatePlaylistName,
|
||||
generateDownloadObjectName
|
||||
}
|
||||
22
deemix/package.json
Normal file
22
deemix/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "deemix",
|
||||
"version": "3.6.15",
|
||||
"description": "a barebones deezer downloader library",
|
||||
"main": "deemix/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "RemixDev, Deeplydrumming",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"async": "^3.2.0",
|
||||
"deezer-js": "^1.3.0",
|
||||
"got": "^11.8.2",
|
||||
"html-entities": "^2.3.3",
|
||||
"metaflac-js2": "^1.0.8",
|
||||
"spotify-web-api-node": "../spotify-web-api-node"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.7.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user