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 startDebug() async { if (_started) return; _started = true; await configure(); await startService(); await Future.delayed(const Duration(milliseconds: 500)); } Future 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 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(); 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? 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 checkOffline(String trackId) async { final c = await _isar.tracks.where().isarIdEqualTo(int.parse(trackId)).count(); return c > 0; } Future 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(); }