put common deezer audio logic in DeezerAudio class

This commit is contained in:
Pato05 2023-10-28 14:42:06 +02:00
parent 2862c9ec05
commit 77d6a5a51d
No known key found for this signature in database
GPG key ID: F53CA394104BA0CB
6 changed files with 671 additions and 286 deletions

318
lib/api/deezer_audio.dart Normal file
View 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);
}
}

View file

@ -1,17 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/deezer_audio.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:just_audio/just_audio.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:dart_blowfish/dart_blowfish.dart';
import 'package:logging/logging.dart';
typedef _IsolateMessage = (
@ -27,7 +23,6 @@ class DeezerAudioSource extends StreamAudioSource {
static final _logger = Logger("DeezerAudioSource");
late AudioQuality Function() _getQuality;
late AudioQuality? _initialQuality;
late String _trackId;
late String _md5origin;
late String _mediaVersion;
@ -36,7 +31,7 @@ class DeezerAudioSource extends StreamAudioSource {
final StreamInfoCallback? onStreamObtained;
// some cache
AudioQuality? _currentQuality;
final DeezerAudio _deezerAudio;
int? _cachedSourceLength;
String? _cachedContentType;
Uri? _downloadUrl;
@ -49,255 +44,24 @@ class DeezerAudioSource extends StreamAudioSource {
required this.trackToken,
required this.trackTokenExpiration,
this.onStreamObtained,
}) {
}) : _deezerAudio = DeezerAudio(
deezerAPI: deezerAPI,
md5origin: md5origin,
quality: getQuality.call(),
trackId: trackId,
mediaVersion: mediaVersion,
) {
_getQuality = getQuality;
_initialQuality = quality;
_trackId = trackId;
_md5origin = md5origin;
_mediaVersion = mediaVersion;
}
AudioQuality? get quality => _currentQuality;
AudioQuality? get quality => _deezerAudio.quality;
String get trackId => _trackId;
String get md5origin => _md5origin;
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
Future<StreamAudioResponse> request([int? start, int? end]) async {
start ??= 0;
@ -322,15 +86,16 @@ class DeezerAudioSource extends StreamAudioSource {
// determine quality to use
final newQuality = _getQuality.call();
if (_downloadUrl != null && _currentQuality != newQuality) {
if (_downloadUrl != null && quality != newQuality) {
// update currentUrl to get tracks with new quality
_downloadUrl = null;
}
_currentQuality = newQuality;
_deezerAudio.quality = newQuality;
if (_downloadUrl == null) {
final gottenUrl = await _getUrl();
final gottenUrl =
await _deezerAudio.getUrl(trackToken, trackTokenExpiration);
if (gottenUrl != null) {
_downloadUrl = gottenUrl;
} else {
@ -338,7 +103,7 @@ class DeezerAudioSource extends StreamAudioSource {
// OLD URL GENERATION
try {
_downloadUrl = await _fallbackUrl();
_downloadUrl = await _deezerAudio.fallback();
} on QualityException {
rethrow;
}
@ -370,10 +135,11 @@ class DeezerAudioSource extends StreamAudioSource {
throw Exception(await res.stream.bytesToString());
}
int dropBytes = start % chunkSize;
int dropBytes = start % DeezerAudio.chunkSize;
_logger.finest(
"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;

View file

@ -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';
part 'database.g.dart';
@collection
class Track {
Id id = Isar.autoIncrement;
final String trackId;
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;
Id get isarId => int.parse(id);
late final String id;
Track({
required this.trackId,
required this.title,
required this.albumId,
required this.artistIds,
//required this.albumArt,
required this.trackNumber,
//required this.lyrics,
required this.favorite,
required this.diskNumber,
required this.explicit,
this.offline = true,
});
// index title for search
@Index(type: IndexType.value)
late final String title;
late final String albumId;
late final List<String> artistIds;
late final DeezerImageDetails albumArt;
late final int? trackNumber;
late final bool offline;
late final Lyrics? lyrics;
late final bool favorite;
late final int? diskNumber;
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);
}
}

View file

@ -3,17 +3,41 @@ import 'dart:isolate';
import 'package:flutter/src/widgets/framework.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/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';
<<<<<<< Updated upstream
=======
import 'package:isar/isar.dart';
import '../download.dart' as dl;
>>>>>>> Stashed changes
class DownloadManager {
//implements dl.DownloadManager {
late Isar _isar;
SendPort? _sendPort;
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) {
return FlutterBackgroundService().configure(
iosConfiguration: IosConfiguration(), // fuck ios
@ -61,10 +85,46 @@ class DownloadManager {
FlutterBackgroundService().invoke(method, args);
}
Future<bool> addOfflineTrack(Track track,
{bool private = true, BuildContext? context, isSingleton = false}) {
// TODO: implement addOfflineTrack
throw UnimplementedError();
@override
Future<bool> addOfflineTrack(d.Track track,
{bool private = true, BuildContext? context, isSingleton = false}) async {
//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) =>

View file

@ -4,8 +4,10 @@ class DownloadService {
static const NOTIFICATION_ID = 6969;
static const NOTIFICATION_CHANNEL_ID = "freezerdownloads";
final ServiceInterface? service;
final ServiceInterface service;
DownloadService(this.service);
void run() {}
void run() {
service.on('addDownloads').listen((event) {});
}
}

View file

@ -109,7 +109,12 @@ void main() async {
}
downloadManager.init();
// photos
cacheManager = DefaultCacheManager();
cacheManager = CacheManager(Config(
DefaultCacheManager.key,
// cache aggressively
stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 1000,
));
// cacheManager = HiveCacheManager(
// boxName: 'freezer-images', boxPath: await Paths.cacheDir());
// TODO: WA