Fixed update notification
[DeezloaderRemix.git] / app / deezer-api.js
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('./utils/logger.js');
5
6 module.exports = new Deezer();
7
8 function Deezer() {
9         this.apiUrl = "http://www.deezer.com/ajax/gw-light.php";
10         this.httpHeaders = {
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",
14                 "Accept": "*/*",
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"
17         }
18         this.albumPicturesHost = "https://e-cdns-images.dzcdn.net/images/cover/";
19         this.reqStream = {}
20         this.delStream = []
21 }
22
23 Deezer.prototype.init = function(username, password, callback) {
24         var self = this;
25         request.post({
26                 url: self.apiUrl,
27                 strictSSL: false,
28                 qs: {
29                         api_version: "1.0",
30                         api_token: "null",
31                         input: "3",
32                         method: 'deezer.getUserData'
33                 },
34                 headers: self.httpHeaders,
35                 jar: true,
36                 json:true,
37         }, function(err, res, body) {
38                 if(body.results.USER.USER_ID !== 0){
39                         // login already done
40                         callback(null, null);
41                         return;
42                 }
43                 request.post({
44                         url: "https://www.deezer.com/ajax/action.php",
45                         headers: this.httpHeaders,
46                         strictSSL: false,
47                         form: {
48                                 type:'login',
49                                 mail:username,
50                                 password:password,
51                                 checkFormLogin: body.results.checkFormLogin
52                         },
53                         jar: true
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){
58                                 request.post({
59                                         url: self.apiUrl,
60                                         strictSSL: false,
61                                         qs: {
62                                                 api_version: "1.0",
63                                                 api_token: "null",
64                                                 input: "3",
65                                                 method: 'deezer.getUserData'
66                                         },
67                                         headers: self.httpHeaders,
68                                         jar: true,
69                                         json:true,
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`;
76                                                 callback(null, null);
77                                         } else {
78                                                 callback(new Error(`Unable to load deezer.com: ${res ? (res.statusCode != 200 ? res.statusCode : "") : ""} ${err ? err.message : ""}`));
79                                         }
80                                 });
81                         }else{
82                                 callback(new Error("Incorrect email or password."));
83                         }
84                 });
85         })
86 }
87
88
89
90 Deezer.prototype.getPlaylist = function(id, callback) {
91         getJSON("https://api.deezer.com/playlist/" + id, function(res, err){
92                 callback(res, err);
93         });
94 }
95
96 Deezer.prototype.getAlbum = function(id, callback) {
97         getJSON("https://api.deezer.com/album/" + id, function(res, err){
98                 callback(res, err);
99         });
100 }
101
102 Deezer.prototype.getAAlbum = function(id, callback) {
103         var self = this;
104         self.getToken().then(data=>{
105                 request.post({
106                         url: self.apiUrl,
107                         headers: self.httpHeaders,
108                         strictSSL: false,
109                         qs: {
110                                 api_version: "1.0",
111                                 input: "3",
112                                 api_token: data,
113                                 method: "album.getData"
114                         },
115                         body: {alb_id:id},
116                         jar: true,
117                         json: true
118                 }, (function (err, res, body) {
119                         if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
120                                 let ajson = {};
121                                 ajson.artist = {}
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
127                                 callback(ajson);
128                         } else {
129                                 callback(null, new Error("Unable to get Album" + id));
130                         }
131                 }).bind(self));
132         })
133 }
134
135 Deezer.prototype.getATrack = function(id, callback) {
136         getJSON("https://api.deezer.com/track/" + id, function(res, err){
137                 callback(res, err);
138         });
139 }
140
141 Deezer.prototype.getArtist = function(id, callback) {
142         getJSON("https://api.deezer.com/artist/" + id, function(res, err){
143                 callback(res, err);
144         });
145
146 }
147
148 Deezer.prototype.getPlaylistTracks = function(id, callback) {
149         getJSON(`https://api.deezer.com/playlist/${id}/tracks?limit=-1`, function(res, err){
150                 callback(res, err);
151         });
152 }
153
154 Deezer.prototype.getAlbumTracks = function(id, callback) {
155         getJSON(`https://api.deezer.com/album/${id}/tracks?limit=-1`, function(res, err){
156                 callback(res, err);
157         });
158 }
159
160 Deezer.prototype.getAdvancedPlaylistTracks = function(id, callback) {
161         var self = this;
162         self.getToken().then(data=>{
163                 request.post({
164                         url: self.apiUrl,
165                         headers: self.httpHeaders,
166                         strictSSL: false,
167                         qs: {
168                                 api_version: "1.0",
169                                 input: "3",
170                                 api_token: data,
171                                 method: "playlist.getSongs"
172                         },
173                         body: {playlist_id:id, nb:-1},
174                         jar: true,
175                         json: true
176                 }, (function (err, res, body) {
177                         if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
178                                 callback(body.results);
179                         } else {
180                                 callback(null, new Error("Unable to get Album" + id));
181                         }
182                 }).bind(self));
183         })
184 }
185
186 Deezer.prototype.getAdvancedAlbumTracks = function(id, callback) {
187         var self = this;
188         self.getToken().then(data=>{
189                 request.post({
190                         url: self.apiUrl,
191                         headers: self.httpHeaders,
192                         strictSSL: false,
193                         qs: {
194                                 api_version: "1.0",
195                                 input: "3",
196                                 api_token: data,
197                                 method: "song.getListByAlbum"
198                         },
199                         body: {alb_id:id,nb:-1},
200                         jar: true,
201                         json: true
202                 }, (function (err, res, body) {
203                         if(!err && res.statusCode == 200 && typeof body.results != 'undefined'){
204                                 callback(body.results);
205                         } else {
206                                 callback(null, new Error("Unable to get Album" + id));
207                         }
208                 }).bind(self));
209         })
210 }
211
212 Deezer.prototype.getArtistAlbums = function(id, callback) {
213         getJSON("https://api.deezer.com/artist/" + id + "/albums?limit=-1", function(res, err){
214                 if(!res.data) {
215                         res.data = [];
216                 }
217                 callback(res, err);
218         });
219 }
220
221 /*
222 **      CHARTS
223 **      From user https://api.deezer.com/user/637006841/playlists?limit=-1
224 */
225 Deezer.prototype.getChartsTopCountry = function(callback) {
226         getJSON("https://api.deezer.com/user/637006841/playlists?limit=-1", function(res, err){
227                 if(!res.data) {
228                         res.data = [];
229                 } else {
230                         //Remove "Loved Tracks"
231                         res.data.shift();
232                 }
233                 callback(res, err);
234         });
235
236 }
237
238 Deezer.prototype.getMePlaylists = function(callback) {
239         getJSON("https://api.deezer.com/user/"+this.userId+"/playlists?limit=-1", function(res, err){
240                 if(!res.data) {
241                         res.data = [];
242                 }
243                 callback(res, err);
244         });
245 }
246
247 Deezer.prototype.getLocalTrack = function(id, callback) {
248         var scopedid = id;
249         var self = this;
250         self.getToken().then(data=>{
251                 request.post({
252                         url: self.apiUrl,
253                         headers: self.httpHeaders,
254                         strictSSL: false,
255                         qs: {
256                                 api_version: "1.0",
257                                 input: "3",
258                                 api_token: data,
259                                 method: "song.getData"
260                         },
261                         body: {sng_id:scopedid},
262                         jar: true,
263                         json: true
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"]));
269                                 callback(json);
270                         } else {
271                                 callback(null, new Error("Unable to get Track " + id));
272                         }
273                 }).bind(self));
274         })
275 }
276
277 Deezer.prototype.getTrack = function(id, maxBitrate, fallbackBitrate, callback) {
278         var scopedid = id;
279         var self = this;
280         self.getToken().then(data=>{
281                 request.post({
282                         url: self.apiUrl,
283                         headers: self.httpHeaders,
284                         strictSSL: false,
285                         qs: {
286                                 api_version: "1.0",
287                                 input: "3",
288                                 api_token: data,
289                                 method: "deezer.pageTrack"
290                         },
291                         body: {sng_id:scopedid},
292                         jar: true,
293                         json: true
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;
300                                 }
301                                 if(json["TOKEN"]) {
302                                         callback(null, new Error("Uploaded Files are currently not supported"));
303                                         return;
304                                 }
305                                 var id = json["SNG_ID"];
306                                 var md5Origin = json["MD5_ORIGIN"];
307                                 var format;
308                                 switch(maxBitrate){
309                                         case "9":
310                                                 format = 9;
311                                                 if (json["FILESIZE_FLAC"]>0) break;
312                                                 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
313                                         case "3":
314                                                 format = 3;
315                                                 if (json["FILESIZE_MP3_320"]>0) break;
316                                                 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
317                                         case "5":
318                                                 format = 5;
319                                                 if (json["FILESIZE_MP3_256"]>0) break;
320                                                 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
321                                         case "1":
322                                                 format = 1;
323                                                 if (json["FILESIZE_MP3_128"]>0) break;
324                                                 if (!fallbackBitrate) return callback(null, new Error("Song not found at desired bitrate."))
325                                         default:
326                                                 format = 8;
327                                 }
328                                 json.format = format;
329                                 var mediaVersion = parseInt(json["MEDIA_VERSION"]);
330                                 json.downloadUrl = self.getDownloadUrl(md5Origin, id, format, mediaVersion);
331                                 callback(json);
332                         } else {
333                                 callback(null, new Error("Unable to get Track " + id));
334                         }
335                 }).bind(self));
336         })
337 }
338
339 Deezer.prototype.search = function(text, type, callback) {
340         if(typeof type === "function") {
341                 callback = type;
342                 type = "";
343         } else {
344                 type += "?";
345         }
346
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);
350                         if(json.error) {
351                                 callback(new Error("Wrong search type/text: " + text));
352                                 return;
353                         }
354                         callback(json);
355                 } else {
356                         callback(new Error("Unable to reach Deezer API"));
357                 }
358         });
359 }
360
361 Deezer.prototype.track2ID = function(artist, track, album, callback, trim=false) {
362         var self = this;
363         artist = artist.replace(/–/g,"-").replace(/’/g, "'");
364         track = track.replace(/–/g,"-").replace(/’/g, "'");
365         if (album) album = album.replace(/–/g,"-").replace(/’/g, "'");
366         if (album){
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);
370                                 if(json.error) {
371                                         if (json.error.code == 4){
372                                                 self.track2ID(artist, track, album, callback, trim);
373                                                 return;
374                                         }else{
375                                                 callback({id:0, name: track, artist: artist}, new Error(json.error.code+" - "+json.error.message));
376                                                 return;
377                                         }
378                                 }
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
382                                         }
383                                         callback({id:json.data[0].id, name: json.data[0].title, artist: json.data[0].artist.name});
384                                 }else {
385                                         if (!trim){
386                                                 if (track.indexOf("(") < track.indexOf(")")){
387                                                         self.track2ID(artist, track.split("(")[0], album, callback, true);
388                                                         return;
389                                                 }else if (track.indexOf(" - ")>0){
390                                                         self.track2ID(artist, track.split(" - ")[0], album, callback, true);
391                                                         return;
392                                                 }else{
393                                                         self.track2ID(artist, track, null, callback, true);
394                                                 }
395                                         }else{
396                                                 self.track2ID(artist, track, null, callback, true);
397                                         }
398                                 }
399                         } else {
400                                 self.track2ID(artist, track, album, callback, trim);
401                                 return;
402                         }
403                 });
404         }else{
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);
408                                 if(json.error) {
409                                         if (json.error.code == 4){
410                                                 self.track2ID(artist, track, null, callback, trim);
411                                                 return;
412                                         }else{
413                                                 callback({id:0, name: track, artist: artist}, new Error(json.error.code+" - "+json.error.message));
414                                                 return;
415                                         }
416                                 }
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
420                                         }
421                                         callback({id:json.data[0].id, name: json.data[0].title, artist: json.data[0].artist.name});
422                                 }else {
423                                         if (!trim){
424                                                 if (track.indexOf("(") < track.indexOf(")")){
425                                                         self.track2ID(artist, track.split("(")[0], null, callback, true);
426                                                         return;
427                                                 }else if (track.indexOf(" - ")>0){
428                                                         self.track2ID(artist, track.split(" - ")[0], null, callback, true);
429                                                         return;
430                                                 }else{
431                                                         callback({id:0, name: track, artist: artist}, new Error("Track not Found"));
432                                                         return;
433                                                 }
434                                         }else{
435                                                 callback({id:0, name: track, artist: artist}, new Error("Track not Found"));
436                                                 return;
437                                         }
438                                 }
439                         } else {
440                                 self.track2ID(artist, track, null, callback, trim);
441                                 return;
442                         }
443                 });
444         }
445 }
446
447 Deezer.prototype.hasTrackAlternative = function(id, callback) {
448         var scopedid = id;
449         var self = this;
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);
453                 var _data;
454                 try{
455                         _data = rexec[1];
456                 }catch(e){
457                         callback(null, new Error("Unable to get Track " + scopedid));
458                 }
459                 if(!err && res.statusCode == 200 && typeof JSON.parse(_data)["DATA"] != 'undefined') {
460                         var json = JSON.parse(_data)["DATA"];
461                         if(json.FALLBACK){
462                                 callback(json.FALLBACK);
463                         }else{
464                                 callback(null, new Error("Unable to get Track " + scopedid));
465                         }
466                 } else {
467                         callback(null, new Error("Unable to get Track " + scopedid));
468                 }
469         }).bind(self));
470 }
471
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();
481 }
482
483 Deezer.prototype.decryptTrack = function(writePath, track, queueId, callback) {
484         var self = this;
485         var chunkLength = 0;
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;}
494                                                 callback();
495                                         });
496                                         if (self.reqStream[queueId]) self.reqStream[queueId].splice(self.reqStream[queueId].indexOf(this),1);
497                                 } else {
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"));
501                                 }
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"));
509                         })
510                 );
511         }else{
512                 logger.error("Decryption aborted");
513                 callback(new Error("aborted"));
514         }
515 }
516
517 function decryptDownload(source, track) {
518         var chunk_size = 2048;
519         var part_size = 0x1800;
520         var blowFishKey = getBlowfishKey(track["SNG_ID"]);
521         var i = 0;
522         var position = 0;
523
524         var destBuffer = new Buffer(source.length);
525         destBuffer.fill(0);
526
527         while(position < source.length) {
528                 var chunk;
529                 if ((source.length - position) >= 2048) {
530                         chunk_size = 2048;
531                 } else {
532                         chunk_size = source.length - position;
533                 }
534                 chunk = new Buffer(chunk_size);
535                 let chunkString
536                 chunk.fill(0);
537                 source.copy(chunk, 0, position, position + chunk_size);
538                 if(i % 3 > 0 || chunk_size < 2048){
539                                 chunkString = chunk.toString('binary')
540                 }else{
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();
544                 }
545                 destBuffer.write(chunkString, position, chunkString.length, 'binary');
546                 position += chunk_size
547                 i++;
548         }
549         return destBuffer;
550 }
551
552
553 function getBlowfishKey(trackInfos) {
554         const SECRET = 'g4el58wc0zvf9na1';
555
556         const idMd5 = crypto.createHash('md5').update(trackInfos.toString(), 'ascii').digest('hex');
557         let bfKey = '';
558
559         for (let i = 0; i < 16; i++) {
560                 bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i));
561         }
562
563         return bfKey;
564 }
565
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();
571                         }
572                         delete this.reqStream[queueId];
573                         this.delStream.push(queueId);
574                         return true;
575                 }
576                 return true;
577         } else {
578                 false;
579         }
580 }
581
582 Deezer.prototype.onDownloadProgress = function(track, progress) {
583         return;
584 }
585
586 Deezer.prototype.getToken = async function(){
587         const res = await request.get({
588                 url: this.apiUrl,
589                 headers: this.httpHeaders,
590                 strictSSL: false,
591                 qs: {
592                         api_version: "1.0",
593                         api_token: "null",
594                         input: "3",
595                         method: 'deezer.getUserData'
596                 },
597                 json: true,
598                 jar: true,
599         })
600         return res.body.results.checkForm;
601 }
602
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"));
607                 } else {
608                         if (body.error) {
609                                 if (body.error.message == "Quota limit exceeded"){
610                                         logger.warn("Quota limit exceeded, retrying in 500ms");
611                                         setTimeout(function(){ getJSON(url, callback); }, 500);
612                                         return;
613                                 }
614                                 callback(null, new Error(body.error.message));
615                                 return;
616                         }
617                         callback(body);
618                 }
619         });
620 }