Pato05
87c9733f51
fix audio service stop on android getTrack backend improvements get new track token when expired move shuffle button into LibraryPlaylists as FAB move favoriteButton next to track title move lyrics button on top of album art search: fix chips, and remove checkbox when selected
787 lines
26 KiB
Dart
787 lines
26 KiB
Dart
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, 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)
|
|
..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<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 {
|
|
// 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<Map<dynamic, dynamic>> callApi(String method,
|
|
{Map<dynamic, dynamic>? 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<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();
|
|
|
|
// NOT WORKING ANYMORE.
|
|
// this didn't last very long now, did it?
|
|
|
|
// //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));
|
|
// 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<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 {
|
|
_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<DeezerLinkResponse?> 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<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});
|
|
print(data['results']['TOP_RESULT']);
|
|
return SearchResults.fromPrivateJson(data['results']);
|
|
}
|
|
|
|
Future<List<Track>> getTracks(List<String> ids) async {
|
|
final data = await callApi('song.getListData', params: {'sng_ids': ids});
|
|
|
|
return (data['results']['data'] as List)
|
|
.map<Track>((t) => Track.fromPrivateJson(t as Map))
|
|
.toList(growable: false);
|
|
}
|
|
|
|
Future<Track> track(String id) async {
|
|
return (await getTracks([id]))[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 = 5000}) 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});
|
|
}
|
|
|
|
Future<void> 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<List<Playlist>> getPlaylists() async {
|
|
Map data = await callApi('deezer.pageProfile',
|
|
params: {'nb': 2000, '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': 2000, '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<void> removeAlbum(String? id) async {
|
|
await callApi('album.deleteFavorite', params: {'ALB_ID': id});
|
|
}
|
|
|
|
//Remove track from favorites
|
|
Future<void> removeFavorite(String id) async {
|
|
await callApi('favorite_song.remove', params: {'SNG_ID': id});
|
|
}
|
|
|
|
Future<void> removeFavoriteShow(String id) =>
|
|
callApi('show.deleteFavorite', params: {'SHOW_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, {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> 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',
|
|
'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<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,
|
|
{CancelToken? cancelToken}) async {
|
|
Map data = await callApi('search_getSuggestedQueries',
|
|
params: {'QUERY': query}, cancelToken: cancelToken);
|
|
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<Track>> 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<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();
|
|
}
|
|
}
|
|
|
|
class PipeAPI {
|
|
PipeAPI._();
|
|
|
|
Future<void> getTrackToken(String trackId) async {}
|
|
}
|