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.instance.dio; Future 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> callApi( String operationName, String query, Map 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(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((lrc) => Lyric.fromPrivateJson(lrc as Map)) .toList(growable: false)); } return Lyrics( id: lrc['id'], writers: lrc['writers'], sync: false, lyrics: [Lyric(text: lrc['text'])]); } }