// download manager to handle download tasks of files import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; final _logger = Logger('AudiobookDownloadManager'); final tq = MemoryTaskQueue(); const downloadDirectory = BaseDirectory.applicationSupport; class AudiobookDownloadManager { // takes in the baseUrl and the token AudiobookDownloadManager({ required this.baseUrl, required this.token, this.requiresWiFi = true, this.retries = 0, this.allowPause = false, // /// The maximum number of concurrent tasks to run at any given time. // int maxConcurrent = 3, // /// The maximum number of concurrent tasks to run for the same host. // int maxConcurrentByHost = 2, // /// The maximum number of concurrent tasks to run for the same group. // int maxConcurrentByGroup = 3, }) { // initialize the download manager FileDownloader().addTaskQueue(tq); _logger.fine('initialized with baseUrl: $baseUrl, token: $token'); _logger.fine( 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', ); initDownloadManager(); } // the base url for the audio files final String baseUrl; // the authentication token to access the [AudioTrack.contentUrl] final String token; // whether to download only on wifi final bool requiresWiFi; // the number of retries to attempt final int retries; // whether to allow pausing of downloads final bool allowPause; final StreamController _taskStatusController = StreamController.broadcast(); Stream get taskUpdateStream => _taskStatusController.stream; late StreamSubscription _updatesSubscription; Future queueAudioBookDownload( LibraryItemExpanded item, { void Function(TaskStatusUpdate)? taskStatusCallback, void Function(TaskProgressUpdate)? taskProgressCallback, void Function(Task, NotificationType)? taskNotificationTapCallback, }) async { _logger.info('queuing download for item: ${item.id}'); // create a download task for each file in the item final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { // check if the file is already downloaded if (isFileDownloaded( constructFilePath(directory, item, file), )) { _logger.info('file already downloaded: ${file.metadata.filename}'); continue; } final task = DownloadTask( taskId: file.ino, url: file.url(baseUrl, item.id, token).toString(), directory: item.relPath, filename: file.metadata.filename, requiresWiFi: requiresWiFi, retries: retries, allowPause: allowPause, group: item.id, baseDirectory: downloadDirectory, // metaData: token ); // _downloadTasks.add(task); tq.add(task); _logger.info('queued task: ${task.taskId}'); } FileDownloader().registerCallbacks( group: item.id, taskProgressCallback: (update) { try { taskProgressCallback?.call(update); } catch (e) { _logger.warning('Error in taskProgressCallback: $e'); } _logger.info('Group: ${item.id}, Progress Update: ${update.progress}'); }, taskStatusCallback: (update) { try { taskStatusCallback?.call(update); } catch (e) { _logger.warning('Error in taskStatusCallback: $e'); } switch (update.status) { case TaskStatus.complete: _logger.info('Group: ${item.id}, Download Complete'); break; case TaskStatus.failed: _logger.warning('Group: ${item.id}, Download Failed'); break; default: _logger .info('Group: ${item.id}, Download Status: ${update.status}'); } }, taskNotificationTapCallback: (task, notificationType) { try { taskNotificationTapCallback?.call(task, notificationType); } catch (e) { _logger.warning('Error in taskNotificationTapCallback: $e'); } _logger.info('Group: ${item.id}, Task: ${task.taskId}'); }, ); } String constructFilePath( Directory directory, LibraryItemExpanded item, LibraryFile file, ) => '${directory.path}/${item.relPath}/${file.metadata.filename}'; void dispose() { _updatesSubscription.cancel(); FileDownloader().destroy(); _logger.fine('Destroyed download manager'); } bool isItemDownloading(String id) { if (tq.isEmpty) { return false; } return tq.enqueued.any((task) => task.group == id); } bool isFileDownloaded(String filePath) { final fileExists = File(filePath).existsSync(); return fileExists; } Future isItemDownloaded(LibraryItemExpanded item) async { final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { if (!isFileDownloaded(constructFilePath(directory, item, file))) { _logger.info('file not downloaded: ${file.metadata.filename}'); return false; } } _logger.info('all files downloaded for item id: ${item.id}'); return true; } Future deleteDownloadedItem(LibraryItemExpanded item) async { _logger.info('deleting downloaded item id: ${item.id}'); final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { final filePath = constructFilePath(directory, item, file); if (isFileDownloaded(filePath)) { File(filePath).deleteSync(); } _logger.info('deleted file: $filePath'); } } Future> getDownloadedFiles(LibraryItemExpanded item) async { final directory = await getApplicationSupportDirectory(); final files = []; for (final file in item.libraryFiles) { final filePath = constructFilePath(directory, item, file); if (isFileDownloaded(filePath)) { files.add(Uri.file(filePath)); } } return files; } Future initDownloadManager() async { // initialize the download manager _logger.info('Initializing download manager'); final fileDownloader = FileDownloader(); _logger.info('Configuring Notification'); fileDownloader.configureNotification( running: const TaskNotification('Downloading', 'file: {filename}'), progressBar: true, ); await fileDownloader.trackTasks(); _logger.info('Listening to download manager updates'); try { _updatesSubscription = fileDownloader.updates.listen((event) { _logger.info('Got event: $event'); _taskStatusController.add(event); }); } catch (e) { _logger.warning('Error when listening to download manager updates: $e'); } } }