deezer-js/deezer/gw.js
2021-12-23 23:21:37 +01:00

479 lines
13 KiB
JavaScript

const got = require('got')
const {map_artist_album, map_user_track, map_user_artist, map_user_album, map_user_playlist} = require('./utils.js')
const { GWAPIError } = require('./errors.js')
const PlaylistStatus = {
PUBLIC: 0,
PRIVATE: 1,
COLLABORATIVE: 2,
}
const EMPTY_TRACK_OBJ = {
SNG_ID: 0,
SNG_TITLE: '',
DURATION: 0,
MD5_ORIGIN: 0,
MEDIA_VERSION: 0,
FILESIZE: 0,
ALB_TITLE: "",
ALB_PICTURE: "",
ART_ID: 0,
ART_NAME: ""
}
class GW{
constructor(cookie_jar, headers){
this.http_headers = headers
this.cookie_jar = cookie_jar
this.api_token = null
}
async api_call(method, args, params){
if (typeof args === undefined) args = {}
if (typeof params === undefined) params = {}
if (!this.api_token && method != 'deezer.getUserData') this.api_token = await this._get_token()
let p = {
api_version: "1.0",
api_token: method == 'deezer.getUserData' ? 'null' : this.api_token,
input: '3',
method: method,
...params
}
let result_json
try{
result_json = await got.post("http://www.deezer.com/ajax/gw-light.php", {
searchParams: p,
json: args,
cookieJar: this.cookie_jar,
headers: this.http_headers,
https: {
rejectUnauthorized: false
},
timeout: 30000
}).json()
}catch (e){
console.debug("[ERROR] deezer.gw", method, args, e.name, e.message)
if (["ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "ENETRESET", "ETIMEDOUT"].includes(e.code)){
await new Promise(r => setTimeout(r, 2000)) // sleep(2000ms)
return this.api_call(method, args, params)
}
throw new GWAPIError(`${method} ${args}:: ${e.name}: ${e.message}`)
}
if (result_json.error.length || Object.keys(result_json.error).length) {
if (
JSON.stringify(result_json.error) == '{"GATEWAY_ERROR":"invalid api token"}' ||
JSON.stringify(result_json.error) == '{"VALID_TOKEN_REQUIRED":"Invalid CSRF token"}'
){
this.api_token = await this._get_token()
return this.api_call(method, args, params)
}
if (result_json.payload && result_json.payload.FALLBACK){
Object.keys(result_json.payload.FALLBACK).forEach(key => {
args[key] = result_json.payload.FALLBACK[key]
})
return this.api_call(method, args, params)
}
throw new GWAPIError(JSON.stringify(result_json.error))
}
if (!this.api_token && method == 'deezer.getUserData') this.api_token = result_json.results.checkForm
return result_json.results
}
async _get_token(){
let token_data = await this.get_user_data()
return token_data.checkForm
}
get_user_data(){
return this.api_call('deezer.getUserData')
}
get_user_profile_page(user_id, tab, options={}){
const limit = options.limit || 10
return this.api_call('deezer.pageProfile', {USER_ID: user_id, tab, nb: limit})
}
get_user_favorite_ids(checksum = null, options={}){
const limit = options.limit || 10000
const start = options.start || 0
return this.api_call('song.getFavoriteIds', {nb: limit, start, checksum})
}
get_child_accounts(){
return this.api_call('deezer.getChildAccounts')
}
get_track(sng_id){
return this.api_call('song.getData', {SNG_ID: sng_id})
}
get_track_page(sng_id){
return this.api_call('deezer.pageTrack', {SNG_ID: sng_id})
}
get_track_lyrics(sng_id){
return this.api_call('song.getLyrics', {SNG_ID: sng_id})
}
async get_tracks(sng_ids){
let tracks_array = []
let body = await this.api_call('song.getListData', {SNG_IDS: sng_ids})
let errors = 0
for (let i = 0; i < sng_ids.length; i++){
if (sng_ids[0] != 0){
tracks_array.push(body.data[i - errors])
} else {
errors++
tracks_array.push(EMPTY_TRACK_OBJ)
}
}
return tracks_array
}
get_album(alb_id){
return this.api_call('album.getData', {ALB_ID: alb_id})
}
get_album_page(alb_id){
return this.api_call('deezer.pageAlbum', {
ALB_ID: alb_id,
lang: 'en',
header: true,
tab: 0
})
}
async get_album_tracks(alb_id){
let tracks_array = []
let body = await this.api_call('song.getListByAlbum', {ALB_ID: alb_id, nb: -1})
body.data.forEach(track => {
let _track = track
_track.POSITION = body.data.indexOf(track)
tracks_array.push(_track)
})
return tracks_array
}
get_artist(art_id){
return this.api_call('artist.getData', {ART_ID: art_id})
}
get_artist_page(art_id){
return this.api_call('deezer.pageArtist', {
ART_ID: art_id,
lang: 'en',
header: true,
tab: 0
})
}
async get_artist_top_tracks(art_id, options={}){
const limit = options.limit || 100
let tracks_array = []
let body = await this.api_call('artist.getTopTrack', {ART_ID: art_id, nb: limit})
body.data.forEach(track => {
track.POSITION = body.data.indexOf(track)
tracks_array.push(track)
})
return tracks_array
}
get_artist_discography(art_id, options={}){
const index = options.index || 0
const limit = options.limit || 25
return this.api_call('album.getDiscography', {
ART_ID: art_id,
discography_mode:"all",
nb: limit,
nb_songs: 0,
start: index
})
}
get_playlist(playlist_id){
return this.get_playlist_page(playlist_id)
}
get_playlist_page(playlist_id){
return this.api_call('deezer.pagePlaylist', {
PLAYLIST_ID: playlist_id,
lang: 'en',
header: true,
tab: 0
})
}
async get_playlist_tracks(playlist_id){
let tracks_array = []
let body = await this.api_call('playlist.getSongs', {PLAYLIST_ID: playlist_id, nb: -1})
body.data.forEach(track => {
track.POSITION = body.data.indexOf(track)
tracks_array.push(track)
})
return tracks_array
}
create_playlist(title, status=PlaylistStatus.PUBLIC, description, songs=[]){
let newSongs = []
songs.forEach(song => {
newSongs.push([song, 0])
});
return this.api_call('playlist.create', {
title,
status,
description,
songs: newSongs
})
}
edit_playlist(playlist_id, title, status, description, songs=[]){
let newSongs = []
songs.forEach(song => {
newSongs.push([song, 0])
});
return this.api_call('playlist.update', {
PLAYLIST_ID: playlist_id,
title,
status,
description,
songs: newSongs
})
}
add_songs_to_playlist(playlist_id, songs, offset=-1){
let newSongs = []
songs.forEach(song => {
newSongs.push([song, 0])
});
return this.api_call('playlist.addSongs', {
PLAYLIST_ID: playlist_id,
songs: newSongs,
offset
})
}
add_song_to_playlist(playlist_id, sng_id, offset=-1){
return this.add_songs_to_playlist(playlist_id, [sng_id], offset)
}
remove_songs_from_playlist(playlist_id, songs){
let newSongs = []
songs.forEach(song => {
newSongs.push([song, 0])
});
return this.api_call('playlist.deleteSongs', {
PLAYLIST_ID: playlist_id,
songs: newSongs
})
}
remove_song_from_playlist(playlist_id, sng_id){
return this.remove_songs_from_playlist(playlist_id, [sng_id])
}
delete_playlist(playlist_id){
return this.api_call('playlist.delete', {PLAYLIST_ID: playlist_id})
}
add_song_to_favorites(sng_id){
return this.gw_api_call('favorite_song.add', {SNG_ID: sng_id})
}
remove_song_from_favorites(sng_id){
return this.gw_api_call('favorite_song.remove', {SNG_ID: sng_id})
}
add_album_to_favorites(alb_id){
return this.gw_api_call('album.addFavorite', {ALB_ID: alb_id})
}
remove_album_from_favorites(alb_id){
return this.gw_api_call('album.deleteFavorite', {ALB_ID: alb_id})
}
add_artist_to_favorites(art_id){
return this.gw_api_call('artist.addFavorite', {ART_ID: art_id})
}
remove_artist_from_favorites(art_id){
return this.gw_api_call('artist.deleteFavorite', {ART_ID: art_id})
}
add_playlist_to_favorites(playlist_id){
return this.gw_api_call('playlist.addFavorite', {PARENT_PLAYLIST_ID: playlist_id})
}
remove_playlist_from_favorites(playlist_id){
return this.gw_api_call('playlist.deleteFavorite', {PLAYLIST_ID: playlist_id})
}
get_page(page){
let params = {
gateway_input: JSON.stringify({
PAGE: page,
VERSION: '2.3',
SUPPORT: {
grid: [
'channel',
'album'
],
'horizontal-grid': [
'album'
],
},
LANG: 'en'
})
}
return this.api_call('page.get', {}, params)
}
search(query, index=0, limit=10, suggest=true, artist_suggest=true, top_tracks=true){
return this.api_call('deezer.pageSearch', {
query,
start: index,
nb: limit,
suggest,
artist_suggest,
top_tracks
})
}
search_music(query, type, options={}){
const index = options.index || 0
const limit = options.limit || 10
return this.api_call('search.music', {
query,
filter: "ALL",
output: type,
start: index,
nb: limit
})
}
// Extra calls
async get_artist_discography_tabs(art_id, options={}){
const limit = options.limit || 100
let index = 0
let releases = []
let result = {all: []}
let ids = []
// Get all releases
let response
do {
response = await this.get_artist_discography(art_id, {index, limit})
releases = releases.concat(response.data)
index += limit
} while (index < response.total)
releases.forEach(release => {
if (ids.indexOf(release.ALB_ID) == -1){
ids.push(release.ALB_ID)
let obj = map_artist_album(release)
if ((release.ART_ID == art_id || release.ART_ID != art_id && release.ROLE_ID == 0) && release.ARTISTS_ALBUMS_IS_OFFICIAL){
// Handle all base record types
if (!result[obj.record_type]) result[obj.record_type] = []
result[obj.record_type].push(obj)
result.all.push(obj)
} else {
if (release.ROLE_ID == 5) { // Handle albums where the artist is featured
if (!result.featured) result.featured = []
result.featured.push(obj)
} else if (release.ROLE_ID == 0) { // Handle "more" albums
if (!result.more) result.more = []
result.more.push(obj)
result.all.push(obj)
}
}
}
})
return result
}
async get_track_with_fallback(sng_id){
let body
if (parseInt(sng_id) > 0){
try{ body = await this.get_track_page(sng_id) }
catch (e) { /*nothing*/ }
}
if (body){
if (body.LYRICS) body.DATA.LYRICS = body.LYRICS
if (body.ISRC) body.DATA.ALBUM_FALLBACK = body.ISRC
body = body.DATA
} else {
body = await this.get_track(sng_id)
}
return body
}
async get_user_playlists(user_id, options={}){
const limit = options.limit || 25
let user_profile_page = await this.get_user_profile_page(user_id, 'playlists', {limit})
let blog_name = user_profile_page.DATA.USER.BLOG_NAME || "Unknown"
let data = user_profile_page.TAB.playlists.data
let result = []
data.forEach(playlist => {
result.push(map_user_playlist(playlist, blog_name))
})
return result
}
async get_user_albums(user_id, options={}){
const limit = options.limit || 25
let data = await this.get_user_profile_page(user_id, 'albums', {limit})
data = data.TAB.albums.data
let result = []
data.forEach(album => {
result.push(map_user_album(album))
})
return result
}
async get_user_artists(user_id, options={}){
const limit = options.limit || 25
let data = await this.get_user_profile_page(user_id, 'artists', {limit})
data = data.TAB.artists.data
let result = []
data.forEach(artist => {
result.push(map_user_artist(artist))
})
return result
}
async get_user_tracks(user_id, options={}){
let user_data = await this.get_user_data()
if (user_data.USER.USER_ID == user_id) return this.get_my_favorite_tracks(options)
const limit = options.limit || 25
let data = this.get_user_profile_page(user_id, 'loved', {limit})
data = data.TAB.loved.data
let result = []
data.forEach(track => {
result.push(map_user_track(track))
})
return result
}
async get_my_favorite_tracks(options={}){
const limit = options.limit || 25
const ids_raw = await this.get_user_favorite_ids(null, {limit})
const ids = ids_raw.data.map(x => x.SNG_ID)
if (!ids.length) return []
let data = await this.get_tracks(ids)
let result = []
data.forEach((track, i) => {
track = {...track, ...ids_raw.data[i]}
result.push(map_user_track(track))
})
return result
}
}
module.exports = {
PlaylistStatus,
EMPTY_TRACK_OBJ,
GW
}