// 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/ads/interstitial_ad_manager.dart'; import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/data/api/music_api.dart'; import 'package:tone_snap/data/enum/ad_scenes.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/firebase/firebase_analytics_manager.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 extends GetxController { static DownloadManager get to => Get.put(DownloadManager(), permanent: true); final downloadStateId = 'download_state_id'; MemoryTaskQueue? tq; List downloadList = []; @override void onInit() { super.onInit(); 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 MusicModel? musicModel = downloadList.firstWhereOrNull((e) => e.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.taskStatus = taskStatus; switch (taskStatus) { case TaskStatus.enqueued: break; case TaskStatus.running: break; case TaskStatus.complete: LogUtil.d('音乐下载路径:${await update.task.filePath()}'); FirebaseAnalyticsManager.logPlayerBDownloadsuccessAction( musicModel.videoId, musicModel.title, musicModel.subtitle, ); musicModel.localPath = await update.task.filePath(); OfflineBox().add(musicModel); 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); FirebaseAnalyticsManager.logPlayerBDownloadfailAction('404,找不到url'); break; case TaskStatus.failed: BaseEasyLoading.toast('Download failed'); downloadList.remove(musicModel); FirebaseAnalyticsManager.logPlayerBDownloadfailAction('下载失败'); break; case TaskStatus.canceled: BaseEasyLoading.toast('Download cancelled'); downloadList.remove(musicModel); FirebaseAnalyticsManager.logPlayerBDownloadfailAction('取消下载'); break; case TaskStatus.waitingToRetry: break; case TaskStatus.paused: break; } } if (update.runtimeType == TaskProgressUpdate) { LogUtil.d('${update.task.filename},下载进度: $update'); musicModel.progress = (update as TaskProgressUpdate).progress; } updateDownloadState(); }); } } /// 下载文件 void downloadMusic(MusicModel? m) { FirebaseAnalyticsManager.logPlayerBDownloadClick( m?.videoId, m?.title, m?.subtitle, ); InterstitialAdManager().showAdIfAvailable( AdScenes.download.name, onTap: () { if (m == null) return; MusicModel musicModel = m.copyWith(); musicModel.taskStatus = TaskStatus.enqueued; musicModel.cancelToken = CancelToken(); downloadList.add(musicModel); updateDownloadState(); _getMusicUrl(musicModel, (url, mimeType) async { if (ObjUtil.isNotEmpty(url)) { String extension = mimeType ?? 'mp4'; if (ObjUtil.isNotEmpty(mimeType)) { // 从 mimeType 中提取主类型和子类型 String type = mimeType!.split(';')[0].trim(); // 获取文件扩展名 extension = type.split('/')[1]; } final filename = '${musicModel.title}_${DateUtil.getNowTimestamp()}.$extension'; final task = DownloadTask( taskId: musicModel.videoId, url: url!, filename: filename, directory: LocalPathUtil.getMusicDownloadDir(), updates: Updates.statusAndProgress, requiresWiFi: false, retries: 0, allowPause: false, metaData: '', ); tq?.add(task); } }); }, ); } Future _getMusicUrl(MusicModel musicModel, Function(String? url, String? mimeType) onTap) async { PlayerModel? playerModel = await MusicApi.player( videoId: musicModel.videoId, cancelToken: musicModel.cancelToken, fail: (baseError) { if (baseError.code == DioExceptionType.cancel.index) { musicModel.taskStatus = TaskStatus.canceled; musicModel.cancelToken = null; } else { musicModel.taskStatus = TaskStatus.failed; musicModel.cancelToken = null; } FirebaseAnalyticsManager.logPlayerBDownloadfailAction('${baseError.code},${baseError.message}'); downloadList.remove(musicModel); updateDownloadState(); } ); if (playerModel != null) { if (ObjUtil.isEmpty(musicModel.coverUrl)) { var thumbnails = playerModel.videoDetails?.thumbnail?.thumbnails; if (thumbnails != null && thumbnails.isNotEmpty) { musicModel.coverUrl = thumbnails.last.url; } } if (ObjUtil.isEmpty(musicModel.musicType)) { musicModel.musicType = playerModel.videoDetails?.musicVideoType; } var formats = playerModel.streamingData?.formats; if (formats != null && formats.isNotEmpty) { onTap(formats[0].url, formats[0].mimeType); } } } void cancelDownload(String? videoId) { if (ObjUtil.isEmpty(videoId)) return; final m = getMusicModel(videoId); if (m != null) { m.cancelToken?.cancel(); if (m.videoId != null) { FileDownloader().cancelTaskWithId(m.videoId!); } } } void updateDownloadState() { DownloadManager.to.update([downloadStateId]); } MusicModel? getMusicModel(String? videoId) { if (ObjUtil.isEmpty(videoId)) return null; final m = downloadList.firstWhereOrNull((e) => e.videoId == videoId); return m; } }