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