Fixed problems with Deezer getTracks
[DeezloaderRemix.git] / app / lib / deezer-api / index.js
1 const request = require('request-promise')
2 const tough = require('tough-cookie');
3 const Track = require('./obj/Track.js')
4 const Album = require('./obj/Album.js')
5 const getBlowfishKey = require('./utils.js').getBlowfishKey
6 const decryptChunk = require('./utils.js').decryptChunk
7 const sleep = require('./utils.js').sleep
8
9 module.exports = class Deezer {
10   constructor(){
11     this.apiUrl = `http://www.deezer.com/ajax/gw-light.php`
12     this.legacyApiUrl = `https://api.deezer.com/`
13     this.httpHeaders = {
14       "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",
15       "Content-Language": "en-US",
16       "Cache-Control": "max-age=0",
17       "Accept": "*/*",
18       "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
19       "Accept-Language": "en-US,en;q=0.9,en-US;q=0.8,en;q=0.7",
20       "Connection": 'keep-alive'
21     }
22     this.albumPicturesHost = `https://e-cdns-images.dzcdn.net/images/cover/`
23     this.artistPictureHost = `https://e-cdns-images.dzcdn.net/images/artist/`
24     this.user = {}
25     this.jar = request.jar()
26   }
27
28   getCookies(){
29     return this.jar.getCookies("https://www.deezer.com")
30   }
31
32   setCookies(cookies){
33     JSON.parse("{\"a\": "+cookies+"}").a.forEach(x => {
34       this.jar.setCookie(tough.Cookie.fromJSON(x), "https://www.deezer.com")
35     })
36   }
37
38   async getToken(){
39     var tokenData = await this.apiCall('deezer.getUserData')
40     return tokenData.results.checkForm
41   }
42
43   // Simple function to request data from the hidden API (gw-light.php)
44   async apiCall(method, args = {}){
45                 try{
46                         var result = await request({
47                                 uri: this.apiUrl,
48                                 method: 'POST',
49                                 qs: {
50                                         api_version: "1.0",
51                                         api_token: (method === "deezer.getUserData" ? "null" : await this.getToken()),
52                                         input: "3",
53                                         method: method
54                                 },
55                                 body: args,
56                                 jar: this.jar,
57                                 json: true,
58                                 headers: this.httpHeaders
59                         })
60                 }catch (err){
61                         return this.apiCall(method, args)
62                 }
63                 return result
64   }
65
66   // Simple function to request data from the legacy API (api.deezer.com)
67   async legacyApiCall(method, args = {}){
68                 try{
69             var result = await request({
70               uri: `${this.legacyApiUrl}${method}`,
71               method: 'GET',
72               qs: args,
73               jar: this.jar,
74               json: true,
75               headers: this.httpHeaders,
76               timeout: 30000
77             })
78                 }catch (err){
79                         return this.legacyApiCall(method, args)
80                 }
81     if (result.error){
82       if (result.error.code == 4){
83         await sleep(500)
84         return await this.legacyApiCall(method, args)
85       }else{
86         throw new Error(`${result.error.type}: ${result.error.message}`)
87       }
88     }
89     return result
90   }
91
92   // Login function
93   async login(mail, password){
94     try{
95       // The new login page requires a checkFormLogin field
96       // We can get that from the hidden API
97       var checkFormLogin = await this.apiCall("deezer.getUserData")
98       // Now we'll ask to login
99       var login = await request({
100         method: 'POST',
101         url: `https://www.deezer.com/ajax/action.php`,
102         form: {
103           type:'login',
104           mail: mail,
105           password: password,
106           checkFormLogin: checkFormLogin.results.checkFormLogin
107         },
108         headers: {
109           ...this.httpHeaders,
110           'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
111         },
112         jar: this.jar,
113         withCredentials: true
114       })
115       if (!login.includes('success'))
116         throw new Error(`Wrong e-mail or password`)
117       // Next we'll get the user data, so we can display playlists the name, images from the user
118       var userData = await this.apiCall(`deezer.getUserData`)
119       this.user = {
120         email: mail,
121         id: userData.results.USER.USER_ID,
122         name: userData.results.USER.BLOG_NAME,
123         picture: userData.results.USER.USER_PICTURE ? `https://e-cdns-images.dzcdn.net/images/user/${userData.results.USER.USER_PICTURE}/250x250-000000-80-0-0.jpg` : ""
124       }
125       return true
126     } catch(err){
127       throw new Error(`Can't connect to Deezer: ${err.message}`)
128     }
129   }
130
131   // Login via cookie function
132   async loginViaCookies(cookies, email){
133     try{
134       this.setCookies(cookies)
135       var userData = await this.apiCall(`deezer.getUserData`)
136       if (!userData.results.USER.USER_ID) throw new Error('Cookie expired, please login again.')
137       this.user = {
138         email: email,
139         id: userData.results.USER.USER_ID,
140         name: userData.results.USER.BLOG_NAME,
141         picture: userData.results.USER.USER_PICTURE ? `https://e-cdns-images.dzcdn.net/images/user/${userData.results.USER.USER_PICTURE}/250x250-000000-80-0-0.jpg` : ""
142       }
143       return true
144     } catch(err){
145       throw new Error(`Can't connect to Deezer: ${err.message}`)
146     }
147   }
148
149   async getTrack(id){
150     var body
151     if (id<0){
152       body = await this.apiCall(`song.getData`, {sng_id: id})
153     }else{
154       body = await this.apiCall(`deezer.pageTrack`, {sng_id: id})
155                         if (body.results.LYRICS) body.results.DATA.LYRICS = body.results.LYRICS
156       body.results = body.results.DATA
157     }
158     return new Track(body.results)
159   }
160
161   async getTracks(ids){
162     var tracksArray = []
163     var body = await this.apiCall(`song.getListData`, {sng_ids: ids})
164                 var errors = 0
165                 for(var i=0; i<ids.length; i++){
166                         if (ids[i] != 0) {
167                                 tracksArray.push(new Track(body.results.data[i-errors]))
168                         }else{
169                                 errors++
170                                 tracksArray.push({
171                                         id: 0,
172                             title: '',
173                             duration: 0,
174                             MD5: 0,
175                             mediaVersion: 0,
176                             filesize: 0,
177                             album: {id: 0, title: "", picture: ""},
178                             artist: {id: 0, name: ""},
179                             artists: [{id: 0, name: ""}],
180                             recordType: -1,
181                                 })
182                         }
183
184                 }
185     return tracksArray
186   }
187
188   async getAlbum(id){
189     var body = await this.apiCall(`album.getData`, {alb_id: id})
190     /*
191     Alternative query, currently not used
192       var body = await this.apiCall(`deezer.pageAlbum`, {alb_id: id, lang: 'en'})
193                         if (body.results.SONGS) body.results.DATA.SONGS = body.results.SONGS
194                         body.results = body.results.DATA
195     */
196     return new Album(body.results)
197   }
198
199   async getAlbumTracks(id){
200     var tracksArray = []
201     var body = await this.apiCall(`song.getListByAlbum`, {alb_id: id, nb: -1})
202     body.results.data.forEach(track=>{
203       tracksArray.push(new Track(track))
204     })
205     return tracksArray
206   }
207
208   async getArtist(id){
209     var body = await this.apiCall(`deezer.pageArtist`, {art_id: id})
210     return body
211   }
212
213   async getPlaylist(id){
214     var body = await this.apiCall(`deezer.pagePlaylist`, {playlist_id: id})
215     return body
216   }
217
218   async getPlaylistTracks(id){
219     var tracksArray = []
220     var body = await this.apiCall(`playlist.getSongs`, {playlist_id: id, nb: -1})
221     body.results.data.forEach((track, index)=>{
222       let _track = new Track(track)
223       _track.position = index
224       tracksArray.push(_track)
225     })
226     return tracksArray
227   }
228
229   async getArtistTopTracks(id){
230     var tracksArray = []
231     var body = await this.apiCall(`artist.getTopTrack`, {art_id: id, nb: 100})
232     body.results.data.forEach((track, index)=>{
233       let _track = new Track(track)
234       _track.position = index
235       tracksArray.push(_track)
236     })
237     return tracksArray
238   }
239
240   async getLyrics(id){
241     var body = await this.apiCall(`song.getLyrics`, {sng_id: id})
242     var lyr = {}
243                 if (body.results.LYRICS_TEXT){
244                         lyr.unsyncLyrics = {
245                                 description: "",
246                                 lyrics: body.results.LYRICS_TEXT
247                         }
248                 }
249                 if (body.results.LYRICS_SYNC_JSON){
250                         lyr.syncLyrics = ""
251                         for(let i=0; i < body.results.LYRICS_SYNC_JSON.length; i++){
252                                 if(body.results.LYRICS_SYNC_JSON[i].lrc_timestamp){
253                                         lyr.syncLyrics += body.results.LYRICS_SYNC_JSON[i].lrc_timestamp + body.results.LYRICS_SYNC_JSON[i].line+"\r\n";
254                                 }else if(i+1 < body.results.LYRICS_SYNC_JSON.length){
255                                         lyr.syncLyrics += body.results.LYRICS_SYNC_JSON[i+1].lrc_timestamp + body.results.LYRICS_SYNC_JSON[i].line+"\r\n";
256                                 }
257                         }
258                 }
259     return lyr
260   }
261
262   async legacyGetUserPlaylists(id){
263     var body = await this.legacyApiCall(`user/${id}/playlists`, {limit: -1})
264     return body
265   }
266
267   async legacyGetTrack(id){
268     var body = await this.legacyApiCall(`track/${id}`)
269     return body
270   }
271
272   async legacyGetTrackByISRC(isrc){
273     var body = await this.legacyApiCall(`track/isrc:${isrc}`)
274     return body
275   }
276
277   async legacyGetChartsTopCountry(){
278     return await this.legacyGetUserPlaylists('637006841')
279   }
280
281   async legacyGetPlaylist(id){
282     var body = await this.legacyApiCall(`playlist/${id}`)
283     return body
284   }
285
286   async legacyGetPlaylistTracks(id){
287     var body = await this.legacyApiCall(`playlist/${id}/tracks`, {limit: -1})
288     return body
289   }
290
291   async legacyGetAlbum(id){
292     var body = await this.legacyApiCall(`album/${id}`)
293     return body
294   }
295
296   async legacyGetAlbumTracks(id){
297     var body = await this.legacyApiCall(`album/${id}/tracks`, {limit: -1})
298     return body
299   }
300
301   async legacyGetArtistAlbums(id){
302     var body = await this.legacyApiCall(`artist/${id}/albums`, {limit: -1})
303     return body
304   }
305
306   async legacyGetArtist(id){
307     var body = await this.legacyApiCall(`artist/${id}`, {limit: -1})
308     return body
309   }
310
311   async legacySearch(term, type, limit = 30){
312     var body = await this.legacyApiCall(`search/${type}`, {q: term, limit: limit})
313     if(body.error) {
314       throw new Error("Wrong search type/text: " + text)
315     }
316     return body
317   }
318
319   decryptDownload(source, trackId) {
320         var chunk_size = 2048
321         var part_size = 0x1800
322         var blowFishKey = getBlowfishKey(trackId)
323         var i = 0
324         var position = 0
325
326         var destBuffer = Buffer.alloc(source.length)
327         destBuffer.fill(0)
328
329         while(position < source.length) {
330                 var chunk
331                 if ((source.length - position) >= 2048)
332                         chunk_size = 2048
333                 else
334                         chunk_size = source.length - position
335                 chunk = Buffer.alloc(chunk_size)
336                 let chunkString
337                 chunk.fill(0)
338                 source.copy(chunk, 0, position, position + chunk_size)
339                 if(i % 3 > 0 || chunk_size < 2048)
340                         chunkString = chunk.toString('binary')
341                 else
342                         chunkString = decryptChunk(chunk, blowFishKey)
343                 destBuffer.write(chunkString, position, chunkString.length, 'binary')
344                 position += chunk_size
345                 i++
346         }
347         return destBuffer
348   }
349 }