// Author: fengshengxiong // Date: 2024/5/10 // Description: 下载管理 import 'package:background_downloader/background_downloader.dart'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/data/api/music_api.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/models/player_model.dart'; import 'package:tone_snap/data/storage/offline_box.dart'; import 'package:tone_snap/modules/sideb/offline/offline_controller.dart'; import 'package:tone_snap/modules/sideb/personal_music_library/personal_music_library_controller.dart'; import 'package:tone_snap/utils/date_util.dart'; import 'package:tone_snap/utils/local_path_util.dart'; import 'package:tone_snap/utils/log_util.dart'; import 'package:tone_snap/utils/obj_util.dart'; class DownloadManager { static final DownloadManager _instance = DownloadManager._getInstance(); factory DownloadManager() => _instance; static MemoryTaskQueue? tq; List> downloadList = []; DownloadManager._getInstance() { if (tq == null) { tq ??= MemoryTaskQueue(); tq!.maxConcurrent = 3; // no more than 5 tasks active at any one time tq!.maxConcurrentByHost = 3; // no more than two tasks talking to the same host at the same time tq!.maxConcurrentByGroup = 3; // no more than three tasks from the same group active at the same time FileDownloader().addTaskQueue(tq!); // 'connects' the TaskQueue to the FileDownloader FileDownloader().updates.listen((update) async { // listen to updates as per usual Rx? musicModel = downloadList.firstWhereOrNull((e) => e.value.videoId == update.task.taskId); if (musicModel == null) return; if (update.runtimeType == TaskStatusUpdate) { TaskStatus taskStatus = (update as TaskStatusUpdate).status; LogUtil.d('${update.task.filename},任务状态: $taskStatus'); musicModel.update((fn) => fn?.taskStatus = taskStatus); switch (taskStatus) { case TaskStatus.enqueued: break; case TaskStatus.running: break; case TaskStatus.complete: LogUtil.d('音乐下载路径:${await update.task.filePath()}'); musicModel.value.localPath = await update.task.filePath(); OfflineBox().add(musicModel.value.copyWith()); downloadList.remove(musicModel); BaseEasyLoading.toast('Download completed'); if (Get.isRegistered()) { PersonalMusicLibraryController.to.refreshOffline(); } if (Get.isRegistered()) { OfflineController.to.getOfflineList(); } break; case TaskStatus.notFound: BaseEasyLoading.toast('Download failed'); downloadList.remove(musicModel); break; case TaskStatus.failed: BaseEasyLoading.toast('Download failed'); downloadList.remove(musicModel); break; case TaskStatus.canceled: BaseEasyLoading.toast('Download cancelled'); downloadList.remove(musicModel); break; case TaskStatus.waitingToRetry: break; case TaskStatus.paused: break; } } if (update.runtimeType == TaskProgressUpdate) { LogUtil.d('${update.task.filename},下载进度: $update'); musicModel.update((fn) => fn?.progress = (update as TaskProgressUpdate).progress); } }); } } /// 下载文件 void downloadMusic(Rx? musicModel) { if (musicModel == null) return; musicModel.update((fn) { fn?.taskStatus = TaskStatus.enqueued; fn?.cancelToken = CancelToken(); }); _getMusicUrl(musicModel, (url, mimeType) async { if (ObjUtil.isEmpty(url)) { BaseEasyLoading.toast('Resource acquisition failed'); musicModel.update((fn) => fn?.taskStatus = TaskStatus.failed); return; } String extension = mimeType ?? 'mp4'; if (ObjUtil.isNotEmpty(mimeType)) { // 从 mimeType 中提取主类型和子类型 String type = mimeType!.split(';')[0].trim(); // 获取文件扩展名 extension = type.split('/')[1]; } final filename = '${musicModel.value.title}_${DateUtil.getNowTimestamp()}.$extension'; final task = DownloadTask( taskId: musicModel.value.videoId, url: url!, filename: filename, directory: LocalPathUtil.getMusicDownloadDir(), updates: Updates.statusAndProgress, requiresWiFi: false, retries: 0, allowPause: false, metaData: '', ); tq?.add(task); downloadList.add(musicModel); }); } Future _getMusicUrl(Rx musicModel, Function(String? url, String? mimeType) onTap) async { PlayerModel? playerModel = await MusicApi.player( videoId: musicModel.value.videoId, cancelToken: musicModel.value.cancelToken, fail: (baseError) { if (baseError.code == DioExceptionType.cancel.index) { musicModel.update((fn) { fn?.taskStatus = TaskStatus.canceled; fn?.cancelToken = null; }); } else { musicModel.update((fn) { fn?.taskStatus = TaskStatus.failed; fn?.cancelToken = null; }); } } ); if (playerModel != null) { if (ObjUtil.isEmpty(musicModel.value.coverUrl)) { var thumbnails = playerModel.videoDetails?.thumbnail?.thumbnails; if (thumbnails != null && thumbnails.isNotEmpty) { musicModel.value.coverUrl = thumbnails[0].url; } } if (ObjUtil.isEmpty(musicModel.value.musicType)) { musicModel.value.musicType = playerModel.videoDetails?.musicVideoType; } var formats = playerModel.streamingData?.formats; if (formats != null && formats.isNotEmpty) { onTap(formats[0].url, formats[0].mimeType); } } } void cancelDownload(Rx? musicModel) { if (musicModel == null || musicModel.value.videoId == null) return; musicModel.value.cancelToken?.cancel(); FileDownloader().cancelTaskWithId(musicModel.value.videoId!); } }