ef3b9c009f189b7829e8828ebd8d7709f98d9545
[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       body.results.sourcePage = 'song.getData'
154       body.results.type = -1
155     }else{
156       body = await this.apiCall(`deezer.pageTrack`, {sng_id: id})
157       body.results.sourcePage = 'deezer.pageTrack'
158     }
159     return new Track(body.results)
160   }
161
162   async getTracks(ids){
163     var tracksArray = []
164     var body = await this.apiCall(`song.getListData`, {sng_ids: ids})
165     body.results.data.forEach(track=>{
166       track.sourcePage = 'song.getData'
167       tracksArray.push(new Track(track))
168     })
169     return tracksArray
170   }
171
172   async getAlbum(id){
173     var body = await this.apiCall(`album.getData`, {alb_id: id})
174     body.results.sourcePage = 'album.getData'
175     /*
176     Alternative query, currently not used
177       var body = await this.apiCall(`deezer.pageAlbum`, {alb_id: id, lang: 'en'})
178       body.sourcePage = 'deezer.pageAlbum'
179     */
180     return new Album(body.results)
181   }
182
183   async getAlbumTracks(id){
184     var tracksArray = []
185     var body = await this.apiCall(`song.getListByAlbum`, {alb_id: id, nb: -1})
186     body.results.data.forEach(track=>{
187       track.sourcePage = 'song.getListByAlbum'
188       tracksArray.push(new Track(track))
189     })
190     return tracksArray
191   }
192
193   async getArtist(id){
194     var body = await this.apiCall(`deezer.pageArtist`, {art_id: id})
195     return body
196   }
197
198   async getPlaylist(id){
199     var body = await this.apiCall(`deezer.pagePlaylist`, {playlist_id: id})
200     return body
201   }
202
203   async getPlaylistTracks(id){
204     var tracksArray = []
205     var body = await this.apiCall(`playlist.getSongs`, {playlist_id: id, nb: -1})
206     body.results.data.forEach((track, index)=>{
207       track.sourcePage = 'playlist.getSongs'
208       let _track = new Track(track)
209       _track.position = index
210       tracksArray.push(_track)
211     })
212     return tracksArray
213   }
214
215   async getArtistTopTracks(id){
216     var tracksArray = []
217     var body = await this.apiCall(`artist.getTopTrack`, {art_id: id, nb: 100})
218     body.results.data.forEach((track, index)=>{
219       track.sourcePage = 'artist.getTopTrack'
220       let _track = new Track(track)
221       _track.position = index
222       tracksArray.push(_track)
223     })
224     return tracksArray
225   }
226
227   async getLyrics(id){
228     var body = await this.apiCall(`song.getLyrics`, {sng_id: id})
229     var lyr = {}
230                 if (body.results.LYRICS_TEXT){
231                         lyr.unsyncLyrics = {
232                                 description: "",
233                                 lyrics: body.results.LYRICS_TEXT
234                         }
235                 }
236                 if (body.results.LYRICS_SYNC_JSON){
237                         lyr.syncLyrics = ""
238                         for(let i=0; i < body.results.LYRICS_SYNC_JSON.length; i++){
239                                 if(body.results.LYRICS_SYNC_JSON[i].lrc_timestamp){
240                                         lyr.syncLyrics += body.results.LYRICS_SYNC_JSON[i].lrc_timestamp + body.results.LYRICS_SYNC_JSON[i].line+"\r\n";
241                                 }else if(i+1 < body.results.LYRICS_SYNC_JSON.length){
242                                         lyr.syncLyrics += body.results.LYRICS_SYNC_JSON[i+1].lrc_timestamp + body.results.LYRICS_SYNC_JSON[i].line+"\r\n";
243                                 }
244                         }
245                 }
246     return lyr
247   }
248
249   async legacyGetUserPlaylists(id){
250     var body = await this.legacyApiCall(`user/${id}/playlists`, {limit: -1})
251     return body
252   }
253
254   async legacyGetTrack(id){
255     var body = await this.legacyApiCall(`track/${id}`)
256     return body
257   }
258
259   async legacyGetTrackByISRC(isrc){
260     var body = await this.legacyApiCall(`track/isrc:${isrc}`)
261     return body
262   }
263
264   async legacyGetChartsTopCountry(){
265     return await this.legacyGetUserPlaylists('637006841')
266   }
267
268   async legacyGetPlaylist(id){
269     var body = await this.legacyApiCall(`playlist/${id}`)
270     return body
271   }
272
273   async legacyGetPlaylistTracks(id){
274     var body = await this.legacyApiCall(`playlist/${id}/tracks`, {limit: -1})
275     return body
276   }
277
278   async legacyGetAlbum(id){
279     var body = await this.legacyApiCall(`album/${id}`)
280     return body
281   }
282
283   async legacyGetAlbumTracks(id){
284     var body = await this.legacyApiCall(`album/${id}/tracks`, {limit: -1})
285     return body
286   }
287
288   async legacyGetArtistAlbums(id){
289     var body = await this.legacyApiCall(`artist/${id}/albums`, {limit: -1})
290     return body
291   }
292
293   async legacyGetArtist(id){
294     var body = await this.legacyApiCall(`artist/${id}`, {limit: -1})
295     return body
296   }
297
298   async legacySearch(term, type, limit = 30){
299     var body = await this.legacyApiCall(`search/${type}`, {q: term, limit: limit})
300     if(body.error) {
301       throw new Error("Wrong search type/text: " + text)
302     }
303     return body
304   }
305
306   decryptDownload(source, trackId) {
307         var chunk_size = 2048
308         var part_size = 0x1800
309         var blowFishKey = getBlowfishKey(trackId)
310         var i = 0
311         var position = 0
312
313         var destBuffer = Buffer.alloc(source.length)
314         destBuffer.fill(0)
315
316         while(position < source.length) {
317                 var chunk
318                 if ((source.length - position) >= 2048)
319                         chunk_size = 2048
320                 else
321                         chunk_size = source.length - position
322                 chunk = Buffer.alloc(chunk_size)
323                 let chunkString
324                 chunk.fill(0)
325                 source.copy(chunk, 0, position, position + chunk_size)
326                 if(i % 3 > 0 || chunk_size < 2048)
327                         chunkString = chunk.toString('binary')
328                 else
329                         chunkString = decryptChunk(chunk, blowFishKey)
330                 destBuffer.write(chunkString, position, chunkString.length, 'binary')
331                 position += chunk_size
332                 i++
333         }
334         return destBuffer
335   }
336 }