add 'login on other device'

This commit is contained in:
Pato05 2024-02-12 03:37:26 +01:00
parent e827549c1d
commit b8f0bb2140
No known key found for this signature in database
GPG Key ID: ED4C6F9C3D574FB6
16 changed files with 642 additions and 133 deletions

View File

@ -24,10 +24,10 @@ class DeezerAudioSource extends StreamAudioSource {
late AudioQuality Function() _getQuality;
late String _trackId;
late String _md5origin;
late String _mediaVersion;
final String trackToken;
final int trackTokenExpiration;
late String? _md5origin;
late String? _mediaVersion;
String? _trackToken;
int? _trackTokenExpiration;
final StreamInfoCallback? onStreamObtained;
// some cache
@ -39,28 +39,28 @@ class DeezerAudioSource extends StreamAudioSource {
DeezerAudioSource({
required AudioQuality Function() getQuality,
required String trackId,
required String md5origin,
required String mediaVersion,
required this.trackToken,
required this.trackTokenExpiration,
String? md5origin,
String? mediaVersion,
String? trackToken,
int? trackTokenExpiration,
this.onStreamObtained,
}) : _deezerAudio = DeezerAudio(
deezerAPI: deezerAPI,
md5origin: md5origin,
quality: getQuality.call(),
trackId: trackId,
mediaVersion: mediaVersion,
) {
_getQuality = getQuality;
_trackId = trackId;
_md5origin = md5origin;
_mediaVersion = mediaVersion;
_trackToken = trackToken;
_trackTokenExpiration = trackTokenExpiration;
}
AudioQuality? get quality => _deezerAudio.quality;
String get trackId => _trackId;
String get md5origin => _md5origin;
String get mediaVersion => _mediaVersion;
String? get md5origin => _md5origin;
String? get mediaVersion => _mediaVersion;
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
@ -94,16 +94,32 @@ class DeezerAudioSource extends StreamAudioSource {
_deezerAudio.quality = newQuality;
if (_downloadUrl == null) {
final gottenUrl =
await _deezerAudio.getUrl(trackToken, trackTokenExpiration);
if (gottenUrl != null) {
_downloadUrl = gottenUrl;
} else {
if (_trackToken == null) {
// TODO: get new track token?
final track = await deezerAPI.track(trackId);
_trackToken = track.trackToken;
_trackTokenExpiration = track.trackTokenExpiration;
_mediaVersion = track.playbackDetails![1];
_md5origin = track.playbackDetails![0];
}
try {
_downloadUrl =
await _deezerAudio.getUrl(_trackToken!, _trackTokenExpiration!);
} catch (e) {
_logger.warning('get_url API failed with error: $e');
_logger.warning('falling back to old url generation!');
if (_md5origin == null || _mediaVersion == null) {
throw Exception(
'Can\'t use old URL API: md5origin and mediaVersion are missing!');
}
// OLD URL GENERATION
try {
_downloadUrl = await _deezerAudio.fallback();
final res = await _deezerAudio.fallback(
md5origin: _md5origin!, mediaVersion: _mediaVersion!);
_downloadUrl = res.uri;
_md5origin = res.md5origin;
_mediaVersion = res.mediaVersion;
} on QualityException {
rethrow;
}

View File

@ -1,9 +1,8 @@
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:just_audio/just_audio.dart';
import 'package:http/http.dart' as http;
import 'deezer.dart';
class UrlAudioSource extends StreamAudioSource {
final Uri uri;
final StreamInfoCallback? onStreamObtained;
@ -13,7 +12,7 @@ class UrlAudioSource extends StreamAudioSource {
Future<StreamAudioResponse> request([int? start, int? end]) async {
final req = http.Request('GET', uri)
..headers.addAll({
'User-Agent': deezerAPI.headers['User-Agent']!,
'User-Agent': DeezerAPI.userAgent,
'Accept-Language': '*',
'Accept': '*/*',
if (start != null || end != null)

View File

@ -383,7 +383,7 @@ class DeezerAPI {
return SearchResults.fromPrivateJson(data['results']);
}
Future<Track> track(String? id) async {
Future<Track> track(String id) async {
Map<dynamic, dynamic> data = await callApi('song.getListData', params: {
'sng_ids': [id]
});

View File

@ -14,32 +14,31 @@ class DeezerAudio {
static const chunkSize = 2048;
final DeezerAPI deezerAPI;
String md5origin;
AudioQuality quality;
String trackId;
String mediaVersion;
DeezerAudio({
required this.deezerAPI,
required this.md5origin,
required this.quality,
required this.trackId,
required this.mediaVersion,
});
static final _logger = Logger('DeezerAudio');
Future<Uri> fallback() async {
Future<({Uri uri, String md5origin, String mediaVersion})> fallback(
{required String md5origin, required String mediaVersion}) async {
final res = await fallbackUrl(
md5origin: md5origin,
preferredQuality: quality,
trackId: trackId,
mediaVersion: mediaVersion);
md5origin = res.md5origin;
quality = res.quality;
trackId = res.trackId;
mediaVersion = res.mediaVersion;
return res.uri;
return (
uri: res.uri,
md5origin: res.md5origin,
mediaVersion: res.mediaVersion
);
}
Future<
@ -281,6 +280,9 @@ class DeezerAudio {
}
}
static bool isTokenExpired(int trackTokenExpiration) =>
DateTime.now().millisecondsSinceEpoch ~/ 1000 > trackTokenExpiration;
Future<Uri?> getUrl(String trackToken, int expiration) =>
getTrackUrl(deezerAPI, trackId, trackToken, expiration, quality: quality);
@ -292,10 +294,7 @@ class DeezerAudio {
required AudioQuality quality,
}) async {
final String actualTrackToken;
if (DateTime.now().millisecondsSinceEpoch ~/ 1000 > expiration) {
if (!deezerAPI.canStreamHQ && !deezerAPI.canStreamLossless) {
_logger.fine('using old url generation, token expired.');
}
if (isTokenExpired(expiration)) {
final newTrack = await deezerAPI.track(trackId);
actualTrackToken = newTrack.trackToken!;
} else {

View File

@ -161,7 +161,7 @@ class DownloadManager {
bool isSingleton = false}) async {
if (!isSupported) return false;
//Permission
if (!private && !(await checkPermission())) return false;
if (!private && !(await checkCanDownload())) return false;
//Ask for quality
AudioQuality? quality;
@ -201,7 +201,7 @@ class DownloadManager {
Future addOfflineAlbum(Album? album,
{private = true, BuildContext? context}) async {
//Permission
if (!private && !(await checkPermission())) return;
if (!private && !(await checkCanDownload())) return;
//Ask for quality
AudioQuality? quality;
@ -245,7 +245,7 @@ class DownloadManager {
if (!isSupported) return false;
//Permission
if (!private && !(await checkPermission())) return;
if (!private && !(await checkCanDownload())) return;
//Ask for quality
if (!private &&
@ -629,15 +629,20 @@ class DownloadManager {
'updateSettings', settings.getServiceSettings());
}
//Check storage permission
Future<bool> checkPermission() async {
static Future<bool> checkCanDownload() {
if (settings.downloadPath == null) {
Fluttertoast.showToast(
msg: 'Set download path in settings first!'.i18n,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM);
return false;
return Future.value(false);
}
return checkPermission();
}
//Check storage permission
static Future<bool> checkPermission() async {
if (await Permission.storage.request().isGranted) {
return true;
} else if ( // android 12 or later

View File

@ -86,7 +86,9 @@ class DownloadManager {
Future<bool> addOfflineTrack(d.Track track,
{bool private = true, BuildContext? context, isSingleton = false}) async {
//Permission
//if (!private && !(await checkPermission())) return false;
if (!private && !(await dl.DownloadManager.checkCanDownload())) {
return false;
}
//Ask for quality
//AudioQuality? quality;
@ -127,6 +129,6 @@ class DownloadManager {
static void _startNative(ServiceInstance service) =>
_startService(ServiceInterface(service: service));
static void _startService(ServiceInterface? service) =>
static void _startService(ServiceInterface service) =>
DownloadService(service).run();
}

View File

@ -1,4 +1,8 @@
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer_audio.dart';
import 'package:freezer/api/download_manager/database.dart';
import 'package:freezer/api/download_manager/service_interface.dart';
import 'package:freezer/settings.dart';
class DownloadService {
static const NOTIFICATION_ID = 6969;
@ -7,7 +11,30 @@ class DownloadService {
final ServiceInterface service;
DownloadService(this.service);
AudioQuality? _preferredQuality;
AudioQuality? _downloadQuality;
bool useGetURL = false;
void run() {
service.on('addDownloads').listen((event) {});
service.on('updateQuality').listen((event) {
_preferredQuality = AudioQuality.values[event!['q']!];
});
service.on('updateCapabilities').listen((event) {
final bool canStreamHQ = event!['canStreamHQ'];
final bool canStreamLossless = event['canStreamLossless'];
if (canStreamHQ || canStreamLossless) useGetURL = true;
_downloadQuality = settings.maxQualityFor(
_preferredQuality!, canStreamHQ, canStreamLossless);
});
}
void downloadTrack(String trackId) {
// final deezerAudio = DeezerAudio(deezerAPI: deezerAPI, md5origin: md5origin, quality: quality, trackId: trackId, mediaVersion: mediaVersion)
// if (useGetURL) {
// final url =
// }
}
}

View File

@ -3,11 +3,11 @@ import 'package:audio_session/audio_session.dart';
import 'package:flutter/foundation.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/deezer_audio_source.dart';
import 'package:freezer/api/offline_audio_source.dart';
import 'package:freezer/api/audio_sources/deezer_audio_source.dart';
import 'package:freezer/api/audio_sources/offline_audio_source.dart';
import 'package:freezer/api/paths.dart';
import 'package:freezer/api/player/player_helper.dart';
import 'package:freezer/api/url_audio_source.dart';
import 'package:freezer/api/audio_sources/url_audio_source.dart';
import 'package:freezer/ui/android_auto.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:just_audio/just_audio.dart';
@ -153,6 +153,7 @@ class AudioPlayerTask extends BaseAudioHandler {
// Linux/Windows specific options
JustAudioMediaKit.title = 'Freezer';
JustAudioMediaKit.protocolWhitelist = const ['http'];
//JustAudioMediaKit.bufferSize = 128;
_deezerAPI = initArgs.deezerAPI;
_androidAuto = AndroidAuto(deezerAPI: _deezerAPI);
@ -624,9 +625,10 @@ class AudioPlayerTask extends BaseAudioHandler {
//This just returns fake url that contains metadata
List? playbackDetails = jsonDecode(mediaItem.extras!['playbackDetails']);
if ((playbackDetails ?? []).length < 2) {
throw Exception('not enough playback details');
}
// DON'T CARE, WE DON'T NEED THOSE WITH NEW API
// if ((playbackDetails ?? []).length < 2) {
// throw Exception('not enough playback details');
// }
//String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
// final uri = Uri.http('localhost:36958', '', {
@ -642,10 +644,12 @@ class AudioPlayerTask extends BaseAudioHandler {
return DeezerAudioSource(
getQuality: () => _currentQuality,
trackId: mediaItem.id,
trackToken: mediaItem.extras!['trackToken'] ?? '',
trackTokenExpiration: mediaItem.extras!['trackTokenExpiration'] ?? 0,
md5origin: playbackDetails![0],
mediaVersion: playbackDetails[1],
// THESE NEXT 4 CAN BE NULL.
// IF THEY ARE NULL, THEY'RE GONNA BE FETCHED LATER ON.
trackToken: mediaItem.extras!['trackToken'],
trackTokenExpiration: mediaItem.extras!['trackTokenExpiration'],
md5origin: playbackDetails?[0],
mediaVersion: playbackDetails?[1],
onStreamObtained: (qualityInfo) =>
customEvent.add({'action': 'streamInfo', 'data': qualityInfo}),
);

View File

@ -181,6 +181,15 @@ class Settings {
Settings();
AudioQuality maxQualityFor(
AudioQuality quality, bool canStreamHQ, bool canStreamLossless) {
if (canStreamLossless) return quality;
final maxQuality =
canStreamHQ ? AudioQuality.MP3_320 : AudioQuality.MP3_128;
return _minQuality(quality, maxQuality);
}
void checkQuality(bool canStreamHQ, bool canStreamLossless) {
if (canStreamLossless) return;

View File

@ -0,0 +1,249 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/utils.dart';
import 'package:pointycastle/export.dart' as pc;
import 'package:pointycastle/src/platform_check/platform_check.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:freezer/translations.i18n.dart';
class LoginOnOtherDevice extends StatefulWidget {
const LoginOnOtherDevice({super.key});
@override
State<LoginOnOtherDevice> createState() => _LoginOnOtherDeviceState();
}
class _LoginOnOtherDeviceState extends State<LoginOnOtherDevice> {
String _ipAddress = '';
String _code = '';
String? _error;
bool _loading = false;
bool _step2 = false;
final GlobalKey<FormState> _formKey = GlobalKey();
late final encrypt.Key key;
void _doHandshake() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
// generate keypair
final keyGen = pc.RSAKeyGenerator();
final secureRandom = pc.SecureRandom('Fortuna')
..seed(pc.KeyParameter(
Platform.instance.platformEntropySource().getBytes(32)));
keyGen.init(pc.ParametersWithRandom(
pc.RSAKeyGeneratorParameters(BigInt.parse('65537'), 2048, 64),
secureRandom));
final keyPair = keyGen.generateKeyPair();
final privKey = keyPair.privateKey as pc.RSAPrivateKey;
final pubKey = keyPair.publicKey as pc.RSAPublicKey;
// initial handshake
final hash = Hmac(sha512, Utils.splitDigits(int.parse(_code)))
.convert(Utils.serializeBigInt(pubKey.exponent! ^ pubKey.modulus!))
.toString();
late final Response res;
try {
res = await Dio().post('http://$_ipAddress/',
data: jsonEncode({
'_': 'handshake',
'pubKey': [
base64.encode(Utils.serializeBigInt(pubKey.modulus!)),
base64.encode(Utils.serializeBigInt(pubKey.exponent!))
],
'hash': hash,
}),
options: Options(responseType: ResponseType.json));
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
if (e.response?.statusCode == 403) {
setState(() {
_error = 'Wrong code';
_loading = false;
});
return;
}
final data = e.response?.data as Map?;
if (data != null && data['message'] is String) {
setState(() {
_error = 'Request failed: ${data['message']}';
_loading = false;
});
return;
}
setState(() {
_error = 'Request failed: $e';
_loading = false;
});
return;
}
}
print(res);
final data = res.data as Map;
if (!data['ok']) {
setState(() {
_error = data['message'] as String? ?? 'Unknown server error';
_loading = false;
});
return;
}
final encryptedKey = data['key'] as String;
final encrypter = encrypt.Encrypter(encrypt.RSA(privateKey: privKey));
key = encrypt.Key(Uint8List.fromList(
encrypter.decryptBytes(encrypt.Encrypted.fromBase64(encryptedKey))));
setState(() {
_loading = false;
_step2 = true;
});
}
void _loginUsingArl(String arl) async {
setState(() {
_loading = true;
});
final encrypter = encrypt.Encrypter(
encrypt.AES(key),
);
final iv = encrypt.IV.fromSecureRandom(16);
final encryptedARL = encrypter.encrypt(arl, iv: iv);
// send it over with dio
late final Response res;
try {
res = await Dio().post('http://$_ipAddress/',
data: jsonEncode({
'_': 'arl',
'arl': encryptedARL.base64,
'iv': iv.base64,
}),
options: Options(responseType: ResponseType.json));
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
final data = e.response?.data as Map?;
if (data != null && data['message'] is String) {
setState(() {
_error = 'Request failed: ${data['message']}';
_loading = false;
});
}
}
setState(() {
_error = 'Request failed: $e';
_loading = false;
});
return;
}
if (!res.data['ok']) {
setState(() {
_error = res.data['message'] as String? ?? 'Unknown server error.';
_loading = false;
});
return;
}
Navigator.pop(context);
ScaffoldMessenger.of(context).snack('Logged in successfully!'.i18n);
}
void _cancel() async {
if (_step2) {
final hash =
Hmac(sha512, key.bytes).convert(utf8.encode(_code)).toString();
await Dio().post('http://$_ipAddress/',
data: jsonEncode({'_': 'cancel', 'hash': hash}));
}
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Login on other device'.i18n),
content: _step2
? Column(mainAxisSize: MainAxisSize.min, children: [
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
OutlinedButton(
onPressed:
_loading ? null : () => _loginUsingArl(settings.arl!),
child: Text('Login with current ARL'.i18n)),
])
: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
TextFormField(
validator: (value) {
value ??= '';
final p = value.split(':');
if (p.length != 2) return 'Invalid IP and Port';
final ip = p[0];
final port = int.tryParse(p[1]);
if (port == null || port > 65535) return 'Invalid port';
final ipParts = ip.split('.');
if (ipParts.length != 4) return 'Invalid IP';
for (final part in ipParts) {
final a = int.tryParse(part);
if (a == null || a < 0 || a > 255) {
return 'Invalid IP';
}
}
return null;
},
onChanged: (value) => _ipAddress = value,
decoration:
InputDecoration(label: Text('IP Address'.i18n)),
),
TextFormField(
validator: (value) {
value ??= '';
if (value.length != 6 || int.tryParse(value) == null) {
return 'Invalid code';
}
return null;
},
onChanged: (value) => _code = value,
decoration: InputDecoration(label: Text('Code'.i18n)),
)
]),
),
actions: [
if (!_step2)
TextButton(
onPressed: _loading ? null : _doHandshake,
child: Text('Login'.i18n),
),
TextButton(onPressed: _cancel, child: Text('Cancel'.i18n))
],
);
}
}

View File

@ -4,11 +4,16 @@ import 'dart:io';
import 'dart:math';
import 'package:async/async.dart';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:encrypt/encrypt_io.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/utils.dart';
import 'package:logging/logging.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:rxdart/rxdart.dart';
@ -216,7 +221,7 @@ class _LoginWidgetState extends State<LoginWidget> {
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16.0),
),
const SizedBox(height: 16.0),
const SizedBox(height: 32.0),
ElevatedButton(
child:
Text('Login using other device'.i18n),
@ -242,12 +247,13 @@ class _LoginWidgetState extends State<LoginWidget> {
// const SizedBox(height: 2.0),
// only supported on android
if (Platform.isAndroid)
if (Platform.isAndroid) ...[
ElevatedButton(
onPressed: _loginBrowser,
child: Text('Login using browser'.i18n),
),
const SizedBox(height: 2.0),
const SizedBox(height: 2.0),
],
ElevatedButton(
child: Text('Login using token'.i18n),
onPressed: () {
@ -441,16 +447,14 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
late int _code;
late Timer _codeTimer;
final _timerNotifier = ValueNotifier<double>(0.0);
bool step2 = false;
bool _step2 = false;
final _logger = Logger('OtherDeviceLogin');
void _generateCode() {
_code = Random.secure().nextInt(899999) + 100000;
}
Future<void> _initServer() async {
_server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
_deviceIP = await NetworkInfo().getWifiIP();
void _initTimer() {
_generateCode();
const tps = 30 * 1000 / 50;
_codeTimer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
@ -460,14 +464,30 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
setState(() => _generateCode());
}
});
}
Future<void> _initServer() async {
_server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
_deviceIP = await NetworkInfo().getWifiIP();
_initTimer();
encrypt.Key? key;
void invalidRequest(HttpRequest request) {
request.response.statusCode = 400;
request.response.close();
}
_serverSubscription = _server.listen((request) async {
final buffer = Uint8List(0);
final buffer = <int>[];
final reqCompleter = Completer<void>();
final subs = request.listen(
(data) {
if (data.length + buffer.length > 8192) {
_logger.severe('Request too big!');
request.response.close();
reqCompleter.completeError("Request too big!");
return;
}
buffer.addAll(data);
@ -476,13 +496,129 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
reqCompleter.complete();
},
);
await reqCompleter.future;
subs.cancel();
try {
jsonDecode(utf8.decode(buffer));
await reqCompleter.future;
} catch (e) {
return;
}
subs.cancel();
late final Map data;
try {
data = jsonDecode(utf8.decode(buffer));
} catch (e) {
_logger.severe('Error $e');
request.response.close();
return invalidRequest(request);
}
// check if data is correct
if (!data.containsKey('_') || data['_'] is! String) {
return invalidRequest(request);
}
print(data);
switch (data['_']) {
case 'handshake':
if (key != null) {
request.response.statusCode = 400;
request.response.headers.contentType = ContentType.json;
request.response.write(jsonEncode({
'ok': false,
'message': 'Another client has already done the handshake.'
}));
request.response.close();
return;
}
if (!data.containsKey('pubKey') ||
data['pubKey'] is! List ||
data['pubKey'].length != 2 ||
!data.containsKey('hash') ||
data['hash'] is! String) {
return invalidRequest(request);
}
final pubKey = RSAPublicKey(
Utils.deserializeBigInt(base64.decode(data['pubKey'][0])),
Utils.deserializeBigInt(base64.decode(data['pubKey'][1])));
final hash = Hmac(sha512, Utils.splitDigits(_code))
.convert(
Utils.serializeBigInt(pubKey.exponent! ^ pubKey.modulus!))
.toString();
if (hash != data['hash']) {
request.response.statusCode = 403;
request.response.write(
jsonEncode({'ok': false, 'message': 'Hash doesn\'t match'}));
request.response.close();
return;
}
final encrypter = encrypt.Encrypter(encrypt.RSA(publicKey: pubKey));
key = encrypt.Key.fromSecureRandom(32);
final encryptedKey = encrypter.encryptBytes(key!.bytes);
request.response.headers.contentType = ContentType.json;
request.response.write(jsonEncode({
'ok': true,
'key': encryptedKey.base64,
}));
request.response.close();
_codeTimer.cancel();
setState(() => _step2 = true);
break;
case 'arl':
if (!data.containsKey('arl') ||
data['arl'] is! String ||
!data.containsKey('iv') ||
data['iv'] is! String) {
return invalidRequest(request);
}
final encryptedArl = data['arl'] as String;
final iv = data['iv'] as String;
final encrypter = encrypt.Encrypter(encrypt.AES(key!));
final decryptedArl = encrypter.decrypt(
encrypt.Encrypted.fromBase64(encryptedArl),
iv: encrypt.IV.fromBase64(iv));
if (decryptedArl.length != 192) {
request.response.headers.contentType = ContentType.json;
request.response.write(
jsonEncode({'ok': false, 'message': 'Wrong ARL length!'}));
request.response.close();
return;
}
request.response.headers.contentType = ContentType.json;
request.response.write(jsonEncode({'ok': true}));
request.response.close();
settings.arl = decryptedArl;
Navigator.pop(context);
widget.callback();
break;
case 'cancel':
if (!data.containsKey('hash') || data['hash'] is! String) {
return invalidRequest(request);
}
final hash = Hmac(sha512, key!.bytes)
.convert(utf8.encode(_code.toString()))
.toString();
if (hash != data['hash']) {
request.response.statusCode = 403;
request.response.write(
jsonEncode({'ok': false, 'message': 'Hash doesn\'t match'}));
request.response.close();
return;
}
key = null;
_initTimer();
setState(() {
_step2 = false;
});
request.response.close();
break;
}
});
}
@ -506,9 +642,18 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
return AlertDialog(
title: Text('Login using other device'.i18n),
contentPadding: const EdgeInsets.only(top: 12),
content: step2
? Text('Please follow the on-screen instructions'.i18n,
style: Theme.of(context).textTheme.bodyLarge)
content: _step2
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const LinearProgressIndicator(),
Padding(
padding: const EdgeInsets.fromLTRB(24, 18, 24, 24),
child: Text('Please follow the on-screen instructions'.i18n,
style: Theme.of(context).textTheme.bodyLarge),
),
])
: FutureBuilder(
future: _serverReady,
builder: (context, snapshot) {
@ -523,7 +668,7 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
ValueListenableBuilder(
valueListenable: _timerNotifier,
builder: (context, value, _) =>
LinearProgressIndicator(value: value),
LinearProgressIndicator(value: 1.0 - value),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 18, 24, 24),
@ -532,24 +677,23 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'On your other device, go to Freezer\'s settings > General > Login on other device, input the parameters below and follow the on-screen instructions.'
.i18n),
'On your other device, go to Freezer\'s settings > General > Login on other device and input the parameters below'
.i18n,
style: Theme.of(context).textTheme.bodyLarge,
),
RichText(
textAlign: TextAlign.start,
text: TextSpan(
style:
Theme.of(context).textTheme.bodyMedium,
children: [
TextSpan(text: 'IP Address: '.i18n),
TextSpan(
text:
_deviceIP ?? 'Could not get IP!',
style: TextStyle(fontSize: 32.sp)),
const TextSpan(text: ':'),
TextSpan(
text: _server.port.toString(),
style: TextStyle(fontSize: 32.sp)),
])),
TextSpan(text: 'IP Address: '.i18n),
TextSpan(
text:
'${_deviceIP ?? 'Could not get IP!'}:${_server.port}',
style: Theme.of(context)
.textTheme
.displaySmall),
])),
RichText(
text: TextSpan(
style:
@ -558,7 +702,9 @@ class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
TextSpan(text: 'Code: '.i18n),
TextSpan(
text: '$_code',
style: TextStyle(fontSize: 32.sp)),
style: Theme.of(context)
.textTheme
.displaySmall),
])),
],
),

View File

@ -28,7 +28,7 @@ FutureOr openScreenByURL(BuildContext context, String url) async {
switch (res.type) {
case DeezerLinkType.TRACK:
Track t = await deezerAPI.track(res.id);
Track t = await deezerAPI.track(res.id!);
MenuSheet(context).defaultTrackMenu(t);
break;
case DeezerLinkType.ALBUM:

View File

@ -11,6 +11,7 @@ import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttericon/web_symbols_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/ui/login_on_other_device.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:scrobblenaut/scrobblenaut.dart';
@ -1051,7 +1052,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
settings.downloadPath ?? 'Not set, click here to set!'.i18n),
onTap: () async {
//Check permissions
if (!await downloadManager.checkPermission()) {
if (!await DownloadManager.checkPermission()) {
return;
}
DownloadManager.getDirectory('Pick-a-Path'.i18n).then((path) {
@ -1448,6 +1449,13 @@ class _GeneralSettingsState extends State<GeneralSettings> {
setState(() {});
},
),
ListTile(
title: Text('Login on other device'.i18n),
leading: const Icon(Icons.send_to_mobile),
onTap: () => showDialog(
context: context,
builder: (context) => const LoginOnOtherDevice()),
),
ListTile(
title: Text(
'Log out'.i18n,

30
lib/utils.dart Normal file
View File

@ -0,0 +1,30 @@
import 'dart:typed_data';
class Utils {
static List<int> splitDigits(int number) {
final digits = <int>[];
while (number != 0) {
digits.add(number % 10);
number = number ~/ 10;
}
return digits;
}
static Uint8List serializeBigInt(BigInt bi) {
Uint8List array = Uint8List((bi.bitLength / 8).ceil());
for (int i = 0; i < array.length; i++) {
array[i] = (bi >> (i * 8)).toUnsigned(8).toInt();
}
return array;
}
static BigInt deserializeBigInt(Uint8List array) {
var bi = BigInt.zero;
for (var byte in array.reversed) {
bi <<= 8;
bi |= BigInt.from(byte);
}
return bi;
}
}

View File

@ -221,10 +221,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.18.0"
connectivity_plus:
dependency: "direct main"
description:
@ -362,15 +362,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.3"
equalizer:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "84c15ca304a8129a1cad5a6891059fb411f0fc55"
url: "https://github.com/gladson97/equalizer.git"
source: git
version: "0.0.2+2"
equatable:
dependency: transitive
description:
@ -505,22 +496,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350
url: "https://pub.dev"
source: hosted
version: "5.8.0"
flutter_isolate:
dependency: "direct main"
description:
name: flutter_isolate
sha256: "994ddec596da4ca12ca52154fd59404077584643eb7e3f1008a55fda9fe0b76b"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
flutter_lints:
dependency: "direct dev"
description:
@ -901,10 +876,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
mime:
dependency: transitive
description:
@ -1085,42 +1060,50 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
sha256: "3c84d49f0a5e1915364707159ab71f11b3b8a429532176d3a6248a45718ad4f9"
url: "https://pub.dev"
source: hosted
version: "10.4.5"
version: "11.2.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
sha256: a5ebaa420cee8fd880ef10dedd42c6b3f493e7dbe27d7e0a7e1798669373082a
url: "https://pub.dev"
source: hosted
version: "10.3.6"
version: "12.0.4"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
sha256: "6ca25ee52518a8a26e80aaefe3c71caf6e2dfd809c1b20900d0882df6faed36e"
url: "https://pub.dev"
source: hosted
version: "9.1.4"
version: "9.3.1"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c"
url: "https://pub.dev"
source: hosted
version: "3.12.0"
version: "4.1.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
version: "0.2.1"
petitparser:
dependency: transitive
description:
@ -1355,18 +1338,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
stream_transform:
dependency: transitive
description:
@ -1403,10 +1386,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.6.1"
time:
dependency: transitive
description:
@ -1603,10 +1586,10 @@ packages:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "0.3.0"
web_socket_channel:
dependency: transitive
description:
@ -1615,6 +1598,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.0"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: d81b68e88cc353e546afb93fb38958e3717282c5ac6e5d3be4a4aef9fc3c1413
url: "https://pub.dev"
source: hosted
version: "4.5.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
url: "https://pub.dev"
source: hosted
version: "3.15.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
url: "https://pub.dev"
source: hosted
version: "2.9.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
url: "https://pub.dev"
source: hosted
version: "3.12.0"
win32:
dependency: transitive
description:
@ -1656,5 +1671,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.1.3 <4.0.0"
flutter: ">=3.13.0"
dart: ">=3.2.3 <4.0.0"
flutter: ">=3.16.6"