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
161 lines
4.2 KiB
Dart
161 lines
4.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
|
final pipeAPI = PipeAPI._();
|
|
|
|
class PipeAPI {
|
|
PipeAPI._();
|
|
|
|
// JWT for pipe.deezer.com
|
|
String? _jwt;
|
|
int _jwtExpiration = 0;
|
|
|
|
final _logger = Logger('PipeAPI');
|
|
|
|
Dio get dio => deezerAPI.dio;
|
|
|
|
Future<void> authorize({bool force = false}) async {
|
|
// authorize on pipe.deezer.com
|
|
|
|
if (!force &&
|
|
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000 < _jwtExpiration) {
|
|
// only continue if JWT expired!
|
|
return;
|
|
}
|
|
|
|
// arl should be contained in cookies, so we should be fine
|
|
var res = await dio.post('https://auth.deezer.com/login/arl?jo=p&rto=c&i=c',
|
|
options: Options(responseType: ResponseType.plain));
|
|
final data = jsonDecode(res.data);
|
|
|
|
if (res.statusCode == 400) {
|
|
// renew token (refresh token should be in cookies)
|
|
res = await dio.post('https://auth.deezer.com/login/renew?jo=p&rto=c&i=c',
|
|
options: Options(responseType: ResponseType.plain));
|
|
}
|
|
|
|
if (res.statusCode != 200 || data['jwt'] == null || data['jwt'] == '') {
|
|
throw Exception('Pipe authentication failed!');
|
|
}
|
|
|
|
_jwt = data['jwt'];
|
|
_logger.fine('got jwt: $_jwt');
|
|
// decode JWT
|
|
final parts = _jwt!.split('.');
|
|
final jwtData = jsonDecode(utf8.decode(base64Url.decode(parts[1])));
|
|
_jwtExpiration = jwtData['exp'];
|
|
}
|
|
|
|
Future<Map<dynamic, dynamic>> callApi(
|
|
String operationName, String query, Map<String, dynamic> variables,
|
|
{CancelToken? cancelToken}) async {
|
|
// authorize if necessary.
|
|
await authorize();
|
|
|
|
final res = await dio.post('https://pipe.deezer.com/api',
|
|
data: jsonEncode({
|
|
'operationName': operationName,
|
|
'variables': variables,
|
|
'query': query,
|
|
}),
|
|
options: Options(headers: {'Authorization': 'Bearer $_jwt'}),
|
|
cancelToken: cancelToken);
|
|
return res.data;
|
|
}
|
|
|
|
// -- Not working --
|
|
Future<(String, int)> getTrackToken(String trackId) async {
|
|
final data = await callApi(
|
|
'TrackMediaToken',
|
|
'query TrackMediaToken(\$trackId: String!) {\n track(trackId: \$trackId) {\n media {\n token {\n payload\n expiresAt\n __typename\n }\n __typename\n }\n __typename\n }\n}',
|
|
{'trackId': trackId},
|
|
);
|
|
|
|
print('[getTrackToken] $data');
|
|
|
|
return (
|
|
data['data']['track']['media']['token']['payload'] as String,
|
|
data['data']['track']['media']['token']['expiresAt'] as int
|
|
);
|
|
}
|
|
|
|
Future<Lyrics?> lyrics(String trackId, {CancelToken? cancelToken}) async {
|
|
final data = await callApi(
|
|
'SynchronizedTrackLyrics',
|
|
r'''query SynchronizedTrackLyrics($trackId: String!) {
|
|
track(trackId: $trackId) {
|
|
...SynchronizedTrackLyrics
|
|
__typename
|
|
}
|
|
}
|
|
|
|
fragment SynchronizedTrackLyrics on Track {
|
|
id
|
|
lyrics {
|
|
...Lyrics
|
|
__typename
|
|
}
|
|
__typename
|
|
}
|
|
|
|
fragment Lyrics on Lyrics {
|
|
id
|
|
copyright
|
|
text
|
|
writers
|
|
synchronizedLines {
|
|
...LyricsSynchronizedLines
|
|
__typename
|
|
}
|
|
__typename
|
|
}
|
|
|
|
fragment LyricsSynchronizedLines on LyricsSynchronizedLine {
|
|
lrcTimestamp
|
|
line
|
|
lineTranslated
|
|
milliseconds
|
|
duration
|
|
__typename
|
|
}''',
|
|
{'trackId': trackId},
|
|
cancelToken: cancelToken,
|
|
);
|
|
if (data['errors'] != null && data['errors'].isNotEmpty) {
|
|
for (final Map error in data['errors']) {
|
|
if (error['type'] == 'JwtTokenExpiredError') {
|
|
await authorize(force: true);
|
|
return lyrics(trackId, cancelToken: cancelToken);
|
|
}
|
|
}
|
|
|
|
throw Exception(data['errors']);
|
|
}
|
|
|
|
final lrc = data['data']['track']['lyrics'] as Map?;
|
|
if (lrc == null) {
|
|
return null;
|
|
}
|
|
|
|
if (lrc['synchronizedLines'] != null) {
|
|
return Lyrics(
|
|
id: lrc['id'],
|
|
writers: lrc['writers'],
|
|
sync: true,
|
|
lyrics: (lrc['synchronizedLines'] as List)
|
|
.map<Lyric>((lrc) => Lyric.fromPrivateJson(lrc as Map))
|
|
.toList(growable: false));
|
|
}
|
|
|
|
return Lyrics(
|
|
id: lrc['id'],
|
|
writers: lrc['writers'],
|
|
sync: false,
|
|
lyrics: [Lyric(text: lrc['text'])]);
|
|
}
|
|
}
|