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