import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/spotify.dart'; import 'package:freezer/settings.dart'; import 'package:http/http.dart' as http; import 'dart:io'; import 'dart:convert'; import 'dart:async'; DeezerAPI deezerAPI = DeezerAPI(); class DeezerAPI { DeezerAPI({this.arl}); String arl; String token; String userId; String userName; String favoritesPlaylistId; String sid; Future _authorizing; //Get headers Map get headers => { "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", "Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}', "Cache-Control": "max-age=0", "Accept": "*/*", "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3", "Accept-Language": "${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'},${settings.deezerLanguage??"en"};q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "keep-alive", "Cookie": "arl=${arl}" + ((sid == null) ? '' : '; sid=${sid}') }; //Call private API Future> callApi(String method, {Map params, String gatewayInput}) async { //Generate URL Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', { 'api_version': '1.0', 'api_token': this.token, 'input': '3', 'method': method, //Used for homepage if (gatewayInput != null) 'gateway_input': gatewayInput }); //Post http.Response res = await http.post(uri, headers: headers, body: jsonEncode(params)); dynamic body = jsonDecode(res.body); //Grab SID if (method == 'deezer.getUserData') { for (String cookieHeader in res.headers['set-cookie'].split(';')) { if (cookieHeader.startsWith('sid=')) { sid = cookieHeader.split('=')[1]; } } } // In case of error "Invalid CSRF token" retrieve new one and retry the same call if (body['error'].isNotEmpty && body['error'].containsKey('VALID_TOKEN_REQUIRED') && await rawAuthorize()) { return callApi(method, params: params, gatewayInput: gatewayInput); } return body; } Future> callPublicApi(String path) async { http.Response res = await http.get('https://api.deezer.com/' + path); return jsonDecode(res.body); } //Wrapper so it can be globally awaited Future authorize() async { if (_authorizing == null) { this._authorizing = this.rawAuthorize(); } return _authorizing; } //Authorize, bool = success Future rawAuthorize({Function onError}) async { try { Map data = await callApi('deezer.getUserData'); if (data['results']['USER']['USER_ID'] == 0) { return false; } else { this.token = data['results']['checkForm']; this.userId = data['results']['USER']['USER_ID'].toString(); this.userName = data['results']['USER']['BLOG_NAME']; this.favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID']; return true; } } catch (e) { if (onError != null) onError(e); print('Login Error (D): ' + e.toString()); return false; } } //URL/Link parser Future parseLink(String url) async { Uri uri = Uri.parse(url); //https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID if (uri.host == 'www.deezer.com' || uri.host == 'deezer.com') { if (uri.pathSegments.length < 2) return null; DeezerLinkType type = DeezerLinkResponse.typeFromString(uri.pathSegments[uri.pathSegments.length-2]); return DeezerLinkResponse(type: type, id: uri.pathSegments[uri.pathSegments.length-1]); } //Share URL if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') { http.BaseRequest request = http.Request('HEAD', Uri.parse(url)); request.followRedirects = false; http.StreamedResponse response = await request.send(); String newUrl = response.headers['location']; return parseLink(newUrl); } //Spotify if (uri.host == 'open.spotify.com') { if (uri.pathSegments.length < 2) return null; String spotifyUri = 'spotify:' + uri.pathSegments.sublist(0, 2).join(':'); try { //Tracks if (uri.pathSegments[0] == 'track') { String id = await spotify.convertTrack(spotifyUri); return DeezerLinkResponse(type: DeezerLinkType.TRACK, id: id); } //Albums if (uri.pathSegments[0] == 'album') { String id = await spotify.convertAlbum(spotifyUri); return DeezerLinkResponse(type: DeezerLinkType.ALBUM, id: id); } } catch (e) {} } } //Search Future search(String query) async { Map data = await callApi('deezer.pageSearch', params: { 'nb': 128, 'query': query, 'start': 0 }); return SearchResults.fromPrivateJson(data['results']); } Future track(String id) async { Map data = await callApi('song.getListData', params: {'sng_ids': [id]}); return Track.fromPrivateJson(data['results']['data'][0]); } //Get album details, tracks Future album(String id) async { Map data = await callApi('deezer.pageAlbum', params: { 'alb_id': id, 'header': true, 'lang': settings.deezerLanguage??'en' }); return Album.fromPrivateJson(data['results']['DATA'], songsJson: data['results']['SONGS']); } //Get artist details Future artist(String id) async { Map data = await callApi('deezer.pageArtist', params: { 'art_id': id, 'lang': settings.deezerLanguage??'en', }); return Artist.fromPrivateJson( data['results']['DATA'], topJson: data['results']['TOP'], albumsJson: data['results']['ALBUMS'], highlight: data['results']['HIGHLIGHT'] ); } //Get playlist tracks at offset Future> playlistTracksPage(String id, int start, {int nb = 50}) async { Map data = await callApi('deezer.pagePlaylist', params: { 'playlist_id': id, 'lang': settings.deezerLanguage??'en', 'nb': nb, 'tags': true, 'start': start }); return data['results']['SONGS']['data'].map((json) => Track.fromPrivateJson(json)).toList(); } //Get playlist details Future playlist(String id, {int nb = 100}) async { Map data = await callApi('deezer.pagePlaylist', params: { 'playlist_id': id, 'lang': settings.deezerLanguage??'en', 'nb': nb, 'tags': true, 'start': 0 }); return Playlist.fromPrivateJson(data['results']['DATA'], songsJson: data['results']['SONGS']); } //Get playlist with all tracks Future fullPlaylist(String id) async { return await playlist(id, nb: 100000); } //Add track to favorites Future addFavoriteTrack(String id) async { await callApi('favorite_song.add', params: {'SNG_ID': id}); } //Add album to favorites/library Future addFavoriteAlbum(String id) async { await callApi('album.addFavorite', params: {'ALB_ID': id}); } //Add artist to favorites/library Future addFavoriteArtist(String id) async { await callApi('artist.addFavorite', params: {'ART_ID': id}); } //Remove artist from favorites/library Future removeArtist(String id) async { await callApi('artist.deleteFavorite', params: {'ART_ID': id}); } // Mark track as disliked Future dislikeTrack(String id) async { await callApi('favorite_dislike.add', params: {'ID': id, 'TYPE': 'song'}); } //Add tracks to playlist Future addToPlaylist(String trackId, String playlistId, {int offset = -1}) async { await callApi('playlist.addSongs', params: { 'offset': offset, 'playlist_id': playlistId, 'songs': [[trackId, 0]] }); } //Remove track from playlist Future removeFromPlaylist(String trackId, String playlistId) async { await callApi('playlist.deleteSongs', params: { 'playlist_id': playlistId, 'songs': [[trackId, 0]] }); } //Get users playlists Future> getPlaylists() async { Map data = await callApi('deezer.pageProfile', params: { 'nb': 100, 'tab': 'playlists', 'user_id': this.userId }); return data['results']['TAB']['playlists']['data'].map((json) => Playlist.fromPrivateJson(json, library: true)).toList(); } //Get favorite albums Future> getAlbums() async { Map data = await callApi('deezer.pageProfile', params: { 'nb': 50, 'tab': 'albums', 'user_id': this.userId }); List albumList = data['results']['TAB']['albums']['data']; List albums = albumList.map((json) => Album.fromPrivateJson(json, library: true)).toList(); return albums; } //Remove album from library Future removeAlbum(String id) async { await callApi('album.deleteFavorite', params: { 'ALB_ID': id }); } //Remove track from favorites Future removeFavorite(String id) async { await callApi('favorite_song.remove', params: { 'SNG_ID': id }); } //Get favorite artists Future> getArtists() async { Map data = await callApi('deezer.pageProfile', params: { 'nb': 40, 'tab': 'artists', 'user_id': this.userId }); return data['results']['TAB']['artists']['data'].map((json) => Artist.fromPrivateJson(json, library: true)).toList(); } //Get lyrics by track id Future lyrics(String trackId) async { Map data = await callApi('song.getLyrics', params: { 'sng_id': trackId }); if (data['error'] != null && data['error'].length > 0) return Lyrics.error(); return Lyrics.fromPrivateJson(data['results']); } Future smartTrackList(String id) async { Map data = await callApi('deezer.pageSmartTracklist', params: { 'smarttracklist_id': id }); return SmartTrackList.fromPrivateJson(data['results']['DATA'], songsJson: data['results']['SONGS']); } Future> flow() async { Map data = await callApi('radio.getUserRadio', params: { 'user_id': userId }); return data['results']['data'].map((json) => Track.fromPrivateJson(json)).toList(); } //Get homepage/music library from deezer Future homePage() async { List grid = ['album', 'artist', 'channel', 'flow', 'playlist', 'radio', 'show', 'smarttracklist', 'track', 'user']; Map data = await callApi('page.get', gatewayInput: jsonEncode({ "PAGE": "home", "VERSION": "2.3", "SUPPORT": { /* "deeplink-list": ["deeplink"], "list": ["episode"], "grid-preview-one": grid, "grid-preview-two": grid, "slideshow": grid, "message": ["call_onboarding"], */ "grid": grid, "horizontal-grid": grid, "item-highlight": ["radio"], "large-card": ["album", "playlist", "show", "video-link"], "ads": [] //Nope }, "LANG": settings.deezerLanguage??'en', "OPTIONS": [] })); return HomePage.fromPrivateJson(data['results']); } //Log song listen to deezer Future logListen(String trackId) async { await callApi('log.listen', params: { 'params': { 'timestamp': DateTime.now().millisecondsSinceEpoch, 'ts_listen': DateTime.now().millisecondsSinceEpoch, 'type': 1, 'stat': {'seek': 0, 'pause': 0, 'sync': 1}, 'media': {'id': trackId, 'type': 'song', 'format': 'MP3_128'} } }); } Future getChannel(String target) async { List grid = ['album', 'artist', 'channel', 'flow', 'playlist', 'radio', 'show', 'smarttracklist', 'track', 'user']; Map data = await callApi('page.get', gatewayInput: jsonEncode({ 'PAGE': target, "VERSION": "2.3", "SUPPORT": { /* "deeplink-list": ["deeplink"], "list": ["episode"], "grid-preview-one": grid, "grid-preview-two": grid, "slideshow": grid, "message": ["call_onboarding"], */ "grid": grid, "horizontal-grid": grid, "item-highlight": ["radio"], "large-card": ["album", "playlist", "show", "video-link"], "ads": [] //Nope }, "LANG": settings.deezerLanguage??'en', "OPTIONS": [] })); return HomePage.fromPrivateJson(data['results']); } //Add playlist to library Future addPlaylist(String id) async { await callApi('playlist.addFavorite', params: { 'parent_playlist_id': int.parse(id) }); } //Remove playlist from library Future removePlaylist(String id) async { await callApi('playlist.deleteFavorite', params: { 'playlist_id': int.parse(id) }); } //Delete playlist Future deletePlaylist(String id) async { await callApi('playlist.delete', params: { 'playlist_id': id }); } //Create playlist //Status 1 - private, 2 - collaborative Future createPlaylist(String title, {String description = "", int status = 1, List trackIds = const []}) async { Map data = await callApi('playlist.create', params: { 'title': title, 'description': description, 'songs': trackIds.map((id) => [int.parse(id), trackIds.indexOf(id)]).toList(), 'status': status }); //Return playlistId return data['results'].toString(); } //Get part of discography Future> discographyPage(String artistId, {int start = 0, int nb = 50}) async { Map data = await callApi('album.getDiscography', params: { 'art_id': int.parse(artistId), 'discography_mode': 'all', 'nb': nb, 'start': start, 'nb_songs': 30 }); return data['results']['data'].map((a) => Album.fromPrivateJson(a)).toList(); } Future searchSuggestions(String query) async { Map data = await callApi('search_getSuggestedQueries', params: { 'QUERY': query }); return data['results']['SUGGESTION'].map((s) => s['QUERY']).toList(); } //Get smart radio for artist id Future> smartRadio(String artistId) async { Map data = await callApi('smart.getSmartRadio', params: { 'art_id': int.parse(artistId) }); return data['results']['data'].map((t) => Track.fromPrivateJson(t)).toList(); } //Update playlist metadata, status = see createPlaylist Future updatePlaylist(String id, String title, String description, {int status = 1}) async { await callApi('playlist.update', params: { 'description': description, 'title': title, 'playlist_id': int.parse(id), 'status': status, 'songs': [] }); } //Get shuffled library Future> libraryShuffle({int start=0}) async { Map data = await callApi('tracklist.getShuffledCollection', params: { 'nb': 50, 'start': start }); return data['results']['data'].map((t) => Track.fromPrivateJson(t)).toList(); } //Get similar tracks for track with id [trackId] Future> playMix(String trackId) async { Map data = await callApi('song.getContextualTrackMix', params: { 'sng_ids': [trackId] }); return data['results']['data'].map((t) => Track.fromPrivateJson(t)).toList(); } Future> allShowEpisodes(String showId) async { Map data = await callApi('deezer.pageShow', params: { 'country': settings.deezerCountry, 'lang': settings.deezerLanguage, 'nb': 1000, 'show_id': showId, 'start': 0, 'user_id': int.parse(deezerAPI.userId) }); return data['results']['EPISODES']['data'].map((e) => ShowEpisode.fromPrivateJson(e)).toList(); } }