1 const request = require('requestretry').defaults({maxAttempts: 2147483647, retryDelay: 1000, timeout: 8000});
2 const crypto = require('crypto');
3 const fs = require("fs-extra");
4 const logger = require('./logger.js');
6 module.exports = new Deezer();
9 this.apiUrl = "http://www.deezer.com/ajax/gw-light.php";
11 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
12 "Content-Language": "en-US",
13 "Cache-Control": "max-age=0",
15 "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
16 "Accept-Language": "en-US,en;q=0.9,en-US;q=0.8,en;q=0.7"
18 this.albumPicturesHost = "https://e-cdns-images.dzcdn.net/images/cover/";
23 Deezer.prototype.init = function(username, password, callback) {
32 method: 'deezer.getUserData'
34 headers: self.httpHeaders,
37 }, function(err, res, body) {
38 if(body.results.USER.USER_ID !== 0){
44 url: "https://www.deezer.com/ajax/action.php",
45 headers: this.httpHeaders,
51 checkFormLogin: body.results.checkFormLogin
54 }, function(err, res, body) {
55 if(err || res.statusCode != 200) {
56 callback(new Error(`Unable to load deezer.com: ${res ? (res.statusCode != 200 ? res.statusCode : "") : ""} ${err ? err.message : ""}`));
57 }else if(body.indexOf("success") > -1){
65 method: 'deezer.getUserData'
67 headers: self.httpHeaders,
70 }, function(err, res, body) {
71 if(!err && res.statusCode == 200) {
72 const user = body.results.USER;
73 self.userId = user.USER_ID;
74 self.userName = user.BLOG_NAME;
75 self.userPicture = `https:\/\/e-cdns-images.dzcdn.net\/images\/user\/${user.USER_PICTURE}\/250x250-000000-80-0-0.jpg`;
78 callback(new Error(`Unable to load deezer.com: ${res ? (res.statusCode != 200 ? res.statusCode : "") : ""} ${err ? err.message : ""}`));
82 callback(new Error("Incorrect email or password."));
90 Deezer.prototype.getPlaylist = function(id, callback) {
91 getJSON("https://api.deezer.com/playlist/" + id, function(res, err){
96 Deezer.prototype.getAlbum = function(id, callback) {
97 getJSON("https://api.deezer.com/album/" + id, function(res, err){
102 Deezer.prototype.getAAlbum = function(id, callback) {
104 self.getToken().then(data=>{
107 headers: self.httpHeaders,
113 method: "album.getData"
118 }, (function (err, res, body) {
119 if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
122 ajson.artist.name = body.results.ART_NAME
123 ajson.nb_tracks = body.results.NUMBER_TRACK
124 ajson.label = body.results.LABEL_NAME
125 ajson.release_date = body.results.PHYSICAL_RELEASE_DATE
126 ajson.totalDiskNumber = body.results.NUMBER_DISK
129 callback(null, new Error("Unable to get Album" + id));
135 Deezer.prototype.getATrack = function(id, callback) {
136 getJSON("https://api.deezer.com/track/" + id, function(res, err){
141 Deezer.prototype.getArtist = function(id, callback) {
142 getJSON("https://api.deezer.com/artist/" + id, function(res, err){
148 Deezer.prototype.getPlaylistTracks = function(id, callback) {
149 getJSON(`https://api.deezer.com/playlist/${id}/tracks?limit=-1`, function(res, err){
154 Deezer.prototype.getAlbumTracks = function(id, callback) {
155 getJSON(`https://api.deezer.com/album/${id}/tracks?limit=-1`, function(res, err){
160 Deezer.prototype.getAdvancedPlaylistTracks = function(id, callback) {
162 self.getToken().then(data=>{
165 headers: self.httpHeaders,
171 method: "playlist.getSongs"
173 body: {playlist_id:id, nb:-1},
176 }, (function (err, res, body) {
177 if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
178 callback(body.results);
180 callback(null, new Error("Unable to get Album" + id));
186 Deezer.prototype.getAdvancedAlbumTracks = function(id, callback) {
188 self.getToken().then(data=>{
191 headers: self.httpHeaders,
197 method: "song.getListByAlbum"
199 body: {alb_id:id,nb:-1},
202 }, (function (err, res, body) {
203 if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
204 callback(body.results);
206 callback(null, new Error("Unable to get Album" + id));
212 Deezer.prototype.getArtistAlbums = function(id, callback) {
213 getJSON("https://api.deezer.com/artist/" + id + "/albums?limit=-1", function(res, err){
223 ** From user https://api.deezer.com/user/637006841/playlists?limit=-1
225 Deezer.prototype.getChartsTopCountry = function(callback) {
226 getJSON("https://api.deezer.com/user/637006841/playlists?limit=-1", function(res, err){
230 //Remove "Loved Tracks"
238 Deezer.prototype.getMePlaylists = function(callback) {
239 getJSON("https://api.deezer.com/user/"+this.userId+"/playlists?limit=-1", function(res, err){
247 Deezer.prototype.getLocalTrack = function(id, callback) {
250 self.getToken().then(data=>{
253 headers: self.httpHeaders,
259 method: "song.getData"
261 body: {sng_id:scopedid},
264 }, (function (err, res, body) {
265 if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
266 var json = body.results;
267 json.format = (json["MD5_ORIGIN"].split('.').pop() == "flac" ? "9" : "3");
268 json.downloadUrl = self.getDownloadUrl(json["MD5_ORIGIN"], json["SNG_ID"], 0 ,parseInt(json["MEDIA_VERSION"]));
271 callback(null, new Error("Unable to get Track " + id));
277 Deezer.prototype.getTrack = function(id, maxBitrate, fallbackBitrate, callback) {
280 self.getToken().then(data=>{
283 headers: self.httpHeaders,
289 method: "deezer.pageTrack"
291 body: {sng_id:scopedid},
294 }, (function (err, res, body) {
295 if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
296 var json = body.results.DATA;
297 if (body.results.LYRICS){
298 json.LYRICS_SYNC_JSON = body.results.LYRICS.LYRICS_SYNC_JSON;
299 json.LYRICS_TEXT = body.results.LYRICS.LYRICS_TEXT;
302 callback(null, new Error("Uploaded Files are currently not supported"));
305 var id = json["SNG_ID"];
306 var md5Origin = json["MD5_ORIGIN"];
311 if (json["FILESIZE_FLAC"]>0) break;
312 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
315 if (json["FILESIZE_MP3_320"]>0) break;
316 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
319 if (json["FILESIZE_MP3_256"]>0) break;
320 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
323 if (json["FILESIZE_MP3_128"]>0) break;
324 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
328 json.format = format;
329 var mediaVersion = parseInt(json["MEDIA_VERSION"]);
330 json.downloadUrl = self.getDownloadUrl(md5Origin, id, format, mediaVersion);
333 callback(null, new Error("Unable to get Track " + id));
339 Deezer.prototype.search = function(text, type, callback) {
340 if(typeof type === "function") {
347 request.get({url: "https://api.deezer.com/search/" + type + "q=" + text, strictSSL: false, headers: this.httpHeaders, jar: true}, function(err, res, body) {
348 if(!err && res.statusCode == 200) {
349 var json = JSON.parse(body);
351 callback(new Error("Wrong search type/text: " + text));
356 callback(new Error("Unable to reach Deezer API"));
361 Deezer.prototype.track2ID = function(artist, track, album, callback, trim=false) {
363 artist = artist.replace(/–/g,"-").replace(/’/g, "'");
364 track = track.replace(/–/g,"-").replace(/’/g, "'");
365 if (album) album = album.replace(/–/g,"-").replace(/’/g, "'");
367 request.get({url: 'https://api.deezer.com/search/?q=track:"'+encodeURIComponent(track)+'" artist:"'+encodeURIComponent(artist)+'" album:"'+encodeURIComponent(album)+'"&limit=1&strict=on', strictSSL: false, headers: this.httpHeaders, jar: true}, function(err, res, body) {
368 if(!err && res.statusCode == 200) {
369 var json = JSON.parse(body);
371 if (json.error.code == 4){
372 self.track2ID(artist, track, album, callback, trim);
375 callback({id:0, name: track, artist: artist}, new Error(json.error.code+" - "+json.error.message));
379 if (json.data && json.data[0]){
380 if (json.data[0].title_version && json.data[0].title.indexOf(json.data[0].title_version) == -1){
381 json.data[0].title += " "+json.data[0].title_version
383 callback({id:json.data[0].id, name: json.data[0].title, artist: json.data[0].artist.name});
386 if (track.indexOf("(") < track.indexOf(")")){
387 self.track2ID(artist, track.split("(")[0], album, callback, true);
389 }else if (track.indexOf(" - ")>0){
390 self.track2ID(artist, track.split(" - ")[0], album, callback, true);
393 self.track2ID(artist, track, null, callback, true);
396 self.track2ID(artist, track, null, callback, true);
400 self.track2ID(artist, track, album, callback, trim);
405 request.get({url: 'https://api.deezer.com/search/?q=track:"'+encodeURIComponent(track)+'" artist:"'+encodeURIComponent(artist)+'"&limit=1&strict=on', strictSSL: false, headers: this.httpHeaders, jar: true}, function(err, res, body) {
406 if(!err && res.statusCode == 200) {
407 var json = JSON.parse(body);
409 if (json.error.code == 4){
410 self.track2ID(artist, track, null, callback, trim);
413 callback({id:0, name: track, artist: artist}, new Error(json.error.code+" - "+json.error.message));
417 if (json.data && json.data[0]){
418 if (json.data[0].title_version && json.data[0].title.indexOf(json.data[0].title_version) == -1){
419 json.data[0].title += " "+json.data[0].title_version
421 callback({id:json.data[0].id, name: json.data[0].title, artist: json.data[0].artist.name});
424 if (track.indexOf("(") < track.indexOf(")")){
425 self.track2ID(artist, track.split("(")[0], null, callback, true);
427 }else if (track.indexOf(" - ")>0){
428 self.track2ID(artist, track.split(" - ")[0], null, callback, true);
431 callback({id:0, name: track, artist: artist}, new Error("Track not Found"));
435 callback({id:0, name: track, artist: artist}, new Error("Track not Found"));
440 self.track2ID(artist, track, null, callback, trim);
447 Deezer.prototype.hasTrackAlternative = function(id, callback) {
450 request.get({url: "https://www.deezer.com/track/"+id,strictSSL: false, headers: this.httpHeaders, jar: true}, (function(err, res, body) {
451 var regex = new RegExp(/<script>window\.__DZR_APP_STATE__ = (.*)<\/script>/g);
452 var rexec = regex.exec(body);
457 callback(null, new Error("Unable to get Track " + scopedid));
459 if(!err && res.statusCode == 200 && typeof JSON.parse(_data)["DATA"] != 'undefined') {
460 var json = JSON.parse(_data)["DATA"];
462 callback(json.FALLBACK);
464 callback(null, new Error("Unable to get Track " + scopedid));
467 callback(null, new Error("Unable to get Track " + scopedid));
472 Deezer.prototype.getDownloadUrl = function(md5Origin, id, format, mediaVersion) {
473 var urlPart = md5Origin + "¤" + format + "¤" + id + "¤" + mediaVersion;
474 var md5sum = crypto.createHash('md5');
475 md5sum.update(new Buffer(urlPart, 'binary'));
476 md5val = md5sum.digest('hex');
477 urlPart = md5val + "¤" + urlPart + "¤";
478 var cipher = crypto.createCipheriv("aes-128-ecb", new Buffer("jo6aey6haid2Teih"), new Buffer(""));
479 var buffer = Buffer.concat([cipher.update(urlPart, 'binary'), cipher.final()]);
480 return "https://e-cdns-proxy-" + md5Origin.substring(0, 1) + ".dzcdn.net/mobile/1/" + buffer.toString("hex").toLowerCase();
483 Deezer.prototype.decryptTrack = function(writePath, track, queueId, callback) {
486 if (self.delStream.indexOf(queueId) == -1){
487 if (typeof self.reqStream[queueId] != "object") self.reqStream[queueId] = [];
488 self.reqStream[queueId].push(
489 request.get({url: track.downloadUrl,strictSSL: false, headers: self.httpHeaders, encoding: 'binary'}, function(err, res, body) {
490 if(!err && res.statusCode == 200) {
491 var decryptedSource = decryptDownload(new Buffer(body, 'binary'), track);
492 fs.outputFile(writePath,decryptedSource,function(err){
493 if(err){callback(err);return;}
496 if (self.reqStream[queueId]) self.reqStream[queueId].splice(self.reqStream[queueId].indexOf(this),1);
498 logger.error("Decryption error"+(err ? " | "+err : "")+ (res ? ": "+res.statusCode : ""));
499 if (self.reqStream[queueId]) self.reqStream[queueId].splice(self.reqStream[queueId].indexOf(this),1);
500 callback(err || new Error("Can't download the track"));
502 }).on("data", function(data) {
503 chunkLength += data.length;
504 self.onDownloadProgress(track, chunkLength);
505 }).on("abort", function() {
506 logger.error("Decryption aborted");
507 if (self.reqStream[queueId]) self.reqStream[queueId].splice(self.reqStream[queueId].indexOf(this),1);
508 callback(new Error("aborted"));
512 logger.error("Decryption aborted");
513 callback(new Error("aborted"));
517 function decryptDownload(source, track) {
518 var chunk_size = 2048;
519 var part_size = 0x1800;
520 var blowFishKey = getBlowfishKey(track["SNG_ID"]);
524 var destBuffer = new Buffer(source.length);
527 while(position < source.length) {
529 if ((source.length - position) >= 2048) {
532 chunk_size = source.length - position;
534 chunk = new Buffer(chunk_size);
537 source.copy(chunk, 0, position, position + chunk_size);
538 if(i % 3 > 0 || chunk_size < 2048){
539 chunkString = chunk.toString('binary')
541 var cipher = crypto.createDecipheriv('bf-cbc', blowFishKey, new Buffer([0, 1, 2, 3, 4, 5, 6, 7]));
542 cipher.setAutoPadding(false);
543 chunkString = cipher.update(chunk, 'binary', 'binary') + cipher.final();
545 destBuffer.write(chunkString, position, chunkString.length, 'binary');
546 position += chunk_size
553 function getBlowfishKey(trackInfos) {
554 const SECRET = 'g4el58wc0zvf9na1';
556 const idMd5 = crypto.createHash('md5').update(trackInfos.toString(), 'ascii').digest('hex');
559 for (let i = 0; i < 16; i++) {
560 bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i));
566 Deezer.prototype.cancelDecryptTrack = function(queueId) {
567 if(Object.keys(this.reqStream).length != 0) {
568 if (this.reqStream[queueId]){
569 while (this.reqStream[queueId][0]){
570 this.reqStream[queueId][0].abort();
572 delete this.reqStream[queueId];
573 this.delStream.push(queueId);
582 Deezer.prototype.onDownloadProgress = function(track, progress) {
586 Deezer.prototype.getToken = async function(){
587 const res = await request.get({
589 headers: this.httpHeaders,
595 method: 'deezer.getUserData'
600 return res.body.results.checkForm;
603 function getJSON(url, callback){
604 request.get({url: url, headers: this.httpHeaders, strictSSL: false, jar: true, json: true}, function(err, res, body) {
605 if(err || res.statusCode != 200 || !body) {
606 callback(null, new Error("Unable to initialize Deezer API"));
609 if (body.error.message == "Quota limit exceeded"){
610 logger.warn("Quota limit exceeded, retrying in 500ms");
611 setTimeout(function(){ getJSON(url, callback); }, 500);
614 callback(null, new Error(body.error.message));