freezer/lib/api/pipe_api.dart
Pato05 87c9733f51
add build script for linux
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
2024-02-19 00:49:32 +01:00

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'])]);
}
}