2020-10-31 15:54:28 +00:00
|
|
|
const {DeezerAPI} = require('./deezer');
|
|
|
|
const Datastore = require('nedb');
|
2020-08-28 22:06:19 +01:00
|
|
|
const {Settings} = require('./settings');
|
|
|
|
const fs = require('fs');
|
|
|
|
const https = require('https');
|
2020-10-31 15:54:28 +00:00
|
|
|
const logger = require('./winston');
|
|
|
|
const path = require('path');
|
|
|
|
const decryptor = require('nodeezcryptor');
|
|
|
|
const sanitize = require('sanitize-filename');
|
2020-08-28 22:06:19 +01:00
|
|
|
const ID3Writer = require('browser-id3-writer');
|
|
|
|
const Metaflac = require('metaflac-js2');
|
2020-10-31 15:54:28 +00:00
|
|
|
const { Track, Lyrics } = require('./definitions');
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
let deezer;
|
|
|
|
|
|
|
|
class DownloadManager {
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
constructor(settings, callback) {
|
2020-08-28 22:06:19 +01:00
|
|
|
this.settings = settings;
|
2020-10-31 15:54:28 +00:00
|
|
|
this.downloading = false;
|
|
|
|
this.callback = callback;
|
|
|
|
|
|
|
|
this.queue = [];
|
|
|
|
this.threads = [];
|
|
|
|
|
|
|
|
this.updateRequests = 0;
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Update DeezerAPI global
|
|
|
|
setDeezer(d) {
|
|
|
|
deezer = d;
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
async load() {
|
|
|
|
this.db = new Datastore({filename: Settings.getDownloadsDB(), autoload: true});
|
|
|
|
|
|
|
|
//Load from DB
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
this.db.find({state: 0}, (err, docs) => {
|
|
|
|
if (!err) {
|
|
|
|
this.queue = docs.map(d => Download.fromDB(d));
|
|
|
|
}
|
|
|
|
resolve();
|
2020-08-28 22:06:19 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Create temp dir
|
|
|
|
if (!fs.existsSync(Settings.getTempDownloads())) {
|
|
|
|
fs.promises.mkdir(Settings.getTempDownloads(), {recursive: true});
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
|
|
|
this.downloading = true;
|
2020-10-31 15:54:28 +00:00
|
|
|
await this.updateQueue();
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
this.downloading = false;
|
2020-10-31 15:54:28 +00:00
|
|
|
//Stop all threads
|
|
|
|
let nThreads = this.threads.length;
|
|
|
|
for (let i=nThreads-1; i>=0; i--) {
|
|
|
|
await this.threads[i].stop();
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
2020-10-31 15:54:28 +00:00
|
|
|
this.updateQueue();
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-11-03 09:11:37 +00:00
|
|
|
//data: {tracks: [], quality, playlistName}
|
|
|
|
async addBatch(data) {
|
|
|
|
for (let track of data.tracks) {
|
|
|
|
let p = this.settings.downloadsPath;
|
|
|
|
if (data.playlistName && this.settings.playlistFolder) {
|
|
|
|
p = path.join(p, sanitize(data.playlistName));
|
|
|
|
}
|
|
|
|
await this.add(track, data.quality, p);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async add(track, quality, p) {
|
2020-10-31 15:54:28 +00:00
|
|
|
//Sanitize quality
|
|
|
|
let q = this.settings.downloadsQuality;
|
|
|
|
if (quality)
|
|
|
|
q = parseInt(quality.toString(), 10);
|
2020-11-03 09:11:37 +00:00
|
|
|
let download = new Download(track, q, 0, p);
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Check if in queue
|
|
|
|
if (this.queue.some(d => d.track.id == track.id)) {
|
2020-08-28 22:06:19 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Check if in DB
|
|
|
|
let dbDownload = await new Promise((resolve) => {
|
|
|
|
this.db.find({_id: download.track.id}, (err, docs) => {
|
|
|
|
if (err) return resolve(null);
|
|
|
|
if (docs.length == 0) return resolve(null);
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Update download as not done, will be skipped while downloading
|
|
|
|
this.db.update({_id: download.track.id}, {state: 0, quality: download.quality}, {}, () => {
|
|
|
|
resolve(Download.fromDB(docs[0]));
|
|
|
|
});
|
2020-08-28 22:06:19 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Insert to DB
|
|
|
|
if (!dbDownload) {
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
this.db.insert(download.toDB(), () => {
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
2020-10-31 15:54:28 +00:00
|
|
|
|
|
|
|
//Queue
|
|
|
|
this.queue.push(download);
|
|
|
|
this.updateQueue();
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
2020-09-01 19:37:02 +01:00
|
|
|
|
|
|
|
async delete(index) {
|
2020-10-31 15:54:28 +00:00
|
|
|
//-1 = Delete all
|
2020-09-01 19:37:02 +01:00
|
|
|
if (index == -1) {
|
2020-10-31 15:54:28 +00:00
|
|
|
let ids = this.queue.map(q => q.track.id);
|
|
|
|
this.queue = [];
|
|
|
|
//Remove from DB
|
|
|
|
await new Promise((res) => {
|
|
|
|
this.db.remove({_id: {$in: ids}}, {multi: true}, () => {
|
|
|
|
res();
|
|
|
|
})
|
2020-09-02 13:39:43 +01:00
|
|
|
});
|
2020-10-31 15:54:28 +00:00
|
|
|
this.updateQueue();
|
2020-09-01 19:37:02 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Remove single item
|
|
|
|
let id = this.queue[index].track.id;
|
|
|
|
this.queue.splice(index, 1);
|
|
|
|
await new Promise((res) => {
|
|
|
|
this.db.remove({_id: id}, {}, () => {
|
|
|
|
res();
|
|
|
|
})
|
|
|
|
})
|
|
|
|
this.updateQueue();
|
2020-09-01 19:37:02 +01:00
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Thread safe update
|
|
|
|
async updateQueue() {
|
|
|
|
this.updateRequests++;
|
|
|
|
if (this._updatePromise) return;
|
|
|
|
this._updatePromise = this._updateQueue();
|
|
|
|
await this._updatePromise;
|
|
|
|
this._updatePromise = null;
|
|
|
|
this.updateRequests--;
|
|
|
|
if (this.updateRequests > 0) {
|
|
|
|
this.updateRequests--;
|
|
|
|
this.updateQueue();
|
|
|
|
}
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
async _updateQueue() {
|
|
|
|
//Finished downloads
|
|
|
|
if (this.threads.length > 0) {
|
|
|
|
for (let i=this.threads.length-1; i>=0; i--) {
|
|
|
|
if (this.threads[i].download.state == 3 || this.threads[i].download.state == -1) {
|
|
|
|
//Update DB
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
this.db.update({_id: this.threads[i].download.track.id}, {state: this.threads[i].download.state}, {}, () => {
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.threads.splice(i, 1);
|
|
|
|
} else {
|
|
|
|
//Remove if stopped
|
|
|
|
if (this.threads[i].stopped) {
|
|
|
|
this.queue.unshift(this.threads[i].download);
|
|
|
|
this.threads.splice(i, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//Create new threads
|
|
|
|
if (this.downloading) {
|
|
|
|
let nThreads = this.settings.downloadThreads - this.threads.length;
|
|
|
|
for (let i=0; i<nThreads; i++) {
|
|
|
|
if (this.queue.length > 0) {
|
|
|
|
let thread = new DownloadThread(this.queue[0], () => {this.updateQueue();}, this.settings);
|
|
|
|
thread.start();
|
|
|
|
this.threads.push(thread);
|
|
|
|
this.queue.splice(0, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//Stop downloading if queues empty
|
|
|
|
if (this.queue.length == 0 && this.threads.length == 0 && this.downloading)
|
|
|
|
this.downloading = false;
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Update UI
|
|
|
|
if (this.callback)
|
|
|
|
this.callback();
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
2020-10-31 15:54:28 +00:00
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
class DownloadThread {
|
|
|
|
constructor (download, callback, settings) {
|
|
|
|
this.download = download;
|
|
|
|
this.callback = callback;
|
|
|
|
this.settings = settings;
|
|
|
|
this.stopped = true;
|
|
|
|
this.isUserUploaded = download.track.id.toString().startsWith('-');
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Callback wrapper
|
|
|
|
_cb() {
|
|
|
|
if (this.callback) this.callback();
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
2020-10-31 15:54:28 +00:00
|
|
|
this.download.state = 1;
|
|
|
|
this.stopped = false;
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Fallback
|
|
|
|
this.qualityInfo = await deezer.fallback(this.download.track.streamUrl, this.download.quality);
|
|
|
|
if (!this.qualityInfo) {
|
|
|
|
this.download.state = -1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Get track info
|
|
|
|
if (!this.isUserUploaded) {
|
|
|
|
this.rawTrack = await deezer.callApi('deezer.pageTrack', {'sng_id': this.download.track.id});
|
|
|
|
this.track = new Track(this.rawTrack.results.DATA);
|
|
|
|
this.publicTrack = await deezer.callPublicApi('track', this.track.id);
|
|
|
|
this.publicAlbum = await deezer.callPublicApi('album', this.track.album.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
//Check if exists
|
|
|
|
let outPath = this.generatePath(this.qualityInfo.quality);
|
|
|
|
try {
|
|
|
|
await fs.promises.access(outPath, fs.constants.R_OK);
|
|
|
|
//File exists
|
|
|
|
this.download.state = 3;
|
|
|
|
return this._cb();
|
|
|
|
} catch (_) {}
|
|
|
|
|
2020-08-28 22:06:19 +01:00
|
|
|
//Path to temp file
|
2020-10-31 15:54:28 +00:00
|
|
|
let tmp = path.join(Settings.getTempDownloads(), `${this.download.track.id}.ENC`);
|
2020-08-28 22:06:19 +01:00
|
|
|
//Get start offset
|
|
|
|
let start = 0;
|
|
|
|
try {
|
|
|
|
let stat = await fs.promises.stat(tmp);
|
|
|
|
if (stat.size) start = stat.size;
|
2020-10-31 15:54:28 +00:00
|
|
|
|
|
|
|
// eslint-disable-next-line no-empty
|
2020-08-28 22:06:19 +01:00
|
|
|
} catch (e) {}
|
2020-10-31 15:54:28 +00:00
|
|
|
this.download.downloaded = start;
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Download
|
|
|
|
let url = DeezerAPI.getUrl(this.qualityInfo.trackId, this.qualityInfo.md5origin, this.qualityInfo.mediaVersion, this.qualityInfo.quality);
|
|
|
|
if (this.stopped) return;
|
|
|
|
this._request = https.get(url, {headers: {'Range': `bytes=${start}-`}}, (r) => {
|
|
|
|
this._response = r;
|
2020-10-01 13:30:00 +01:00
|
|
|
let outFile = fs.createWriteStream(tmp, {flags: 'a'});
|
2020-10-31 15:54:28 +00:00
|
|
|
|
2020-08-28 22:06:19 +01:00
|
|
|
//On download done
|
|
|
|
r.on('end', () => {
|
2020-10-31 15:54:28 +00:00
|
|
|
if (this.download.size != this.download.downloaded) return;
|
2020-10-01 13:30:00 +01:00
|
|
|
|
2020-10-02 13:25:56 +01:00
|
|
|
outFile.on('finish', () => {
|
|
|
|
outFile.close(() => {
|
2020-10-31 15:54:28 +00:00
|
|
|
this.postPromise = this._post(tmp);
|
2020-10-02 13:25:56 +01:00
|
|
|
});
|
2020-10-01 13:30:00 +01:00
|
|
|
});
|
2020-10-02 13:25:56 +01:00
|
|
|
outFile.end();
|
2020-08-28 22:06:19 +01:00
|
|
|
});
|
|
|
|
//Progress
|
|
|
|
r.on('data', (c) => {
|
2020-10-01 13:30:00 +01:00
|
|
|
outFile.write(c);
|
2020-10-31 15:54:28 +00:00
|
|
|
this.download.downloaded += c.length;
|
2020-08-28 22:06:19 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
r.on('error', (e) => {
|
2020-09-28 11:04:19 +01:00
|
|
|
logger.error(`Download error: ${e}`);
|
2020-08-28 22:06:19 +01:00
|
|
|
//TODO: Download error handling
|
|
|
|
})
|
|
|
|
|
|
|
|
//Save size
|
|
|
|
this.size = parseInt(r.headers['content-length'], 10) + start;
|
2020-10-31 15:54:28 +00:00
|
|
|
this.download.size = this.size;
|
2020-08-28 22:06:19 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
2020-10-31 15:54:28 +00:00
|
|
|
//If post processing, wait for it
|
|
|
|
if (this.postPromise) {
|
|
|
|
await this._postPromise;
|
|
|
|
return this._cb();
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Cancel download
|
|
|
|
if (this._response)
|
|
|
|
this._response.destroy();
|
|
|
|
if (this._request)
|
|
|
|
this._request.destroy();
|
|
|
|
|
|
|
|
// this._response = null;
|
|
|
|
// this._request = null;
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
this.stopped = true;
|
|
|
|
this.download.state = 0;
|
|
|
|
this._cb();
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
async _post(tmp) {
|
|
|
|
this.download.state = 2;
|
2020-08-28 22:06:19 +01:00
|
|
|
|
|
|
|
//Decrypt
|
2020-10-31 15:54:28 +00:00
|
|
|
decryptor.decryptFile(decryptor.getKey(this.qualityInfo.trackId), tmp, `${tmp}.DEC`);
|
|
|
|
let outPath = this.generatePath(this.qualityInfo.quality);
|
|
|
|
await fs.promises.mkdir(path.dirname(outPath), {recursive: true});
|
|
|
|
await fs.promises.copyFile(`${tmp}.DEC`, outPath);
|
2020-09-01 19:37:02 +01:00
|
|
|
await fs.promises.unlink(`${tmp}.DEC`);
|
2020-10-31 15:54:28 +00:00
|
|
|
await fs.promises.unlink(tmp);
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
if (!this.isUserUploaded) {
|
|
|
|
//Tag
|
|
|
|
await this.tagTrack(outPath);
|
|
|
|
|
|
|
|
//Lyrics
|
|
|
|
if (this.settings.downloadLyrics) {
|
|
|
|
let lrcFile = outPath.split('.').slice(0, -1).join('.') + '.lrc';
|
|
|
|
let lrc;
|
|
|
|
try {
|
|
|
|
lrc = this.generateLRC();
|
|
|
|
} catch (e) {
|
|
|
|
logger.warn('Error getting lyrics! ' + e);
|
|
|
|
}
|
|
|
|
if (lrc) {
|
|
|
|
await fs.promises.writeFile(lrcFile, lrc, {encoding: 'utf-8'});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
this.download.state = 3;
|
|
|
|
this._cb();
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
async tagTrack(path) {
|
2020-08-28 22:06:19 +01:00
|
|
|
let cover;
|
|
|
|
try {
|
2020-10-31 15:54:28 +00:00
|
|
|
cover = await this.downloadCover(this.track.albumArt.full);
|
2020-08-28 22:06:19 +01:00
|
|
|
} catch (e) {}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//Genre tag
|
|
|
|
let genres = [];
|
|
|
|
if (this.publicAlbum.genres && this.publicAlbum.genres.data)
|
|
|
|
genres = this.publicAlbum.genres.data.map(g => g.name);
|
|
|
|
|
2020-08-28 22:06:19 +01:00
|
|
|
if (path.toLowerCase().endsWith('.mp3')) {
|
|
|
|
//Load
|
|
|
|
const audioData = await fs.promises.readFile(path);
|
|
|
|
const writer = new ID3Writer(audioData);
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
writer.setFrame('TIT2', this.track.title);
|
|
|
|
writer.setFrame('TPE1', this.track.artists.map((a) => a.name));
|
|
|
|
if (this.publicAlbum.artist) writer.setFrame('TPE2', this.publicAlbum.artist.name);
|
|
|
|
writer.setFrame('TALB', this.track.album.title);
|
|
|
|
writer.setFrame('TRCK', this.track.trackNumber);
|
|
|
|
writer.setFrame('TPOS', this.track.diskNumber);
|
|
|
|
writer.setFrame('TCON', genres);
|
|
|
|
let date = new Date(this.publicTrack.release_date);
|
|
|
|
writer.setFrame('TYER', date.getFullYear());
|
|
|
|
writer.setFrame('TDAT', `${date.getMonth().toString().padStart(2, '0')}${date.getDay().toString().padStart(2, '0')}`);
|
|
|
|
if (this.publicTrack.bpm > 2) writer.setFrame('TBPM', this.publicTrack.bpm);
|
|
|
|
if (this.publicAlbum.label) writer.setFrame('TPUB', this.publicAlbum.label);
|
|
|
|
writer.setFrame('TSRC', this.publicTrack.isrc);
|
|
|
|
if (this.rawTrack.results.LYRICS) writer.setFrame('USLT', {
|
|
|
|
lyrics: this.rawTrack.results.LYRICS.LYRICS_TEXT,
|
|
|
|
language: 'eng',
|
|
|
|
description: 'Unsychronised lyrics'
|
|
|
|
});
|
|
|
|
|
|
|
|
if (cover) writer.setFrame('APIC', {type: 3, data: cover, description: 'Cover'});
|
2020-08-28 22:06:19 +01:00
|
|
|
writer.addTag();
|
|
|
|
|
|
|
|
//Write
|
|
|
|
await fs.promises.writeFile(path, Buffer.from(writer.arrayBuffer));
|
2020-10-31 15:54:28 +00:00
|
|
|
return;
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
2020-10-31 15:54:28 +00:00
|
|
|
|
2020-08-28 22:06:19 +01:00
|
|
|
//Tag FLAC
|
|
|
|
if (path.toLowerCase().endsWith('.flac')) {
|
|
|
|
const flac = new Metaflac(path);
|
|
|
|
flac.removeAllTags();
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
flac.setTag(`TITLE=${this.track.title}`);
|
|
|
|
flac.setTag(`ALBUM=${this.track.album.title}`);
|
|
|
|
flac.setTag(`ARTIST=${this.track.artistString}`);
|
|
|
|
flac.setTag(`TRACKNUMBER=${this.track.trackNumber}`);
|
|
|
|
flac.setTag(`DISCNUMBER=${this.track.diskNumber}`);
|
|
|
|
if (this.publicAlbum.artist) flac.setTag(`ALBUMARTIST=${this.publicAlbum.artist.name}`);
|
|
|
|
flac.setTag(`GENRE=${genres.join(", ")}`);
|
|
|
|
flac.setTag(`DATE=${this.publicTrack.release_date}`);
|
|
|
|
if (this.publicTrack.bpm > 2) flac.setTag(`BPM=${this.publicTrack.bpm}`);
|
|
|
|
if (this.publicAlbum.label) flac.setTag(`LABEL=${this.publicAlbum.label}`);
|
|
|
|
flac.setTag(`ISRC=${this.publicTrack.isrc}`);
|
|
|
|
if (this.publicAlbum.upc) flac.setTag(`BARCODE=${this.publicAlbum.upc}`);
|
|
|
|
if (this.rawTrack.results.LYRICS) flac.setTag(`LYRICS=${this.rawTrack.results.LYRICS.LYRICS_TEXT}`);
|
|
|
|
|
2020-08-28 22:06:19 +01:00
|
|
|
if (cover) flac.importPicture(cover);
|
|
|
|
|
|
|
|
flac.save();
|
|
|
|
}
|
2020-10-31 15:54:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async downloadCover(url) {
|
|
|
|
return await new Promise((res) => {
|
|
|
|
let out = Buffer.alloc(0);
|
|
|
|
https.get(url, (r) => {
|
|
|
|
r.on('data', (d) => {
|
|
|
|
out = Buffer.concat([out, d]);
|
|
|
|
});
|
|
|
|
r.on('end', () => {
|
|
|
|
res(out);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
generateLRC() {
|
|
|
|
//Check if exists
|
|
|
|
if (!this.rawTrack.results.LYRICS || !this.rawTrack.results.LYRICS.LYRICS_SYNC_JSON) return;
|
|
|
|
let lyrics = new Lyrics(this.rawTrack.results.LYRICS);
|
|
|
|
if (lyrics.lyrics.length == 0) return;
|
|
|
|
//Metadata
|
|
|
|
let out = `[ar:${this.track.artistString}]\r\n[al:${this.track.album.title}]\r\n[ti:${this.track.title}]\r\n`;
|
|
|
|
//Lyrics
|
|
|
|
for (let l of lyrics.lyrics) {
|
|
|
|
if (l.lrcTimestamp && l.text)
|
|
|
|
out += `${l.lrcTimestamp}${l.text}\r\n`;
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
generatePath(quality) {
|
2020-11-03 09:11:37 +00:00
|
|
|
//Path
|
|
|
|
let folder = this.settings.downloadsPath;
|
|
|
|
if (this.download.path)
|
|
|
|
folder = this.download.path;
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
//User uploaded mp3s
|
|
|
|
if (this.isUserUploaded) {
|
|
|
|
//Generate path
|
|
|
|
if (this.settings.createArtistFolder && this.download.track.artists[0].name.length > 0)
|
2020-11-03 09:11:37 +00:00
|
|
|
folder = path.join(folder, sanitize(this.download.track.artists[0].name));
|
2020-10-31 15:54:28 +00:00
|
|
|
if (this.settings.createAlbumFolder && this.download.track.album.title.length > 0)
|
2020-11-03 09:11:37 +00:00
|
|
|
folder = path.join(folder, sanitize(this.download.track.album.title));
|
2020-10-31 15:54:28 +00:00
|
|
|
//Filename
|
2020-11-03 09:11:37 +00:00
|
|
|
let out = path.join(folder, sanitize(this.download.track.title));
|
2020-10-31 15:54:28 +00:00
|
|
|
if (!out.includes('.'))
|
|
|
|
out += '.mp3';
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Generate filename
|
|
|
|
let fn = this.settings.downloadFilename;
|
|
|
|
|
|
|
|
//Disable feats for single artist
|
|
|
|
let feats = '';
|
|
|
|
if (this.track.artists.length >= 2)
|
|
|
|
feats = this.track.artists.slice(1).map((a) => a.name).join(', ');
|
|
|
|
|
|
|
|
//Date
|
|
|
|
let date = new Date(this.publicTrack.release_date);
|
|
|
|
|
|
|
|
let props = {
|
|
|
|
'%title%': this.track.title,
|
|
|
|
'%artists%': this.track.artistString,
|
|
|
|
'%artist%': this.track.artists[0].name,
|
|
|
|
'%feats%': feats,
|
|
|
|
'%trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString(),
|
|
|
|
'%0trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString().padStart(2, '0'),
|
|
|
|
'%album%': this.track.album.title,
|
|
|
|
'%year%': date.getFullYear().toString(),
|
2020-11-05 21:00:29 +00:00
|
|
|
'%label%': (this.publicAlbum.label) ? this.publicAlbum.label : ''
|
2020-10-31 15:54:28 +00:00
|
|
|
};
|
|
|
|
for (let k of Object.keys(props)) {
|
|
|
|
fn = fn.replace(new RegExp(k, 'g'), sanitize(props[k]));
|
|
|
|
}
|
|
|
|
//Generate folders
|
2020-11-03 09:11:37 +00:00
|
|
|
if (this.settings.createArtistFolder) folder = path.join(folder, sanitize(this.track.artists[0].name));
|
|
|
|
if (this.settings.createAlbumFolder) folder = path.join(folder, sanitize(this.track.album.title));
|
2020-10-31 15:54:28 +00:00
|
|
|
|
|
|
|
//Extension
|
|
|
|
if (quality.toString() == '9') {
|
|
|
|
fn += '.flac';
|
|
|
|
} else {
|
|
|
|
fn += '.mp3';
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-11-03 09:11:37 +00:00
|
|
|
return path.join(folder, fn);
|
2020-08-28 22:06:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
class Download {
|
2020-11-03 09:11:37 +00:00
|
|
|
constructor (track, quality, state, path) {
|
2020-10-31 15:54:28 +00:00
|
|
|
this.track = track;
|
|
|
|
this.quality = quality;
|
2020-11-03 09:11:37 +00:00
|
|
|
this.path = path;
|
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
// 0 - none
|
|
|
|
// 1 - downloading
|
|
|
|
// 2 - postprocess
|
|
|
|
// 3 - done
|
|
|
|
// -1 - error
|
|
|
|
this.state = state;
|
|
|
|
|
|
|
|
//Updated from threads
|
|
|
|
this.downloaded = 0;
|
|
|
|
this.size = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
toDB() {
|
|
|
|
return {
|
|
|
|
_id: this.track.id,
|
|
|
|
track: this.track,
|
|
|
|
quality: this.quality,
|
2020-11-03 09:11:37 +00:00
|
|
|
state: this.state,
|
|
|
|
path: this.path
|
2020-10-31 15:54:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static fromDB(json) {
|
2020-11-03 09:11:37 +00:00
|
|
|
return new Download(json.track, json.quality, json.state, json.path);
|
2020-10-31 15:54:28 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-28 22:06:19 +01:00
|
|
|
|
2020-10-31 15:54:28 +00:00
|
|
|
module.exports = {DownloadManager}
|