Release 4.0.1
[DeezloaderRemix.git] / app / deezer-api.js
1 const NRrequest = require('request');
2 const request = require('requestretry').defaults({maxAttempts: 2147483647, retryDelay: 1000, timeout: 8000});
3 const crypto = require('crypto');
4 const fs = require("fs-extra");
5 const logger = require('./logger.js');
6
7 module.exports = new Deezer();
8
9 function Deezer() {
10         this.apiUrl = "http://www.deezer.com/ajax/gw-light.php";
11         this.apiQueries = {
12                 api_version: "1.0",
13                 api_token: "null",
14                 input: "3"
15         };
16         this.httpHeaders = {
17                 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36",
18                 "Content-Language": "en-US",
19                 "Cache-Control": "max-age=0",
20                 "Accept": "*/*",
21                 "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
22                 "Accept-Language": "de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4"
23         }
24         this.albumPicturesHost = "https://e-cdns-images.dzcdn.net/images/cover/";
25         this.reqStream = null;
26 }
27
28 Deezer.prototype.init = function(username, password, callback) {
29         var self = this;
30         NRrequest.post({url: "https://www.deezer.com/ajax/action.php", headers: this.httpHeaders, form: {type:'login',mail:username,password:password}, jar: true}, (function(err, res, body) {
31                 if(err || res.statusCode != 200) {
32                         callback(new Error("Unable to load deezer.com"));
33                 }else if(body.indexOf("success") > -1){
34                         request.get({url: "https://www.deezer.com/", headers: this.httpHeaders, jar: true}, (function(err, res, body) {
35                                 if(!err && res.statusCode == 200) {
36                                         var regex = new RegExp(/((?!"api_key\\":\\").*(?=\\"))/g);
37                                         var _token = regex.exec(body);
38                                         if(_token instanceof Array && _token[1]) {
39                                                 self.apiQueries.api_token = _token[1];
40                                                 callback(null, null);
41                                         } else {
42                                                 callback(new Error("Unable to initialize Deezer API"));
43                                         }
44                                 } else {
45                                         callback(new Error("Unable to load deezer.com"));
46                                 }
47                         }).bind(self));
48                 }else{
49                         callback(new Error("Incorrect email or password."));
50                 }
51         }));
52 }
53
54
55
56 Deezer.prototype.getPlaylist = function(id, callback) {
57         getJSON("https://api.deezer.com/playlist/" + id, function(res){
58                 if (!(res instanceof Error)){
59                         callback(res);
60                 } else {
61                         callback(null, res)
62                 }
63         });
64 }
65
66 Deezer.prototype.getAlbum = function(id, callback) {
67         getJSON("https://api.deezer.com/album/" + id, function(res){
68                 if (!(res instanceof Error)){
69                         callback(res);
70                 } else {
71                         callback(null, res)
72                 }
73         });
74 }
75
76 Deezer.prototype.getATrack = function(id, callback) {
77         getJSON("https://api.deezer.com/track/" + id, function(res){
78                 if (!(res instanceof Error)){
79                         callback(res);
80                 } else {
81                         callback(null, res)
82                 }
83         });
84 }
85
86 Deezer.prototype.getArtist = function(id, callback) {
87         getJSON("https://api.deezer.com/artist/" + id, function(res){
88                 if (!(res instanceof Error)){
89                         callback(res);
90                 } else {
91                         callback(null, res)
92                 }
93         });
94
95 }
96
97 Deezer.prototype.getPlaylistSize = function(id, callback) {
98         getJSON("https://api.deezer.com/playlist/" + id + "/tracks?limit=1", function(res){
99                 if (!(res instanceof Error)){
100                         callback(res.total);
101                 } else {
102                         callback(null, res)
103                 }
104         });
105
106 }
107
108 Deezer.prototype.getPlaylistTracks = function(id, callback) {
109         getJSON("https://api.deezer.com/playlist/" + id + "/tracks?limit=-1", function(res){
110                 if (!(res instanceof Error)){
111                         callback(res)
112                 } else {
113                         callback(null, res)
114                 }
115         });
116 }
117
118 Deezer.prototype.getAlbumSize = function(id, callback) {
119         getJSON("https://api.deezer.com/album/" + id + "/tracks?limit=1", function(res){
120                 if (!(res instanceof Error)){
121                         callback(res.total);
122                 } else {
123                         callback(null, res)
124                 }
125         });
126
127 }
128
129 Deezer.prototype.getAlbumTracks = function(id, callback) {
130         getJSON("https://api.deezer.com/album/" + id + "/tracks?limit=-1", function(res){
131                 if (!(res instanceof Error)){
132                         callback(res);
133                 } else {
134                         callback(null, res)
135                 }
136
137         });
138 }
139
140 Deezer.prototype.getArtistAlbums = function(id, callback) {
141         getJSON("https://api.deezer.com/artist/" + id + "/albums?limit=-1", function(res){
142                 if (!(res instanceof Error)){
143                         if(!res.data) {
144                                 res.data = [];
145                         }
146                         callback(res);
147                 } else {
148                         callback(null, res)
149                 }
150         });
151 }
152
153 /*
154 **      CHARTS
155 **      From user https://api.deezer.com/user/637006841/playlists?limit=-1
156 */
157 Deezer.prototype.getChartsTopCountry = function(callback) {
158         getJSON("https://api.deezer.com/user/637006841/playlists?limit=-1", function(res){
159                 if (!(res instanceof Error)){
160                         if(!res.data) {
161                                 res.data = [];
162                         } else {
163                                 //Remove "Loved Tracks"
164                                 res.data.shift();
165                         }
166                         callback(res);
167                 } else {
168                         callback(null, res)
169                 }
170         });
171
172 }
173
174 Deezer.prototype.getTrack = function(id, wantFlac, callback) {
175         var scopedid = id;
176         var self = this;
177         request.get({url: "https://www.deezer.com/track/"+id, headers: this.httpHeaders, jar: true}, (function(err, res, body) {
178                 var regex = new RegExp(/<script>window\.__DZR_APP_STATE__ = (.*)<\/script>/g);
179                 var rexec = regex.exec(body);
180                 var _data;
181                 try{
182                         _data = rexec[1];
183                 }catch(e){
184                         callback(new Error("Unable to get Track"));
185                         return;
186                 }
187                 if(!err && res.statusCode == 200 && typeof JSON.parse(_data)["DATA"] != 'undefined') {
188                         var json = JSON.parse(_data)["DATA"];
189                         var lyrics = JSON.parse(_data)["LYRICS"];
190                         if(lyrics){
191                                 json["LYRICS_TEXT"] = lyrics["LYRICS_TEXT"];
192                                 json["LYRICS_SYNC_JSON"] = lyrics["LYRICS_SYNC_JSON"];
193                                 json["LYRICS_COPYRIGHTS"] = lyrics["LYRICS_COPYRIGHTS"];
194                                 json["LYRICS_WRITERS"] = lyrics["LYRICS_WRITERS"];
195                         }
196                         if(json["TOKEN"]) {
197                                 callback(new Error("Uploaded Files are currently not supported"));
198                                 return;
199                         }
200                         var id = json["SNG_ID"];
201                         var md5Origin = json["MD5_ORIGIN"];
202                         var format;
203                         if(wantFlac && json["FILESIZE_FLAC"] > 0){
204                                 format = 9;
205                         }else{
206                                 format = 3;
207                                 if(json["FILESIZE_MP3_320"] <= 0) {
208                                         if(json["FILESIZE_MP3_256"] > 0) {
209                                                 format = 5;
210                                         } else {
211                                                 format = 1;
212                                         }
213                                 }
214                         }
215                         json.format = format;
216                         var mediaVersion = parseInt(json["MEDIA_VERSION"]);
217                         json.downloadUrl = self.getDownloadUrl(md5Origin, id, format, mediaVersion);
218                         self.getATrack(id,function(trckjson, err){
219                                 if (err)
220                                         json["BPM"] = 0;
221                                 else
222                                         json["BPM"] = trckjson["bpm"];
223                                 callback(json);
224                         });
225                 } else {
226                         callback(new Error("Unable to get Track " + id));
227                 }
228         }).bind(self));
229 }
230
231 Deezer.prototype.search = function(text, type, callback) {
232         if(typeof type === "function") {
233                 callback = type;
234                 type = "";
235         } else {
236                 type += "?";
237         }
238         request.get({url: "https://api.deezer.com/search/" + type + "q=" + text, headers: this.httpHeaders, jar: true}, function(err, res, body) {
239                 if(!err && res.statusCode == 200) {
240                         var json = JSON.parse(body);
241                         if(json.error) {
242                                 callback(null, new Error("Wrong search type/text: " + text));
243                                 return;
244                         }
245                         callback(json);
246                 } else {
247                         callback(null, new Error("Unable to reach Deezer API"));
248                 }
249         });
250 }
251
252 Deezer.prototype.track2ID = function(artist, track, callback, trim=false) {
253         var self = this;
254         request.get({url: 'https://api.deezer.com/search/?q=track:"'+encodeURIComponent(track)+'" artist:"'+encodeURIComponent(artist)+'"&limit=1', headers: this.httpHeaders, jar: true}, function(err, res, body) {
255                 if(!err && res.statusCode == 200) {
256                         var json = JSON.parse(body);
257                         if(json.error) {
258                                 if (json.error.code == 4){
259                                         self.track2ID(artist, track, callback, trim);
260                                         return;
261                                 }else{
262                                         callback(0, new Error(json.error.code+" - "+json.error.message));
263                                         return;
264                                 }
265                         }
266                         if (json.total>0){
267                                 callback(json.data[0].id);
268                         }else {
269                                 if (!trim){
270                                         if (track.indexOf("(") < track.indexOf(")")){
271                                                 self.track2ID(artist, track.split("(")[0], callback, true);
272                                                 return;
273                                         }else if (track.indexOf(" - ")>0){
274                                                 self.track2ID(artist, track.split(" - ")[0], callback, true);
275                                                 return;
276                                         }else{
277                                                 callback(0, new Error("Track not Found"));
278                                                 return;
279                                         }
280                                 }else{
281                                         callback(0, new Error("Track not Found"));
282                                         return;
283                                 }
284                         }
285                 } else {
286                         self.track2ID(artist, track, callback, trim);
287                         return;
288                 }
289         });
290 }
291
292 Deezer.prototype.hasTrackAlternative = function(id, callback) {
293         var scopedid = id;
294         var self = this;
295         request.get({url: "https://www.deezer.com/track/"+id, headers: this.httpHeaders, jar: true}, (function(err, res, body) {
296                 var regex = new RegExp(/<script>window\.__DZR_APP_STATE__ = (.*)<\/script>/g);
297                 var rexec = regex.exec(body);
298                 var _data;
299                 try{
300                         _data = rexec[1];
301                 }catch(e){
302                         callback(null, new Error("Unable to get Track " + scopedid));
303                 }
304                 if(!err && res.statusCode == 200 && typeof JSON.parse(_data)["DATA"] != 'undefined') {
305                         var json = JSON.parse(_data)["DATA"];
306                         if(json.FALLBACK){
307                                 callback(json.FALLBACK);
308                         }else{
309                                 callback(null, new Error("Unable to get Track " + scopedid));
310                         }
311                 } else {
312                         callback(null, new Error("Unable to get Track " + scopedid));
313                 }
314         }).bind(self));
315 }
316
317 Deezer.prototype.getDownloadUrl = function(md5Origin, id, format, mediaVersion) {
318
319         var urlPart = md5Origin + "¤" + format + "¤" + id + "¤" + mediaVersion;
320         var md5sum = crypto.createHash('md5');
321         md5sum.update(new Buffer(urlPart, 'binary'));
322         md5val = md5sum.digest('hex');
323         urlPart = md5val + "¤" + urlPart + "¤";
324         var cipher = crypto.createCipheriv("aes-128-ecb", new Buffer("jo6aey6haid2Teih"), new Buffer(""));
325         var buffer = Buffer.concat([cipher.update(urlPart, 'binary'), cipher.final()]);
326         return "https://e-cdns-proxy-" + md5Origin.substring(0, 1) + ".dzcdn.net/mobile/1/" + buffer.toString("hex").toLowerCase();
327 }
328
329 Deezer.prototype.decryptTrack = function(writePath, track, callback) {
330         var self = this;
331         var chunkLength = 0;
332         this.reqStream = request.get({url: track.downloadUrl, headers: this.httpHeaders, jar: true, encoding: null}, function(err, res, body) {
333                 if(!err && res.statusCode == 200) {
334                         var decryptedSource = decryptDownload(new Buffer(body, 'binary'), track);
335                         fs.outputFile(writePath,decryptedSource,function(err){
336                                 if(err){callback(err);return;}
337                                 callback();
338                         });
339                 } else {
340                         logger.logs("Error","Decryption error");
341                         callback(err || new Error("Can't download the track"));
342                 }
343         }).on("data", function(data) {
344                 chunkLength += data.length;
345                 self.onDownloadProgress(track, chunkLength);
346         }).on("abort", function() {
347                 logger.logs("Error","Decryption aborted");
348                 callback(new Error("aborted"));
349         });
350 }
351
352 function decryptDownload(source, track) {
353         var chunk_size = 2048;
354         var part_size = 0x1800;
355         var blowFishKey = getBlowfishKey(track["SNG_ID"]);
356         var i = 0;
357         var position = 0;
358
359         var destBuffer = new Buffer(source.length);
360         destBuffer.fill(0);
361
362         while(position < source.length) {
363                 var chunk;
364                 if ((source.length - position) >= 2048) {
365                         chunk_size = 2048;
366                 } else {
367                         chunk_size = source.length - position;
368                 }
369                 chunk = new Buffer(chunk_size);
370                 chunk.fill(0);
371                 source.copy(chunk, 0, position, position + chunk_size);
372                 if(i % 3 > 0 || chunk_size < 2048){
373                         //Do nothing
374                 }else{
375                         var cipher = crypto.createDecipheriv('bf-cbc', blowFishKey, new Buffer([0, 1, 2, 3, 4, 5, 6, 7]));
376                         cipher.setAutoPadding(false);
377                         chunk = cipher.update(chunk, 'binary', 'binary') + cipher.final();
378                 }
379                 destBuffer.write(chunk.toString("binary"), position, 'binary');
380                 position += chunk_size
381                 i++;
382         }
383         return destBuffer;
384 }
385
386
387 function getBlowfishKey(trackInfos) {
388         const SECRET = 'g4el58wc0zvf9na1';
389
390         const idMd5 = crypto.createHash('md5').update(trackInfos.toString(), 'ascii').digest('hex');
391         let bfKey = '';
392
393         for (let i = 0; i < 16; i++) {
394                 bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i));
395         }
396
397         return bfKey;
398 }
399
400 Deezer.prototype.cancelDecryptTrack = function() {
401         if(this.reqStream) {
402                 this.reqStream.abort();
403                 this.reqStream = null;
404                 return true;
405         } else {
406                 false;
407         }
408 }
409
410 Deezer.prototype.onDownloadProgress = function(track, progress) {
411         return;
412 }
413
414 function getJSON(url, callback){
415         request.get({url: url, headers: this.httpHeaders, jar: true}, function(err, res, body) {
416                 if(err || res.statusCode != 200 || !body) {
417                         logger.logs("Error","Unable to initialize Deezer API");
418                         callback(new Error());
419                 } else {
420                         var json = JSON.parse(body);
421                         if (json.error) {
422                                 logger.logs("Error","Wrong id");
423                                 callback(new Error());
424                                 return;
425                         }
426                         callback(json);
427                 }
428         });
429 }