import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:freezer/api/cache.dart'; 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 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'dart:convert'; import 'dart:async'; import 'package:path_provider/path_provider.dart'; final deezerAPI = DeezerAPI(); class DeezerAPI { static final _logger = Logger('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}-${settings.deezerCountry}', "Cache-Control": "max-age=0", "Accept": "*/*", "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3", "Accept-Language": "${settings.deezerLanguage}-${settings.deezerCountry},${settings.deezerLanguage};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': 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 { Uri uri = Uri.https('api.deezer.com', '/$path'); http.Response res = await http.get(uri); return jsonDecode(res.body); } //Wrapper so it can be globally awaited Future authorize() async => _authorizing ??= rawAuthorize(); //Login with email static Future getArlByEmail(String? email, String password) async { //Get MD5 of password Digest digest = md5.convert(utf8.encode(password)); String md5password = '$digest'; //Get access token String url = "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json"; http.Response response = await http.get(Uri.parse(url)); String? accessToken = jsonDecode(response.body)["access_token"]; //Get SID url = "https://api.deezer.com/platform/generic/track/42069"; response = await http .get(Uri.parse(url), headers: {"Authorization": "Bearer $accessToken"}); String? sid; for (String cookieHeader in response.headers['set-cookie']!.split(';')) { if (cookieHeader.startsWith('sid=')) { sid = cookieHeader.split('=')[1]; } } if (sid == null) return null; //Get ARL url = "https://deezer.com/ajax/gw-light.php?api_version=1.0&api_token=null&input=3&method=user.getArl"; response = await http.get(Uri.parse(url), headers: {"Cookie": "sid=$sid"}); return jsonDecode(response.body)["results"]; } //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 { token = data['results']['checkForm']; userId = data['results']['USER']['USER_ID'].toString(); userName = data['results']['USER']['BLOG_NAME']; favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID']; if (cache.favoritesPlaylistId != favoritesPlaylistId) { cache.favoritesPlaylistId = favoritesPlaylistId; await cache.save(); } return true; } } catch (e) { if (onError != null) onError(e); _logger.warning('Login Error (D): $e'); 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 SpotifyScrapper.convertTrack(spotifyUri); return DeezerLinkResponse(type: DeezerLinkType.TRACK, id: id); } //Albums if (uri.pathSegments[0] == 'album') { String id = await SpotifyScrapper.convertAlbum(spotifyUri); return DeezerLinkResponse(type: DeezerLinkType.ALBUM, id: id); } } catch (e) { // we don't care about errors apparently } } return null; } //Check if Deezer available in country static Future chceckAvailability() async { try { http.Response res = await http.get(Uri.parse('https://api.deezer.com/infos')); return jsonDecode(res.body)["open"]; } catch (e) { return null; } } //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 }); 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, }); 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 = 2000 /*was 50*/}) async { Map data = await callApi('deezer.pagePlaylist', params: { 'playlist_id': id, 'lang': settings.deezerLanguage, '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, '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': 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': 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': 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}); getExternalStorageDirectory().then((value) => File(join(value!.path, 'test.json')).writeAsString(jsonEncode(data))); return SmartTrackList.fromPrivateJson(data['results'], songsJson: data['results']['SONGS']); } Future?> flow([String? config]) async { Map data = await callApi('radio.getUserRadio', params: { if (config != null) 'config_id': config, '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.5", "SUPPORT": { "deeplink-list": ["deeplink"], "list": ["episode"], "grid-preview-one": grid, "grid-preview-two": grid, "slideshow": grid, "message": ["call_onboarding"], "grid": grid, "horizontal-grid": grid, "horizontal-list": ["track", "song"], "item-highlight": ["radio"], "large-card": ["album", "playlist", "show", "video-link"], "long-card-horizontal-grid": grid, "ads": [], //Nope // ADDED BY VERSION 2.5 "small-horizontal-grid": ["flow"], "filterable-grid": ["flow"], }, "LANG": settings.deezerLanguage, "OPTIONS": [] })); return HomePage.fromPrivateJson(data['results']); } //Log song listen to deezer Future logListen(String trackId, {int seek = 0, int pause = 0, int sync = 1, int? timestamp}) async { await callApi('log.listen', params: { 'params': { 'timestamp': timestamp ?? DateTime.now().millisecondsSinceEpoch, 'ts_listen': DateTime.now().millisecondsSinceEpoch, 'type': 1, 'stat': { 'seek': seek, // amount of times seeked 'pause': pause, // amount of times paused 'sync': sync }, '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.5", "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, "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'] as List?) ?.map((s) => s['QUERY'] as String) .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(); } }