put common deezer audio logic in DeezerAudio class
This commit is contained in:
parent
2862c9ec05
commit
77d6a5a51d
318
lib/api/deezer_audio.dart
Normal file
318
lib/api/deezer_audio.dart
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:dart_blowfish/dart_blowfish.dart';
|
||||||
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
import 'package:freezer/api/definitions.dart';
|
||||||
|
import 'package:freezer/settings.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<
|
||||||
|
({
|
||||||
|
String md5origin,
|
||||||
|
String trackId,
|
||||||
|
String mediaVersion,
|
||||||
|
AudioQuality quality,
|
||||||
|
Uri uri
|
||||||
|
})> fallbackUrl({
|
||||||
|
required String md5origin,
|
||||||
|
required AudioQuality preferredQuality,
|
||||||
|
required String trackId,
|
||||||
|
required String mediaVersion,
|
||||||
|
}) async {
|
||||||
|
_logger.finer("called _fallbackUrl()");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final rec = await qualityFallback(
|
||||||
|
md5origin, preferredQuality, trackId, mediaVersion);
|
||||||
|
return (
|
||||||
|
md5origin: md5origin,
|
||||||
|
trackId: trackId,
|
||||||
|
mediaVersion: mediaVersion,
|
||||||
|
quality: rec.$1,
|
||||||
|
uri: rec.$2
|
||||||
|
);
|
||||||
|
} on QualityException {
|
||||||
|
_logger.warning("quality fallback failed! trying trackId fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map? privateJson;
|
||||||
|
try {
|
||||||
|
// TRACK ID FALLBACK
|
||||||
|
final data = await deezerAPI
|
||||||
|
.callApi("deezer.pageTrack", params: {"sng_id": trackId});
|
||||||
|
privateJson = data["results"]["DATA"];
|
||||||
|
if (privateJson!.containsKey("FALLBACK")) {
|
||||||
|
String fallbackId = privateJson["FALLBACK"]["SNG_ID"];
|
||||||
|
if (fallbackId != trackId) {
|
||||||
|
final newPrivate =
|
||||||
|
await deezerAPI.callApi("song.getListData", params: {
|
||||||
|
"sng_ids": [fallbackId]
|
||||||
|
});
|
||||||
|
final Map trackData = newPrivate["results"]["data"][0];
|
||||||
|
trackId = trackData["SNG_ID"];
|
||||||
|
md5origin = trackData["MD5_ORIGIN"];
|
||||||
|
mediaVersion = trackData["MEDIA_VERSION"];
|
||||||
|
return await fallbackUrl(
|
||||||
|
md5origin: md5origin,
|
||||||
|
preferredQuality: preferredQuality,
|
||||||
|
trackId: trackId,
|
||||||
|
mediaVersion: mediaVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
_logger.warning("ID fallback failed! Trying ISRC fallback!", e, st);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data =
|
||||||
|
await deezerAPI.callPublicApi("track/isrc:${privateJson!["ISRC"]!}");
|
||||||
|
final newId = data["id"] as int;
|
||||||
|
if (newId == int.parse(trackId)) throw Exception();
|
||||||
|
|
||||||
|
final newPrivate = await deezerAPI.callApi("song.getListData", params: {
|
||||||
|
"sng_ids": [newId]
|
||||||
|
});
|
||||||
|
final trackData = newPrivate["results"]["data"][0] as Map;
|
||||||
|
trackId = trackData["SNG_ID"];
|
||||||
|
md5origin = trackData["MD5_ORIGIN"];
|
||||||
|
mediaVersion = trackData["MEDIA_VERSION"];
|
||||||
|
return await fallbackUrl(
|
||||||
|
md5origin: md5origin,
|
||||||
|
preferredQuality: preferredQuality,
|
||||||
|
trackId: trackId,
|
||||||
|
mediaVersion: mediaVersion,
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
_logger.severe("ISRC Fallback failed, track unavailable!", e, st);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(
|
||||||
|
"Every known method of fallback failed, track unavailable!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(AudioQuality, Uri)> qualityFallback(String md5origin,
|
||||||
|
AudioQuality currentQuality, String trackId, String mediaVersion) async {
|
||||||
|
// only use url generation with MP3_128
|
||||||
|
currentQuality = AudioQuality.MP3_128;
|
||||||
|
final genUri =
|
||||||
|
_generateTrackUri(md5origin, currentQuality, trackId, mediaVersion);
|
||||||
|
|
||||||
|
final req = await http.head(genUri, headers: {
|
||||||
|
'User-Agent': DeezerAPI.userAgent,
|
||||||
|
'Accept-Language': '*',
|
||||||
|
'Accept': '*/*'
|
||||||
|
});
|
||||||
|
int rc = req.statusCode;
|
||||||
|
if (rc > 400) {
|
||||||
|
_logger.warning(
|
||||||
|
"quality fallback, response code: $rc, current quality: $currentQuality");
|
||||||
|
switch (currentQuality) {
|
||||||
|
case AudioQuality.FLAC:
|
||||||
|
currentQuality = AudioQuality.MP3_320;
|
||||||
|
break;
|
||||||
|
case AudioQuality.MP3_320:
|
||||||
|
currentQuality = AudioQuality.MP3_128;
|
||||||
|
break;
|
||||||
|
case AudioQuality.MP3_128:
|
||||||
|
default:
|
||||||
|
throw QualityException("No quality to fallback to!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await qualityFallback(
|
||||||
|
md5origin, currentQuality, trackId, mediaVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (currentQuality, genUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri _generateTrackUri(String md5origin, AudioQuality quality, String trackId,
|
||||||
|
String mediaVersion) {
|
||||||
|
int magic = 164;
|
||||||
|
// step 1
|
||||||
|
final step1 = <int>[
|
||||||
|
...md5origin.codeUnits,
|
||||||
|
magic,
|
||||||
|
...quality.toDeezerQualityInt().toString().codeUnits,
|
||||||
|
magic,
|
||||||
|
...trackId.codeUnits,
|
||||||
|
magic,
|
||||||
|
...mediaVersion.codeUnits,
|
||||||
|
];
|
||||||
|
|
||||||
|
// get md5 hash of step1
|
||||||
|
final md5hex = md5.convert(step1).toString().toLowerCase();
|
||||||
|
|
||||||
|
// step2
|
||||||
|
final step2 = <int>[
|
||||||
|
...md5hex.codeUnits,
|
||||||
|
magic,
|
||||||
|
...step1,
|
||||||
|
magic,
|
||||||
|
];
|
||||||
|
|
||||||
|
// pad step2 with dots to get correct length
|
||||||
|
while (step2.length % 16 > 0) {
|
||||||
|
// step2.addAll(('.' * (step2.length % 16)).codeUnits);
|
||||||
|
step2.add(46);
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt with AES ECB
|
||||||
|
final k = Uint8List.fromList('jo6aey6haid2Teih'.codeUnits);
|
||||||
|
final encrypter = Encrypter(AES(Key(k), mode: AESMode.ecb, padding: null));
|
||||||
|
final String step3 = encrypter
|
||||||
|
.encryptBytes(step2, iv: IV(Uint8List(8)))
|
||||||
|
.base16
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
final uri =
|
||||||
|
Uri.https('e-cdns-proxy-${md5origin[0]}.dzcdn.net', '/mobile/1/$step3');
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<int> getKey(String id) {
|
||||||
|
final secret = utf8.encode('g4el58wc0zvf9na1');
|
||||||
|
final idmd5 =
|
||||||
|
utf8.encode(md5.convert(utf8.encode(id)).toString().toLowerCase());
|
||||||
|
final buffer = <int>[];
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
final s0 = idmd5[i];
|
||||||
|
final s1 = idmd5[i + 16];
|
||||||
|
final s2 = secret[i];
|
||||||
|
buffer.add(s0 ^ s1 ^ s2);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<int> decryptChunk(List<int> key, List<int> data) {
|
||||||
|
final Uint8List iv =
|
||||||
|
Uint8List.fromList(const [00, 01, 02, 03, 04, 05, 06, 07]);
|
||||||
|
final bf = Blowfish(
|
||||||
|
key: Uint8List.fromList(key), mode: Mode.cbc, padding: Padding.none)
|
||||||
|
..setIv(iv);
|
||||||
|
Uint8List decrypted = bf.decode(data, returnType: Type.uInt8Array);
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<List<int>> decryptionStream(Stream<List<int>> source,
|
||||||
|
{required int start, required String trackId}) async* {
|
||||||
|
var dropBytes = start % 2048;
|
||||||
|
final deezerStart = start - dropBytes;
|
||||||
|
int counter = deezerStart ~/ chunkSize;
|
||||||
|
final buffer = List<int>.empty(growable: true);
|
||||||
|
final key = getKey(trackId);
|
||||||
|
|
||||||
|
await for (var bytes in source) {
|
||||||
|
buffer.addAll(bytes);
|
||||||
|
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < buffer.length; i += chunkSize) {
|
||||||
|
if (buffer.length <= i + chunkSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes = buffer.sublist(i, i + chunkSize);
|
||||||
|
|
||||||
|
if ((counter % 3) == 0) {
|
||||||
|
bytes = decryptChunk(key, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropBytes > 0) {
|
||||||
|
bytes = bytes.sublist(dropBytes);
|
||||||
|
dropBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter++;
|
||||||
|
yield bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < buffer.length) {
|
||||||
|
buffer.removeRange(0, i);
|
||||||
|
} else {
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add remaining items in buffer
|
||||||
|
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
if (dropBytes > 0) {
|
||||||
|
yield buffer.sublist(dropBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uri?> getUrl(String trackToken, int expiration) =>
|
||||||
|
getTrackUrl(deezerAPI, trackId, trackToken, expiration, quality: quality);
|
||||||
|
|
||||||
|
static Future<Uri?> getTrackUrl(
|
||||||
|
DeezerAPI deezerAPI,
|
||||||
|
String trackId,
|
||||||
|
String trackToken,
|
||||||
|
int expiration, {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
final newTrack = await deezerAPI.track(trackId);
|
||||||
|
actualTrackToken = newTrack.trackToken!;
|
||||||
|
} else {
|
||||||
|
actualTrackToken = trackToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = await deezerAPI.getTrackUrl(
|
||||||
|
actualTrackToken, quality.toDeezerQualityString());
|
||||||
|
if (res.error != null) {
|
||||||
|
_logger.warning('Error while getting track url: ${res.error!}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (res.sources == null) {
|
||||||
|
_logger.warning('Error while getting track url: No sources!');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uri.parse(res.sources![0].url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:encrypt/encrypt.dart';
|
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
import 'package:freezer/api/deezer_audio.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:dart_blowfish/dart_blowfish.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
typedef _IsolateMessage = (
|
typedef _IsolateMessage = (
|
||||||
|
|
@ -27,7 +23,6 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
static final _logger = Logger("DeezerAudioSource");
|
static final _logger = Logger("DeezerAudioSource");
|
||||||
|
|
||||||
late AudioQuality Function() _getQuality;
|
late AudioQuality Function() _getQuality;
|
||||||
late AudioQuality? _initialQuality;
|
|
||||||
late String _trackId;
|
late String _trackId;
|
||||||
late String _md5origin;
|
late String _md5origin;
|
||||||
late String _mediaVersion;
|
late String _mediaVersion;
|
||||||
|
|
@ -36,7 +31,7 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
final StreamInfoCallback? onStreamObtained;
|
final StreamInfoCallback? onStreamObtained;
|
||||||
|
|
||||||
// some cache
|
// some cache
|
||||||
AudioQuality? _currentQuality;
|
final DeezerAudio _deezerAudio;
|
||||||
int? _cachedSourceLength;
|
int? _cachedSourceLength;
|
||||||
String? _cachedContentType;
|
String? _cachedContentType;
|
||||||
Uri? _downloadUrl;
|
Uri? _downloadUrl;
|
||||||
|
|
@ -49,255 +44,24 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
required this.trackToken,
|
required this.trackToken,
|
||||||
required this.trackTokenExpiration,
|
required this.trackTokenExpiration,
|
||||||
this.onStreamObtained,
|
this.onStreamObtained,
|
||||||
}) {
|
}) : _deezerAudio = DeezerAudio(
|
||||||
|
deezerAPI: deezerAPI,
|
||||||
|
md5origin: md5origin,
|
||||||
|
quality: getQuality.call(),
|
||||||
|
trackId: trackId,
|
||||||
|
mediaVersion: mediaVersion,
|
||||||
|
) {
|
||||||
_getQuality = getQuality;
|
_getQuality = getQuality;
|
||||||
_initialQuality = quality;
|
|
||||||
_trackId = trackId;
|
_trackId = trackId;
|
||||||
_md5origin = md5origin;
|
_md5origin = md5origin;
|
||||||
_mediaVersion = mediaVersion;
|
_mediaVersion = mediaVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioQuality? get quality => _currentQuality;
|
AudioQuality? get quality => _deezerAudio.quality;
|
||||||
String get trackId => _trackId;
|
String get trackId => _trackId;
|
||||||
String get md5origin => _md5origin;
|
String get md5origin => _md5origin;
|
||||||
String get mediaVersion => _mediaVersion;
|
String get mediaVersion => _mediaVersion;
|
||||||
|
|
||||||
static const chunkSize = 2048;
|
|
||||||
|
|
||||||
void _updateTrackData(Map trackData) {
|
|
||||||
_trackId = trackData["SNG_ID"];
|
|
||||||
_md5origin = trackData["MD5_ORIGIN"];
|
|
||||||
_mediaVersion = trackData["MEDIA_VERSION"];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri> _fallbackUrl() async {
|
|
||||||
_logger.finer("called _fallbackUrl()");
|
|
||||||
try {
|
|
||||||
return await _qualityFallback();
|
|
||||||
} on QualityException {
|
|
||||||
_logger.warning("quality fallback failed! trying trackId fallback");
|
|
||||||
_currentQuality = _initialQuality;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map? privateJson;
|
|
||||||
try {
|
|
||||||
// TRACK ID FALLBACK
|
|
||||||
final data = await deezerAPI
|
|
||||||
.callApi("deezer.pageTrack", params: {"sng_id": trackId});
|
|
||||||
privateJson = data["results"]["DATA"];
|
|
||||||
if (privateJson!.containsKey("FALLBACK")) {
|
|
||||||
String fallbackId = privateJson["FALLBACK"]["SNG_ID"];
|
|
||||||
if (fallbackId != trackId) {
|
|
||||||
final newPrivate =
|
|
||||||
await deezerAPI.callApi("song.getListData", params: {
|
|
||||||
"sng_ids": [fallbackId]
|
|
||||||
});
|
|
||||||
final Map trackData = newPrivate["results"]["data"][0];
|
|
||||||
_updateTrackData(trackData);
|
|
||||||
return await _fallbackUrl();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
_logger.warning("ID fallback failed! Trying ISRC fallback!", e, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final data =
|
|
||||||
await deezerAPI.callPublicApi("track/isrc:${privateJson!["ISRC"]!}");
|
|
||||||
final newId = data["id"] as int;
|
|
||||||
if (newId == int.parse(trackId)) throw Exception();
|
|
||||||
|
|
||||||
final newPrivate = await deezerAPI.callApi("song.getListData", params: {
|
|
||||||
"sng_ids": [newId]
|
|
||||||
});
|
|
||||||
final trackData = newPrivate["results"]["data"][0] as Map;
|
|
||||||
_updateTrackData(trackData);
|
|
||||||
return await _fallbackUrl();
|
|
||||||
} catch (e, st) {
|
|
||||||
_logger.severe("ISRC Fallback failed, track unavailable!", e, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception(
|
|
||||||
"Every known method of fallback failed, track unavailable!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri> _qualityFallback() async {
|
|
||||||
// only use url generation with MP3_128
|
|
||||||
_currentQuality = AudioQuality.MP3_128;
|
|
||||||
final genUri = _generateTrackUri();
|
|
||||||
|
|
||||||
final req = await http.head(genUri, headers: {
|
|
||||||
'User-Agent': DeezerAPI.userAgent,
|
|
||||||
'Accept-Language': '*',
|
|
||||||
'Accept': '*/*'
|
|
||||||
});
|
|
||||||
int rc = req.statusCode;
|
|
||||||
if (rc > 400) {
|
|
||||||
_logger.warning(
|
|
||||||
"quality fallback, response code: $rc, current quality: $quality");
|
|
||||||
switch (_currentQuality) {
|
|
||||||
case AudioQuality.FLAC:
|
|
||||||
_currentQuality = AudioQuality.MP3_320;
|
|
||||||
break;
|
|
||||||
case AudioQuality.MP3_320:
|
|
||||||
_currentQuality = AudioQuality.MP3_128;
|
|
||||||
break;
|
|
||||||
case AudioQuality.MP3_128:
|
|
||||||
default:
|
|
||||||
_currentQuality = null;
|
|
||||||
throw QualityException("No quality to fallback to!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _qualityFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
return genUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri _generateTrackUri() {
|
|
||||||
int magic = 164;
|
|
||||||
// step 1
|
|
||||||
final step1 = <int>[
|
|
||||||
...md5origin.codeUnits,
|
|
||||||
magic,
|
|
||||||
...quality!.toDeezerQualityInt().toString().codeUnits,
|
|
||||||
magic,
|
|
||||||
...trackId.codeUnits,
|
|
||||||
magic,
|
|
||||||
...mediaVersion.codeUnits,
|
|
||||||
];
|
|
||||||
|
|
||||||
// get md5 hash of step1
|
|
||||||
final md5hex = md5.convert(step1).toString().toLowerCase();
|
|
||||||
|
|
||||||
// step2
|
|
||||||
final step2 = <int>[
|
|
||||||
...md5hex.codeUnits,
|
|
||||||
magic,
|
|
||||||
...step1,
|
|
||||||
magic,
|
|
||||||
];
|
|
||||||
|
|
||||||
// pad step2 with dots to get correct length
|
|
||||||
while (step2.length % 16 > 0) {
|
|
||||||
// step2.addAll(('.' * (step2.length % 16)).codeUnits);
|
|
||||||
step2.add(46);
|
|
||||||
}
|
|
||||||
|
|
||||||
// encrypt with AES ECB
|
|
||||||
final k = Uint8List.fromList('jo6aey6haid2Teih'.codeUnits);
|
|
||||||
final encrypter = Encrypter(AES(Key(k), mode: AESMode.ecb, padding: null));
|
|
||||||
final String step3 = encrypter
|
|
||||||
.encryptBytes(step2, iv: IV(Uint8List(8)))
|
|
||||||
.base16
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
final uri =
|
|
||||||
Uri.https('e-cdns-proxy-${md5origin[0]}.dzcdn.net', '/mobile/1/$step3');
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<int> getKey(String id) {
|
|
||||||
final secret = utf8.encode('g4el58wc0zvf9na1');
|
|
||||||
final idmd5 =
|
|
||||||
utf8.encode(md5.convert(utf8.encode(id)).toString().toLowerCase());
|
|
||||||
final buffer = <int>[];
|
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
final s0 = idmd5[i];
|
|
||||||
final s1 = idmd5[i + 16];
|
|
||||||
final s2 = secret[i];
|
|
||||||
buffer.add(s0 ^ s1 ^ s2);
|
|
||||||
}
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<int> decryptChunk(List<int> key, List<int> data) {
|
|
||||||
final Uint8List iv =
|
|
||||||
Uint8List.fromList(const [00, 01, 02, 03, 04, 05, 06, 07]);
|
|
||||||
final bf = Blowfish(
|
|
||||||
key: Uint8List.fromList(key), mode: Mode.cbc, padding: Padding.none)
|
|
||||||
..setIv(iv);
|
|
||||||
Uint8List decrypted = bf.decode(data, returnType: Type.uInt8Array);
|
|
||||||
return decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Stream<List<int>> decryptionStream(Stream<List<int>> source,
|
|
||||||
{required int start, required String trackId}) async* {
|
|
||||||
var dropBytes = start % 2048;
|
|
||||||
final deezerStart = start - dropBytes;
|
|
||||||
int counter = deezerStart ~/ chunkSize;
|
|
||||||
final buffer = List<int>.empty(growable: true);
|
|
||||||
final key = getKey(trackId);
|
|
||||||
|
|
||||||
await for (var bytes in source) {
|
|
||||||
buffer.addAll(bytes);
|
|
||||||
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < buffer.length; i += chunkSize) {
|
|
||||||
if (buffer.length <= i + chunkSize) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes = buffer.sublist(i, i + chunkSize);
|
|
||||||
|
|
||||||
if ((counter % 3) == 0) {
|
|
||||||
bytes = decryptChunk(key, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dropBytes > 0) {
|
|
||||||
bytes = bytes.sublist(dropBytes);
|
|
||||||
dropBytes = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
counter++;
|
|
||||||
yield bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < buffer.length) {
|
|
||||||
buffer.removeRange(0, i);
|
|
||||||
} else {
|
|
||||||
buffer.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add remaining items in buffer
|
|
||||||
|
|
||||||
if (buffer.isNotEmpty) {
|
|
||||||
if (dropBytes > 0) {
|
|
||||||
yield buffer.sublist(dropBytes);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> _getUrl() async {
|
|
||||||
final String actualTrackToken;
|
|
||||||
if (DateTime.now().millisecondsSinceEpoch ~/ 1000 > trackTokenExpiration) {
|
|
||||||
if (!deezerAPI.canStreamHQ && !deezerAPI.canStreamLossless) {
|
|
||||||
_logger.fine('using old url generation, token expired.');
|
|
||||||
}
|
|
||||||
final newTrack = await deezerAPI.track(trackId);
|
|
||||||
actualTrackToken = newTrack.trackToken!;
|
|
||||||
} else {
|
|
||||||
actualTrackToken = trackToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
final res = await deezerAPI.getTrackUrl(
|
|
||||||
actualTrackToken, _currentQuality!.toDeezerQualityString());
|
|
||||||
if (res.error != null) {
|
|
||||||
_logger.warning('Error while getting track url: ${res.error!}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (res.sources == null) {
|
|
||||||
_logger.warning('Error while getting track url: No sources!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uri.parse(res.sources![0].url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
||||||
start ??= 0;
|
start ??= 0;
|
||||||
|
|
@ -322,15 +86,16 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
// determine quality to use
|
// determine quality to use
|
||||||
final newQuality = _getQuality.call();
|
final newQuality = _getQuality.call();
|
||||||
|
|
||||||
if (_downloadUrl != null && _currentQuality != newQuality) {
|
if (_downloadUrl != null && quality != newQuality) {
|
||||||
// update currentUrl to get tracks with new quality
|
// update currentUrl to get tracks with new quality
|
||||||
_downloadUrl = null;
|
_downloadUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentQuality = newQuality;
|
_deezerAudio.quality = newQuality;
|
||||||
|
|
||||||
if (_downloadUrl == null) {
|
if (_downloadUrl == null) {
|
||||||
final gottenUrl = await _getUrl();
|
final gottenUrl =
|
||||||
|
await _deezerAudio.getUrl(trackToken, trackTokenExpiration);
|
||||||
if (gottenUrl != null) {
|
if (gottenUrl != null) {
|
||||||
_downloadUrl = gottenUrl;
|
_downloadUrl = gottenUrl;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -338,7 +103,7 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
|
|
||||||
// OLD URL GENERATION
|
// OLD URL GENERATION
|
||||||
try {
|
try {
|
||||||
_downloadUrl = await _fallbackUrl();
|
_downloadUrl = await _deezerAudio.fallback();
|
||||||
} on QualityException {
|
} on QualityException {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
@ -370,10 +135,11 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
throw Exception(await res.stream.bytesToString());
|
throw Exception(await res.stream.bytesToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
int dropBytes = start % chunkSize;
|
int dropBytes = start % DeezerAudio.chunkSize;
|
||||||
_logger.finest(
|
_logger.finest(
|
||||||
"deezerStart: $deezerStart (actual start: $start), end: $end, dropBytes: $dropBytes");
|
"deezerStart: $deezerStart (actual start: $start), end: $end, dropBytes: $dropBytes");
|
||||||
final stream = decryptionStream(res.stream, start: start, trackId: trackId);
|
final stream = DeezerAudio.decryptionStream(res.stream,
|
||||||
|
start: start, trackId: trackId);
|
||||||
|
|
||||||
final cl = res.contentLength! - dropBytes;
|
final cl = res.contentLength! - dropBytes;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,265 @@
|
||||||
|
import 'package:freezer/api/definitions.dart' as d;
|
||||||
|
import 'package:freezer/api/definitions.dart' show AlbumType;
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
part 'database.g.dart';
|
||||||
|
|
||||||
@collection
|
@collection
|
||||||
class Track {
|
class Track {
|
||||||
Id id = Isar.autoIncrement;
|
Id get isarId => int.parse(id);
|
||||||
final String trackId;
|
late final String id;
|
||||||
final String title;
|
|
||||||
final String albumId;
|
|
||||||
final List<String> artistIds;
|
|
||||||
//final DeezerImageDetails albumArt;
|
|
||||||
final int? trackNumber;
|
|
||||||
final bool offline;
|
|
||||||
//final Lyrics lyrics;
|
|
||||||
final bool favorite;
|
|
||||||
final int? diskNumber;
|
|
||||||
final bool explicit;
|
|
||||||
|
|
||||||
Track({
|
// index title for search
|
||||||
required this.trackId,
|
@Index(type: IndexType.value)
|
||||||
required this.title,
|
late final String title;
|
||||||
required this.albumId,
|
late final String albumId;
|
||||||
required this.artistIds,
|
late final List<String> artistIds;
|
||||||
//required this.albumArt,
|
late final DeezerImageDetails albumArt;
|
||||||
required this.trackNumber,
|
late final int? trackNumber;
|
||||||
//required this.lyrics,
|
late final bool offline;
|
||||||
required this.favorite,
|
late final Lyrics? lyrics;
|
||||||
required this.diskNumber,
|
late final bool favorite;
|
||||||
required this.explicit,
|
late final int? diskNumber;
|
||||||
this.offline = true,
|
late final bool explicit;
|
||||||
});
|
|
||||||
|
Track();
|
||||||
|
|
||||||
|
factory Track.from(d.Track t) {
|
||||||
|
return Track()
|
||||||
|
..id = t.id
|
||||||
|
..title = t.title!
|
||||||
|
..albumId = t.album!.id!
|
||||||
|
..artistIds = t.artists!.map((e) => e.id).toList(growable: false)
|
||||||
|
..albumArt = DeezerImageDetails.from(t.albumArt!)
|
||||||
|
..trackNumber = t.trackNumber
|
||||||
|
..offline = t.offline ?? false
|
||||||
|
..lyrics = t.lyrics == null ? null : Lyrics.from(t.lyrics!)
|
||||||
|
..favorite = t.favorite ?? false
|
||||||
|
..diskNumber = t.diskNumber
|
||||||
|
..explicit = t.explicit ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Track to({d.Album? album, List<d.Artist>? artists}) {
|
||||||
|
return d.Track(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
album: album ?? d.Album(id: albumId),
|
||||||
|
artists: artists ??
|
||||||
|
artistIds.map((id) => d.Artist(id: id)).toList(growable: false),
|
||||||
|
albumArt: albumArt.to(),
|
||||||
|
trackNumber: trackNumber,
|
||||||
|
offline: offline,
|
||||||
|
lyrics: lyrics?.to(),
|
||||||
|
favorite: favorite,
|
||||||
|
diskNumber: diskNumber,
|
||||||
|
explicit: explicit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@collection
|
||||||
|
class Album {
|
||||||
|
Id get isarId => int.parse(id);
|
||||||
|
late final String id;
|
||||||
|
|
||||||
|
@Index(type: IndexType.value)
|
||||||
|
late final String title;
|
||||||
|
late final List<String> artistIds;
|
||||||
|
late final List<String> trackIds;
|
||||||
|
late final DeezerImageDetails art;
|
||||||
|
late final int fansCount;
|
||||||
|
late final bool offline;
|
||||||
|
late final bool library;
|
||||||
|
@enumerated
|
||||||
|
late final AlbumType type;
|
||||||
|
late final String releaseDate;
|
||||||
|
|
||||||
|
Album();
|
||||||
|
|
||||||
|
factory Album.from(d.Album album) {
|
||||||
|
return Album()
|
||||||
|
..id = album.id!
|
||||||
|
..title = album.title!
|
||||||
|
..artistIds = album.artists!.map((e) => e.id).toList(growable: false)
|
||||||
|
..trackIds = album.tracks!.map((e) => e.id).toList(growable: false)
|
||||||
|
..art = DeezerImageDetails.from(album.art!)
|
||||||
|
..fansCount = album.fans!
|
||||||
|
..offline = album.offline ?? false
|
||||||
|
..library = album.library ?? false
|
||||||
|
..type = album.type!
|
||||||
|
..releaseDate = album.releaseDate!;
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Album to({List<d.Artist>? artists, List<d.Track>? tracks}) {
|
||||||
|
return d.Album(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
art: art.to(),
|
||||||
|
artists: artists ??
|
||||||
|
artistIds.map((id) => d.Artist(id: id)).toList(growable: false),
|
||||||
|
tracks: tracks ??
|
||||||
|
trackIds.map((id) => d.Track(id: id)).toList(growable: false),
|
||||||
|
fans: fansCount,
|
||||||
|
offline: offline,
|
||||||
|
library: library,
|
||||||
|
type: type,
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@collection
|
||||||
|
class Artist {
|
||||||
|
Id get isarId => int.parse(id);
|
||||||
|
late final String id;
|
||||||
|
|
||||||
|
@Index(type: IndexType.value)
|
||||||
|
late final String name;
|
||||||
|
late final List<String> albumIds;
|
||||||
|
late final List<String> topTracksIds;
|
||||||
|
late final DeezerImageDetails picture;
|
||||||
|
late final int fansCount;
|
||||||
|
late final int albumCount;
|
||||||
|
late final bool offline;
|
||||||
|
late final bool library;
|
||||||
|
late final bool radio;
|
||||||
|
|
||||||
|
Artist();
|
||||||
|
|
||||||
|
factory Artist.from(d.Artist artist) {
|
||||||
|
return Artist()
|
||||||
|
..id = artist.id
|
||||||
|
..name = artist.name!
|
||||||
|
..albumIds = artist.albums!
|
||||||
|
.map<String>((d.Album a) => a.id!)
|
||||||
|
.toList(growable: false)
|
||||||
|
..topTracksIds = artist.topTracks!
|
||||||
|
.map<String>((d.Track t) => t.id)
|
||||||
|
.toList(growable: false)
|
||||||
|
..picture = DeezerImageDetails.from(artist.picture!)
|
||||||
|
..fansCount = artist.fans ?? 0
|
||||||
|
..albumCount = artist.albumCount ?? 0
|
||||||
|
..offline = artist.offline ?? false
|
||||||
|
..library = artist.library ?? false
|
||||||
|
..radio = artist.radio ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@collection
|
||||||
|
class Playlist {
|
||||||
|
late final String id;
|
||||||
|
|
||||||
|
Id get isarId => int.parse(id);
|
||||||
|
|
||||||
|
@Index(type: IndexType.value)
|
||||||
|
late final String title;
|
||||||
|
late final List<String> trackIds;
|
||||||
|
late final DeezerImageDetails image;
|
||||||
|
late final int durationSec;
|
||||||
|
late final String userId;
|
||||||
|
late final String userName;
|
||||||
|
late final int fansCount;
|
||||||
|
late final String description;
|
||||||
|
late final bool library;
|
||||||
|
|
||||||
|
Playlist();
|
||||||
|
|
||||||
|
factory Playlist.from(d.Playlist playlist) {
|
||||||
|
return Playlist()
|
||||||
|
..id = playlist.id
|
||||||
|
..title = playlist.title!
|
||||||
|
..trackIds = playlist.tracks!
|
||||||
|
.map<String>((d.Track t) => t.id)
|
||||||
|
.toList(growable: false)
|
||||||
|
..image = DeezerImageDetails.from(playlist.image! as d.DeezerImageDetails)
|
||||||
|
..durationSec = playlist.duration!.inSeconds
|
||||||
|
..userId = playlist.user!.id!
|
||||||
|
..userName = playlist.user!.name!
|
||||||
|
..fansCount = playlist.fans ?? 0
|
||||||
|
..description = playlist.description ?? ''
|
||||||
|
..library = playlist.library ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Playlist to({List<d.Track>? tracks}) {
|
||||||
|
return d.Playlist(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
tracks: tracks ??
|
||||||
|
trackIds.map((id) => d.Track(id: id)).toList(growable: false),
|
||||||
|
image: image.to(),
|
||||||
|
trackCount: trackIds.length,
|
||||||
|
duration: Duration(seconds: durationSec),
|
||||||
|
user: d.User(id: userId, name: userName),
|
||||||
|
fans: fansCount,
|
||||||
|
library: library,
|
||||||
|
description: description,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@embedded
|
||||||
|
class DeezerImageDetails {
|
||||||
|
late String type;
|
||||||
|
late String md5;
|
||||||
|
|
||||||
|
DeezerImageDetails();
|
||||||
|
|
||||||
|
factory DeezerImageDetails.from(d.DeezerImageDetails details) {
|
||||||
|
return DeezerImageDetails()
|
||||||
|
..type = details.type
|
||||||
|
..md5 = details.md5;
|
||||||
|
}
|
||||||
|
|
||||||
|
d.DeezerImageDetails to() {
|
||||||
|
return d.DeezerImageDetails(md5, type: type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@collection
|
||||||
|
class Lyrics {
|
||||||
|
late final String lyricsId;
|
||||||
|
late final String writers;
|
||||||
|
late final List<Lyric> lyrics;
|
||||||
|
late final bool sync;
|
||||||
|
|
||||||
|
Lyrics();
|
||||||
|
|
||||||
|
factory Lyrics.from(d.Lyrics lyrics) {
|
||||||
|
return Lyrics()
|
||||||
|
..lyricsId = lyrics.id ?? ''
|
||||||
|
..writers = lyrics.writers ?? ''
|
||||||
|
..sync = lyrics.sync
|
||||||
|
..lyrics = lyrics.lyrics!.map(Lyric.from).toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Lyrics to() {
|
||||||
|
return d.Lyrics(
|
||||||
|
id: lyricsId,
|
||||||
|
writers: writers,
|
||||||
|
sync: sync,
|
||||||
|
lyrics: lyrics.map((e) => e.to()).toList(growable: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@embedded
|
||||||
|
class Lyric {
|
||||||
|
late final String text;
|
||||||
|
late final int? offsetMs;
|
||||||
|
late final String? lrcTimestamp;
|
||||||
|
|
||||||
|
Lyric();
|
||||||
|
|
||||||
|
factory Lyric.from(d.Lyric l) {
|
||||||
|
return Lyric()
|
||||||
|
..text = l.text!
|
||||||
|
..offsetMs = l.offset?.inMilliseconds
|
||||||
|
..lrcTimestamp = l.lrcTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Lyric to() {
|
||||||
|
return d.Lyric(
|
||||||
|
offset: offsetMs == null ? null : Duration(milliseconds: offsetMs!),
|
||||||
|
text: text,
|
||||||
|
lrcTimestamp: lrcTimestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,41 @@ import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
import 'package:freezer/api/definitions.dart' as d;
|
||||||
|
import 'package:freezer/api/download_manager/database.dart';
|
||||||
import 'package:freezer/api/download_manager/download_service.dart';
|
import 'package:freezer/api/download_manager/download_service.dart';
|
||||||
import 'package:freezer/api/download_manager/service_interface.dart';
|
import 'package:freezer/api/download_manager/service_interface.dart';
|
||||||
|
import 'package:freezer/api/paths.dart';
|
||||||
|
import 'package:freezer/main.dart';
|
||||||
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
<<<<<<< Updated upstream
|
||||||
|
=======
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
import '../download.dart' as dl;
|
||||||
|
>>>>>>> Stashed changes
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
//implements dl.DownloadManager {
|
//implements dl.DownloadManager {
|
||||||
|
|
||||||
|
late Isar _isar;
|
||||||
|
|
||||||
SendPort? _sendPort;
|
SendPort? _sendPort;
|
||||||
Isolate? _isolate;
|
Isolate? _isolate;
|
||||||
|
|
||||||
Future<bool> configure() {
|
Future<bool> configure() async {
|
||||||
|
_isar = await Isar.open(
|
||||||
|
[
|
||||||
|
// collections
|
||||||
|
TrackSchema,
|
||||||
|
AlbumSchema,
|
||||||
|
ArtistSchema,
|
||||||
|
PlaylistSchema,
|
||||||
|
],
|
||||||
|
directory: await Paths.dataDirectory(),
|
||||||
|
name: 'offline',
|
||||||
|
);
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
return FlutterBackgroundService().configure(
|
return FlutterBackgroundService().configure(
|
||||||
iosConfiguration: IosConfiguration(), // fuck ios
|
iosConfiguration: IosConfiguration(), // fuck ios
|
||||||
|
|
@ -61,10 +85,46 @@ class DownloadManager {
|
||||||
FlutterBackgroundService().invoke(method, args);
|
FlutterBackgroundService().invoke(method, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addOfflineTrack(Track track,
|
@override
|
||||||
{bool private = true, BuildContext? context, isSingleton = false}) {
|
Future<bool> addOfflineTrack(d.Track track,
|
||||||
// TODO: implement addOfflineTrack
|
{bool private = true, BuildContext? context, isSingleton = false}) async {
|
||||||
throw UnimplementedError();
|
//Permission
|
||||||
|
//if (!private && !(await checkPermission())) return false;
|
||||||
|
|
||||||
|
//Ask for quality
|
||||||
|
//AudioQuality? quality;
|
||||||
|
if (!private && settings.downloadQuality == AudioQuality.ASK) {
|
||||||
|
// quality = await qualitySelect(context!);
|
||||||
|
// if (quality == null) return false;
|
||||||
|
}
|
||||||
|
if (private) {
|
||||||
|
if (track.artists == null ||
|
||||||
|
track.artists!.isEmpty ||
|
||||||
|
track.album == null) {
|
||||||
|
track = await deezerAPI.track(track.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache album art
|
||||||
|
cacheManager.getSingleFile(track.albumArt!.thumb);
|
||||||
|
cacheManager.getSingleFile(track.albumArt!.full);
|
||||||
|
|
||||||
|
await _isar.writeTxn(() async {
|
||||||
|
await _isar.tracks.put(Track.from(track));
|
||||||
|
if (track.album != null) {
|
||||||
|
await _isar.albums.put(Album.from(track.album!));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.artists != null) {
|
||||||
|
await _isar.artists
|
||||||
|
.putAll(track.artists!.map(Artist.from).toList(growable: false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// logic for downloading the track
|
||||||
|
invoke('addDownloads', {'track': track.toJson()});
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _startNative(ServiceInstance service) =>
|
static void _startNative(ServiceInstance service) =>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ class DownloadService {
|
||||||
static const NOTIFICATION_ID = 6969;
|
static const NOTIFICATION_ID = 6969;
|
||||||
static const NOTIFICATION_CHANNEL_ID = "freezerdownloads";
|
static const NOTIFICATION_CHANNEL_ID = "freezerdownloads";
|
||||||
|
|
||||||
final ServiceInterface? service;
|
final ServiceInterface service;
|
||||||
DownloadService(this.service);
|
DownloadService(this.service);
|
||||||
|
|
||||||
void run() {}
|
void run() {
|
||||||
|
service.on('addDownloads').listen((event) {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,12 @@ void main() async {
|
||||||
}
|
}
|
||||||
downloadManager.init();
|
downloadManager.init();
|
||||||
// photos
|
// photos
|
||||||
cacheManager = DefaultCacheManager();
|
cacheManager = CacheManager(Config(
|
||||||
|
DefaultCacheManager.key,
|
||||||
|
// cache aggressively
|
||||||
|
stalePeriod: const Duration(days: 30),
|
||||||
|
maxNrOfCacheObjects: 1000,
|
||||||
|
));
|
||||||
// cacheManager = HiveCacheManager(
|
// cacheManager = HiveCacheManager(
|
||||||
// boxName: 'freezer-images', boxPath: await Paths.cacheDir());
|
// boxName: 'freezer-images', boxPath: await Paths.cacheDir());
|
||||||
// TODO: WA
|
// TODO: WA
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue