Pato05
2862c9ec05
restore translations functionality make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available exit app if opening cache or settings fails (another instance running) remove draggable_scrollbar and use builtin widget instead fix email login better way to manage lyrics (less updates and lookups in the lyrics List) fix player_screen on mobile (too big -> just average :)) right click: use TapUp events instead desktop: show context menu on triple dots button also avoid showing connection error if the homepage is cached and available offline i'm probably forgetting something idk
727 lines
24 KiB
Dart
727 lines
24 KiB
Dart
import 'package:cookie_jar/cookie_jar.dart';
|
|
import 'package:crypto/crypto.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 'dart:convert';
|
|
import 'dart:async';
|
|
|
|
final deezerAPI = DeezerAPI();
|
|
|
|
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, String>{
|
|
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)]);
|
|
}
|
|
|
|
String? token;
|
|
String? userId;
|
|
String? userName;
|
|
String? favoritesPlaylistId;
|
|
String? sid;
|
|
late String licenseToken;
|
|
late bool canStreamLossless;
|
|
late bool canStreamHQ;
|
|
|
|
final cookieJar = DefaultCookieJar();
|
|
late final dio = Dio(BaseOptions(
|
|
headers: headers,
|
|
responseType: ResponseType.json,
|
|
validateStatus: (status) => true))
|
|
..interceptors.add(CookieManager(cookieJar));
|
|
|
|
Future<bool>? _authorizing;
|
|
|
|
//Get headers
|
|
Map<String, String> 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<void> logout() async {
|
|
// delete all cookies
|
|
await cookieJar.deleteAll();
|
|
updateHeaders();
|
|
}
|
|
|
|
void updateHeaders() {
|
|
dio.options.headers = headers;
|
|
}
|
|
|
|
//Call private API
|
|
Future<Map<dynamic, dynamic>> callApi(String method,
|
|
{Map<dynamic, dynamic>? params, String? gatewayInput}) 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));
|
|
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<Map> 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<bool> authorize() async => _authorizing ??= rawAuthorize();
|
|
|
|
//Login with email FROM DEEMIX-JS
|
|
Future<String> 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));
|
|
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<String> 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<bool> rawAuthorize({Function? onError}) async {
|
|
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<DeezerLinkResponse?> 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<bool?> 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<GetTrackUrlResponse> getTrackUrl(
|
|
String trackToken, String format) async =>
|
|
(await getTracksUrl([trackToken], format))[0];
|
|
|
|
Future<List<GetTrackUrlResponse>> getTracksUrl(
|
|
List<String> 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<TrackUrlSource>((e) => TrackUrlSource.fromPrivateJson(
|
|
(e as Map).cast<String, dynamic>()))
|
|
.toList(growable: false));
|
|
}).toList(growable: false);
|
|
}
|
|
|
|
//Search
|
|
Future<SearchResults> search(String? query) async {
|
|
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
|
params: {'nb': 128, 'query': query, 'start': 0});
|
|
return SearchResults.fromPrivateJson(data['results']);
|
|
}
|
|
|
|
Future<Track> track(String? id) async {
|
|
Map<dynamic, dynamic> data = await callApi('song.getListData', params: {
|
|
'sng_ids': [id]
|
|
});
|
|
return Track.fromPrivateJson(data['results']['data'][0]);
|
|
}
|
|
|
|
//Get album details, tracks
|
|
Future<Album> album(String? id) async {
|
|
Map<dynamic, dynamic> 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> artist(String? id) async {
|
|
Map<dynamic, dynamic> 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<List<Track>?> 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<Track>((json) => Track.fromPrivateJson(json))
|
|
.toList();
|
|
}
|
|
|
|
//Get playlist details
|
|
Future<Playlist> playlist(String? id, {int nb = 100}) async {
|
|
Map<dynamic, dynamic> 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<Playlist> 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<List<Playlist>> getPlaylists() async {
|
|
Map data = await callApi('deezer.pageProfile',
|
|
params: {'nb': 100, 'tab': 'playlists', 'user_id': userId});
|
|
return data['results']['TAB']['playlists']['data']
|
|
.map<Playlist>((json) => Playlist.fromPrivateJson(json, library: true))
|
|
.toList();
|
|
}
|
|
|
|
//Get favorite albums
|
|
Future<List<Album>> getAlbums() async {
|
|
Map data = await callApi('deezer.pageProfile',
|
|
params: {'nb': 50, 'tab': 'albums', 'user_id': userId});
|
|
List albumList = data['results']['TAB']['albums']['data'];
|
|
List<Album> albums = albumList
|
|
.map<Album>((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<List<Artist>?> getArtists() async {
|
|
Map data = await callApi('deezer.pageProfile',
|
|
params: {'nb': 40, 'tab': 'artists', 'user_id': userId});
|
|
return data['results']['TAB']['artists']['data']
|
|
.map<Artist>((json) => Artist.fromPrivateJson(json, library: true))
|
|
.toList();
|
|
}
|
|
|
|
//Get lyrics by track id
|
|
Future<Lyrics> 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> smartTrackList(String? id) async {
|
|
Map data = await callApi('deezer.pageSmartTracklist',
|
|
params: {'smarttracklist_id': id});
|
|
|
|
return SmartTrackList.fromPrivateJson(data['results'],
|
|
songsJson: data['results']['SONGS']);
|
|
}
|
|
|
|
Future<List<Track>?> 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<Track>((json) => Track.fromPrivateJson(json))
|
|
.toList();
|
|
}
|
|
|
|
//Get homepage/music library from deezer
|
|
Future<HomePage> 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, {
|
|
/// 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.now().millisecondsSinceEpoch) ~/ 1000,
|
|
'ts_listen': DateTime.now().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<HomePage> 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<String> createPlaylist(String? title,
|
|
{String? description = "",
|
|
int? status = 1,
|
|
List<String> trackIds = const []}) async {
|
|
Map data = await callApi('playlist.create', params: {
|
|
'title': title,
|
|
'description': description,
|
|
'songs': trackIds
|
|
.map<List>((id) => [int.parse(id), trackIds.indexOf(id)])
|
|
.toList(),
|
|
'status': status
|
|
});
|
|
//Return playlistId
|
|
return data['results'].toString();
|
|
}
|
|
|
|
//Get part of discography
|
|
Future<List<Album>?> 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<Album>((a) => Album.fromPrivateJson(a))
|
|
.toList();
|
|
}
|
|
|
|
Future<List<String>?> searchSuggestions(String? query) async {
|
|
Map data =
|
|
await callApi('search_getSuggestedQueries', params: {'QUERY': query});
|
|
return (data['results']['SUGGESTION'] as List?)
|
|
?.map<String>((s) => s['QUERY'] as String)
|
|
.toList();
|
|
}
|
|
|
|
//Get smart radio for artist id
|
|
Future<List<Track>?> smartRadio(String artistId) async {
|
|
Map data = await callApi('smart.getSmartRadio',
|
|
params: {'art_id': int.parse(artistId)});
|
|
return data['results']['data']
|
|
.map<Track>((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<List<Track>?> libraryShuffle({int start = 0}) async {
|
|
Map data = await callApi('tracklist.getShuffledCollection',
|
|
params: {'nb': 50, 'start': start});
|
|
return data['results']['data']
|
|
.map<Track>((t) => Track.fromPrivateJson(t))
|
|
.toList();
|
|
}
|
|
|
|
//Get similar tracks for track with id [trackId]
|
|
Future<List<Track>?> playMix(String? trackId) async {
|
|
Map data = await callApi('song.getContextualTrackMix', params: {
|
|
'sng_ids': [trackId]
|
|
});
|
|
return data['results']['data']
|
|
.map<Track>((t) => Track.fromPrivateJson(t))
|
|
.toList();
|
|
}
|
|
|
|
Future<List<ShowEpisode>?> 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<ShowEpisode>((e) => ShowEpisode.fromPrivateJson(e))
|
|
.toList();
|
|
}
|
|
}
|