149 lines
3.8 KiB
Dart
149 lines
3.8 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() async {
|
|
// authorize on pipe.deezer.com
|
|
|
|
if (DateTime.now().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,
|
|
);
|
|
final lyrics = data['data']['track']['lyrics'] as Map?;
|
|
if (lyrics == null) {
|
|
return null;
|
|
}
|
|
|
|
if (lyrics['synchronizedLines'] != null) {
|
|
return Lyrics(
|
|
id: lyrics['id'],
|
|
writers: lyrics['writers'],
|
|
sync: true,
|
|
lyrics: (lyrics['synchronizedLines'] as List)
|
|
.map<Lyric>((lrc) => Lyric.fromPrivateJson(lrc as Map))
|
|
.toList(growable: false));
|
|
}
|
|
|
|
return Lyrics(
|
|
id: lyrics['id'],
|
|
writers: lyrics['writers'],
|
|
sync: false,
|
|
lyrics: [Lyric(text: lyrics['text'])]);
|
|
}
|
|
}
|