freezer/lib/api/pipe_api.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'])]);
}
}