203 lines
6.1 KiB
Dart
203 lines
6.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/definitions.dart' as d;
|
|
import 'package:freezer/api/download/database.dart';
|
|
import 'package:freezer/api/download/service.dart';
|
|
import 'package:freezer/api/download/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:isar/isar.dart';
|
|
import '../download.dart' as dl;
|
|
import 'package:path/path.dart' as p;
|
|
|
|
class DownloadManager {
|
|
//implements dl.DownloadManager {
|
|
|
|
late Isar _isar;
|
|
|
|
Isolate? _isolate;
|
|
ServiceInterface? _service;
|
|
|
|
bool _started = false;
|
|
|
|
Future<void> startDebug() async {
|
|
if (_started) return;
|
|
_started = true;
|
|
|
|
await configure();
|
|
await startService();
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
}
|
|
|
|
Future<bool> configure() async {
|
|
_isar = await Database.open();
|
|
if (Platform.isAndroid || Platform.isIOS) {
|
|
return FlutterBackgroundService().configure(
|
|
iosConfiguration: IosConfiguration(), // fuck ios
|
|
androidConfiguration: AndroidConfiguration(
|
|
onStart: _startNative,
|
|
isForegroundMode: false,
|
|
autoStart: false,
|
|
autoStartOnBoot: false,
|
|
foregroundServiceNotificationId: DownloadService.NOTIFICATION_ID,
|
|
notificationChannelId: DownloadService.NOTIFICATION_CHANNEL_ID,
|
|
initialNotificationTitle: 'Freezer'.i18n,
|
|
initialNotificationContent: 'Starting download service...'.i18n,
|
|
));
|
|
}
|
|
|
|
// will run in foreground instead, in a separate isolate
|
|
return Future.value(true);
|
|
}
|
|
|
|
Future<bool> startService() async {
|
|
if (Platform.isAndroid) {
|
|
final didStart = await FlutterBackgroundService().startService();
|
|
|
|
if (!didStart) return false;
|
|
} else {
|
|
// UI -> Service communication
|
|
final receivePort = ReceivePort();
|
|
_isolate = await Isolate.spawn(_startIsolate, receivePort.sendPort);
|
|
_service = ServiceInterface(receivePort: receivePort);
|
|
_service!.on('sendPort', (args) {
|
|
_service!.sendPort = args!['s'];
|
|
_service!.no('sendPort');
|
|
});
|
|
}
|
|
|
|
final completer = Completer<void>();
|
|
on('ready', (args) => completer.complete());
|
|
await completer.future;
|
|
invoke('updateSettings', {
|
|
'downloadFilename': settings.downloadFilename,
|
|
'deezerLanguage': settings.deezerLanguage,
|
|
'deezerCountry': settings.deezerCountry,
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
void kill() {
|
|
_isolate?.kill();
|
|
}
|
|
|
|
void invoke(String method, [Map<String, dynamic>? args]) {
|
|
if (_service != null) {
|
|
_service!.send(method, args);
|
|
return;
|
|
}
|
|
|
|
FlutterBackgroundService().invoke(method, args);
|
|
}
|
|
|
|
void on(String method, ListenerCallback listener) {
|
|
if (_service != null) {
|
|
_service!.on(method, listener);
|
|
return;
|
|
}
|
|
|
|
FlutterBackgroundService().on(method).listen(listener);
|
|
}
|
|
|
|
Future<bool> checkOffline(String trackId) async {
|
|
final c =
|
|
await _isar.tracks.where().isarIdEqualTo(int.parse(trackId)).count();
|
|
return c > 0;
|
|
}
|
|
|
|
Future<bool> addOfflineTrack(d.Track track, AudioQuality downloadQuality,
|
|
{bool offline = true, bool isSingleton = false}) async {
|
|
//Permission
|
|
if (!offline && !(await dl.DownloadManager.checkCanDownload())) {
|
|
return false;
|
|
}
|
|
|
|
if (downloadQuality == AudioQuality.ASK) {
|
|
throw Exception('Invalid quality.');
|
|
}
|
|
if (offline) {
|
|
if (track.artists == null ||
|
|
track.artists!.isEmpty ||
|
|
track.album == null) {
|
|
track = await DeezerAPI.instance.track(track.id);
|
|
}
|
|
|
|
// cache album art
|
|
cacheManager.getSingleFile(track.albumArt!.thumb);
|
|
cacheManager.getSingleFile(track.albumArt!.full);
|
|
}
|
|
|
|
// logic for downloading the track
|
|
invoke(
|
|
'addDownloads',
|
|
DownloadInfo(
|
|
trackId: track.id,
|
|
path: offline ? null : settings.downloadPath,
|
|
).toJson());
|
|
|
|
return true;
|
|
}
|
|
|
|
//Generate track download path
|
|
String? _generatePath(Track track, bool offline,
|
|
{String? playlistName,
|
|
int? playlistTrackNumber,
|
|
bool isSingleton = false}) {
|
|
var path = settings.downloadPath!;
|
|
|
|
if (settings.playlistFolder && playlistName != null) {
|
|
path = p.join(path, Database.sanitize(playlistName));
|
|
}
|
|
|
|
if (settings.artistFolder) path = p.join(path, '%albumArtist%');
|
|
|
|
//Album folder / with disk number
|
|
if (settings.albumFolder) {
|
|
if (settings.albumDiscFolder) {
|
|
path = p.join(path, '%album% - Disk ${track.diskNumber ?? 1}');
|
|
} else {
|
|
path = p.join(path, '%album%');
|
|
}
|
|
}
|
|
//Final path
|
|
path = p.join(
|
|
path,
|
|
isSingleton ? settings.singletonFilename : settings.downloadFilename,
|
|
);
|
|
//Playlist track number variable (not accessible in service)
|
|
if (playlistTrackNumber != null) {
|
|
path = path.replaceAll(
|
|
'%playlistTrackNumber%', playlistTrackNumber.toString());
|
|
path = path.replaceAll('%0playlistTrackNumber%',
|
|
playlistTrackNumber.toString().padLeft(2, '0'));
|
|
} else {
|
|
path = path.replaceAll('%playlistTrackNumber%', '');
|
|
path = path.replaceAll('%0playlistTrackNumber%', '');
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
static void _startNative(ServiceInstance service) =>
|
|
_startService(ServiceInterface(service: service));
|
|
|
|
static void _startIsolate(SendPort sendPort) async {
|
|
final receivePort = ReceivePort();
|
|
final service = ServiceInterface(receivePort: receivePort)
|
|
..sendPort = sendPort;
|
|
service.send('sendPort', {'s': receivePort.sendPort});
|
|
return _startService(service);
|
|
}
|
|
|
|
static void _startService(ServiceInterface service) =>
|
|
DownloadService(service).run();
|
|
}
|