import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/foundation.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 'cookie_jar_hive_storage.dart'; import 'dart:convert'; import 'dart:async'; final deezerAPI = DeezerAPI(); final cookieJar = PersistCookieJar(storage: HiveStorage('cookies')); class DeezerAPI { // from deemix: https://gitlab.com/RemixDev/deemix-js/-/blob/main/deemix/utils/deezer.js?ref_type=heads#L6 static const CLIENT_ID = "172365"; static const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"; static const USER_AGENT_SUFFIX = 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'; static const WINDOWS_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) $USER_AGENT_SUFFIX"; static const MACOS_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) $USER_AGENT_SUFFIX'; static const USER_AGENTS = { TargetPlatform.android: WINDOWS_USER_AGENT, TargetPlatform.windows: WINDOWS_USER_AGENT, TargetPlatform.iOS: MACOS_USER_AGENT, TargetPlatform.macOS: MACOS_USER_AGENT, TargetPlatform.linux: 'Mozilla/5.0 (X11; Linux x86_64) $USER_AGENT_SUFFIX', }; static String get userAgent => USER_AGENTS[defaultTargetPlatform]!; static final _logger = Logger('DeezerAPI'); DeezerAPI(); set arl(String? arl) { if (arl == null) { cookieJar.delete(Uri.https('www.deezer.com')); return; } cookieJar.saveFromResponse(Uri.https('www.deezer.com'), [ Cookie('arl', arl) ..domain = '.deezer.com' ..httpOnly = true ..sameSite = SameSite.none ..secure = true ]); } String? token; String? userId; String? userName; String? favoritesPlaylistId; String? sid; late String licenseToken; late bool canStreamLossless; late bool canStreamHQ; late final dio = Dio(BaseOptions( headers: headers, responseType: ResponseType.json, validateStatus: (status) => true)) ..interceptors.add(CookieManager(cookieJar)); Future? _authorizing; //Get headers Map get headers => { "User-Agent": userAgent, "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", }; Future logout() async { // actual logout from deezer API await dio.get('https://www.deezer.com/logout.php'); await dio.get('https://auth.deezer.com/logout'); // delete all cookies await cookieJar.deleteAll(); updateHeaders(); } void updateHeaders() { dio.options.headers = headers; } //Call private API Future> callApi(String method, {Map? params, String? gatewayInput, CancelToken? cancelToken}) async { //Post final res = await dio.post('https://www.deezer.com/ajax/gw-light.php', queryParameters: { 'api_version': '1.0', 'api_token': token, 'input': '3', 'method': method, //Used for homepage if (gatewayInput != null) 'gateway_input': gatewayInput }, data: jsonEncode(params), cancelToken: cancelToken); final body = res.data; // 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 { final res = await dio.get('https://api.deezer.com/$path'); return res.data; } //Wrapper so it can be globally awaited Future authorize() async => _authorizing ??= rawAuthorize(); // NOT WORKING ANYMORE. // this didn't last very long now, did it? // //Login with email FROM DEEMIX-JS // Future getArlByEmail(String email, String password) async { // //Get MD5 of password // final md5Password = md5.convert(utf8.encode(password)).toString(); // final hash = md5 // .convert(utf8 // .encode([CLIENT_ID, email, md5Password, CLIENT_SECRET].join(''))) // .toString(); // //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"]; // final res = await dio.get('https://api.deezer.com/auth/token', // queryParameters: { // 'app_id': CLIENT_ID, // 'login': email, // 'password': md5Password, // 'hash': hash // }, // options: Options(responseType: ResponseType.json)); // print(res.data); // final accessToken = res.data['access_token'] as String?; // if (accessToken == null) { // throw Exception('login failed, access token is null'); // } // // print(accessToken); // // return getArlByAccessToken(accessToken); // } // FROM DEEMIX-JS Future getArlByAccessToken(String accessToken) async { //Get SID in cookieJar final res = await dio.get("https://api.deezer.com/platform/generic/track/3135556", options: Options(headers: { 'Authorization': 'Bearer $accessToken', 'User-Agent': userAgent, })); print(res.data); //Get ARL final arlRes = await dio.get("https://www.deezer.com/ajax/gw-light.php", queryParameters: { 'method': 'user.getArl', 'input': '3', 'api_version': '1.0', 'api_token': 'null', }, options: Options(responseType: ResponseType.json)); final arl = arlRes.data["results"]; if (arl == null) throw Exception('couldn\'t obtain ARL'); return arl; } //Authorize, bool = success Future rawAuthorize({Function? onError}) async { _logger.fine('rawAuthorize()'); try { final data = await callApi('deezer.getUserData'); if (data['results']?['USER']?['USER_ID'] == null || 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']; canStreamHQ = data['results']['USER']['OPTIONS']['web_hq'] as bool || data['results']['USER']['OPTIONS']['mobile_hq'] as bool; canStreamLossless = data['results']['USER']['OPTIONS']['web_lossless'] as bool || data['results']['USER']['OPTIONS']['mobile_lossless'] as bool; licenseToken = data['results']['USER']['OPTIONS']['license_token'] as String; settings.checkQuality(canStreamHQ, canStreamLossless); cache.canStreamHQ = canStreamHQ; cache.canStreamLossless = canStreamLossless; 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(Uri uri) async { //https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID[/radio?autoplay=true] if (uri.host == 'www.deezer.com' || uri.host == 'deezer.com') { if (uri.pathSegments.length < 2) return null; if (uri.pathSegments[uri.pathSegments.length - 1] == 'radio') { return DeezerLinkResponse( type: DeezerLinkResponse.typeFromString( uri.pathSegments[uri.pathSegments.length - 3]), id: uri.pathSegments[uri.pathSegments.length - 2]); } return DeezerLinkResponse( type: DeezerLinkResponse.typeFromString( uri.pathSegments[uri.pathSegments.length - 2]), 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); request.followRedirects = false; http.StreamedResponse response = await request.send(); String newUrl = response.headers['location']!; return parseLink(Uri.parse(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: DeezerMediaType.track, id: id); } //Albums if (uri.pathSegments[0] == 'album') { String id = await SpotifyScrapper.convertAlbum(spotifyUri); return DeezerLinkResponse(type: DeezerMediaType.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; } } Future getTrackUrl( String trackToken, String format) async => (await getTracksUrl([trackToken], format))[0]; Future> getTracksUrl( List trackTokens, String format) async { final response = await http.post( Uri.https('media.deezer.com', '/v1/get_url'), body: jsonEncode({ "license_token": licenseToken, "media": [ { "type": "FULL", "formats": [ {"cipher": "BF_CBC_STRIPE", "format": format} ], } ], "track_tokens": trackTokens, }), headers: headers, ); final data = (jsonDecode(response.body) as Map)['data'] as List; return data.map((data) { if (data['errors'] != null) { if (data['errors'][0]['code'] == 2002) { return GetTrackUrlResponse(error: 'Wrong geolocation'); } return GetTrackUrlResponse( error: (data['errors'][0] as Map).toString()); } if (data['media'] == null) return GetTrackUrlResponse(); return GetTrackUrlResponse( sources: (data['media'][0]['sources'] as List) .map((e) => TrackUrlSource.fromPrivateJson( (e as Map).cast())) .toList(growable: false)); }).toList(growable: false); } //Search Future search(String? query) async { Map data = await callApi('deezer.pageSearch', params: {'nb': 128, 'query': query, 'start': 0}); print(data['results']['TOP_RESULT']); return SearchResults.fromPrivateJson(data['results']); } Future> getTracks(List ids) async { final data = await callApi('song.getListData', params: {'sng_ids': ids}); return (data['results']['data'] as List) .map((t) => Track.fromPrivateJson(t as Map)) .toList(growable: false); } Future track(String id) async { return (await getTracks([id]))[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 = 5000}) 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}); } Future addFavoriteShow(String id) => callApi('show.addFavorite', params: {'SHOW_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': 2000, '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': 2000, '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}); } Future removeFavoriteShow(String id) => callApi('show.deleteFavorite', params: {'SHOW_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, {CancelToken? cancelToken}) async { Map data = await callApi('song.getLyrics', params: {'sng_id': trackId}, cancelToken: cancelToken); if (data['error'] != null && data['error'].length > 0) { throw Exception('Deezer reported error: ${data['error']}'); } print(data); 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'], 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', 'external-link' ]; 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, { /// Amount of times the track's been seeked int seek = 0, /// Amount of times the track's been paused int pause = 0, /// 1 if the track was listened in sync, 0 otherwise int sync = 1, /// If the track is skipped to the next song bool next = false, /// If the track is skipped to the previous song bool prev = false, /// When the timestamp has begun as UTC int (in SECONDS) int? timestamp, }) async { await callApi('log.listen', params: { 'params': { 'timestamp': timestamp ?? (DateTime.timestamp().millisecondsSinceEpoch) ~/ 1000, 'ts_listen': DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, 'type': 1, 'stat': { 'seek': seek, // amount of times seeked 'pause': pause, // amount of times paused 'sync': sync, if (next) 'next': true, if (prev) 'prev': true, }, '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, {CancelToken? cancelToken}) async { Map data = await callApi('search_getSuggestedQueries', params: {'QUERY': query}, cancelToken: cancelToken); 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> getSearchTrackMix(String trackId, [bool? startWithInputTrack = true]) async { Map data = await callApi('song.getSearchTrackMix', params: { 'sng_id': trackId, if (startWithInputTrack != null) 'start_with_input_track': startWithInputTrack, }); 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(); } } class PipeAPI { PipeAPI._(); Future getTrackToken(String trackId) async {} }