Playlist Downloads are now working
[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     var result = await request({
46       uri: this.apiUrl,
47       method: 'POST',
48       qs: {
49         api_version: "1.0",
50         api_token: (method === "deezer.getUserData" ? "null" : await this.getToken()),
51         input: "3",
52         method: method
53       },
54       body: args,
55       jar: this.jar,
56       json: true,
57       headers: this.httpHeaders
58     })
59     return result
60   }
61
62   // Simple function to request data from the legacy API (api.deezer.com)
63   async legacyApiCall(method, args = {}){
64     var result = await request({
65       uri: `${this.legacyApiUrl}${method}`,
66       method: 'GET',
67       qs: args,
68       jar: this.jar,
69       json: true,
70       headers: this.httpHeaders
71     })
72     if (result.error){
73       if (result.error.code == 4){
74         await sleep(500)
75         return await legacyApiCall(method, args)
76       }else{
77         throw new Error(`${result.error.type}: ${result.error.message}`)
78       }
79     }
80     return result
81   }
82
83   // Login function
84   async login(mail, password){
85     try{
86       // The new login page requires a checkFormLogin field
87       // We can get that from the hidden API
88       var checkFormLogin = await this.apiCall("deezer.getUserData")
89       // Now we'll ask to login
90       var login = await request({
91         method: 'POST',
92         url: `https://www.deezer.com/ajax/action.php`,
93         form: {
94           type:'login',
95           mail: mail,
96           password: password,
97           checkFormLogin: checkFormLogin.results.checkFormLogin
98         },
99         headers: {
100           ...this.httpHeaders,
101           'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
102         },
103         jar: this.jar,
104         withCredentials: true
105       })
106       if (!login.includes('success'))
107         throw new Error(`Wrong e-mail or password`)
108       // Next we'll get the user data, so we can display playlists the name, images from the user
109       var userData = await this.apiCall(`deezer.getUserData`)
110       this.user = {
111         email: mail,
112         id: userData.results.USER.USER_ID,
113         name: userData.results.USER.BLOG_NAME,
114         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` : ""
115       }
116       return true
117     } catch(err){
118       throw new Error(`Can't connect to Deezer: ${err.message}`)
119     }
120   }
121
122   // Login via cookie function
123   async loginViaCookies(cookies, email){
124     try{
125       this.setCookies(cookies)
126       var userData = await this.apiCall(`deezer.getUserData`)
127       if (!userData.results.USER.USER_ID) throw new Error('Cookie expired, please login again.')
128       this.user = {
129         email: email,
130         id: userData.results.USER.USER_ID,
131         name: userData.results.USER.BLOG_NAME,
132         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` : ""
133       }
134       return true
135     } catch(err){
136       throw new Error(`Can't connect to Deezer: ${err.message}`)
137     }
138   }
139
140   async getTrack(id, settings = {}){
141     var body
142     if (id<0){
143       body = await this.apiCall(`song.getData`, {sng_id: id})
144       body.sourcePage = 'song.getData'
145       body.type = -1
146     }else{
147       body = await this.apiCall(`deezer.pageTrack`, {sng_id: id})
148       body.sourcePage = 'deezer.pageTrack'
149       body.type = 0
150     }
151     return new Track(body)
152   }
153
154   async getAlbum(id){
155     var body = await this.apiCall(`album.getData`, {alb_id: id})
156     body.sourcePage = 'album.getData'
157     /*
158     Alternative query, currently not used
159       var body = await this.apiCall(`deezer.pageAlbum`, {alb_id: id, lang: 'en'})
160       body.sourcePage = 'deezer.pageAlbum'
161     */
162     return new Album(body)
163   }
164
165   async getAlbumTracks(id){
166     var tracksArray = []
167     var body = await this.apiCall(`song.getListByAlbum`, {alb_id: id, nb: -1})
168     body.results.data.forEach(track=>{
169       track.sourcePage = 'song.getListByAlbum'
170       tracksArray.push(new Track(track))
171     })
172     return tracksArray
173   }
174
175   async getArtist(id){
176     var body = await this.apiCall(`deezer.pageArtist`, {art_id: id})
177     return body
178   }
179
180   async getPlaylist(id){
181     var body = await this.apiCall(`deezer.pagePlaylist`, {playlist_id: id})
182     return body
183   }
184
185   async getPlaylistTracks(id){
186     var tracksArray = []
187     var body = await this.apiCall(`playlist.getSongs`, {playlist_id: id, nb: -1})
188     body.results.data.forEach((track, index)=>{
189       track.sourcePage = 'playlist.getSongs'
190       let _track = new Track(track)
191       _track.position = index
192       tracksArray.push(_track)
193     })
194     return tracksArray
195   }
196
197   async getLyrics(id){
198     var body = await this.apiCall(`song.getLyrics`, {sng_id: id})
199     let lyr
200     lyr.unsyncLyrics = {
201       description: "",
202       lyrics: body.results.LYRICS_TEXT
203     }
204     lyr.syncLyrics = ""
205     for(let i=0; i < body.results.LYRICS_SYNC_JSON.length; i++){
206       if(body.results.LYRICS_SYNC_JSON[i].lrc_timestamp){
207         this.syncLyrics += body.results.LYRICS_SYNC_JSON[i].lrc_timestamp + body.results.LYRICS_SYNC_JSON[i].line+"\r\n";
208       }else if(i+1 < body.results.LYRICS_SYNC_JSON.length){
209         this.syncLyrics += body.results.LYRICS_SYNC_JSON[i+1].lrc_timestamp + body.results.LYRICS_SYNC_JSON[i].line+"\r\n";
210       }
211     }
212     return lyr
213   }
214
215   async legacyGetUserPlaylists(id){
216     var body = await this.legacyApiCall(`user/${id}/playlists`, {limit: -1})
217     return body
218   }
219
220   async legacyGetTrack(id){
221     var body = await this.legacyApiCall(`track/${id}`)
222     return body
223   }
224
225   async legacyGetChartsTopCountry(){
226     return await this.legacyGetUserPlaylists('637006841')
227   }
228
229   async legacyGetPlaylist(id){
230     var body = await this.legacyApiCall(`playlist/${id}`)
231     return body
232   }
233
234   async legacyGetPlaylistTracks(id){
235     var body = await this.legacyApiCall(`playlist/${id}/tracks`, {limit: -1})
236     return body
237   }
238
239   async legacyGetAlbum(id){
240     var body = await this.legacyApiCall(`album/${id}`)
241     return body
242   }
243
244   async legacyGetAlbumTracks(id){
245     var body = await this.legacyApiCall(`album/${id}/tracks`, {limit: -1})
246     return body
247   }
248
249   async legacyGetArtistAlbums(id){
250     var body = await this.legacyApiCall(`artist/${id}/albums`, {limit: -1})
251     return body
252   }
253
254   async legacySearch(term, type){
255     var body = await this.legacyApiCall(`search/${type}`, {q: term})
256     if(body.error) {
257       throw new Error("Wrong search type/text: " + text)
258     }
259     return body
260   }
261
262   decryptDownload(source, trackId) {
263         var chunk_size = 2048
264         var part_size = 0x1800
265         var blowFishKey = getBlowfishKey(trackId)
266         var i = 0
267         var position = 0
268
269         var destBuffer = Buffer.alloc(source.length)
270         destBuffer.fill(0)
271
272         while(position < source.length) {
273                 var chunk
274                 if ((source.length - position) >= 2048)
275                         chunk_size = 2048
276                 else
277                         chunk_size = source.length - position
278                 chunk = Buffer.alloc(chunk_size)
279                 let chunkString
280                 chunk.fill(0)
281                 source.copy(chunk, 0, position, position + chunk_size)
282                 if(i % 3 > 0 || chunk_size < 2048)
283                         chunkString = chunk.toString('binary')
284                 else
285                         chunkString = decryptChunk(chunk, blowFishKey)
286                 destBuffer.write(chunkString, position, chunkString.length, 'binary')
287                 position += chunk_size
288                 i++
289         }
290         return destBuffer
291   }
292 }