1import { 2 internalSerializeKey, 3 supportLocalMediaType, 4} from '@/constants/commonConst'; 5import pathConst from '@/constants/pathConst'; 6import {addFileScheme, escapeCharacter, mkdirR} from '@/utils/fileUtils'; 7import {errorLog} from '@/utils/log'; 8import {isSameMediaItem} from '@/utils/mediaItem'; 9import {getQualityOrder} from '@/utils/qualities'; 10import StateMapper from '@/utils/stateMapper'; 11import Toast from '@/utils/toast'; 12import produce from 'immer'; 13import {InteractionManager} from 'react-native'; 14import {downloadFile, exists} from 'react-native-fs'; 15 16import Config from './config'; 17import LocalMusicSheet from './localMusicSheet'; 18import MediaMeta from './mediaExtra'; 19import Network from './network'; 20import PluginManager from './pluginManager'; 21import {PERMISSIONS, check} from 'react-native-permissions'; 22import path from 'path-browserify'; 23import { 24 getCurrentDialog, 25 hideDialog, 26 showDialog, 27} from '@/components/dialogs/useDialog'; 28// import PQueue from 'p-queue/dist'; 29// import PriorityQueue from 'p-queue/dist/priority-queue'; 30 31// interface IDownloadProgress { 32// progress: number; 33// size: number; 34// } 35 36// const downloadQueue = new PQueue({ 37// concurrency: 3 38// }); 39 40// const downloadProgress = new Map<string, IDownloadProgress>(); 41// downloadQueue.concurrency = 3; 42// console.log(downloadQueue.concurrency); 43 44/** 队列中的元素 */ 45interface IDownloadMusicOptions { 46 /** 要下载的音乐 */ 47 musicItem: IMusic.IMusicItem; 48 /** 目标文件名 */ 49 filename: string; 50 /** 下载id */ 51 jobId?: number; 52 /** 下载音质 */ 53 quality?: IMusic.IQualityKey; 54} 55 56/** 下载中 */ 57let downloadingMusicQueue: IDownloadMusicOptions[] = []; 58/** 队列中 */ 59let pendingMusicQueue: IDownloadMusicOptions[] = []; 60/** 下载进度 */ 61let downloadingProgress: Record<string, {progress: number; size: number}> = {}; 62/** 错误信息 */ 63let hasError: boolean = false; 64 65const downloadingQueueStateMapper = new StateMapper( 66 () => downloadingMusicQueue, 67); 68const pendingMusicQueueStateMapper = new StateMapper(() => pendingMusicQueue); 69const downloadingProgressStateMapper = new StateMapper( 70 () => downloadingProgress, 71); 72 73/** 匹配文件后缀 */ 74const getExtensionName = (url: string) => { 75 const regResult = url.match( 76 /^https?\:\/\/.+\.([^\?\.]+?$)|(?:([^\.]+?)\?.+$)/, 77 ); 78 if (regResult) { 79 return regResult[1] ?? regResult[2] ?? 'mp3'; 80 } else { 81 return 'mp3'; 82 } 83}; 84 85/** 生成下载文件 */ 86const getDownloadPath = (fileName?: string) => { 87 const dlPath = 88 Config.get('setting.basic.downloadPath') ?? pathConst.downloadMusicPath; 89 if (!dlPath.endsWith('/')) { 90 return `${dlPath}/${fileName ?? ''}`; 91 } 92 return fileName ? dlPath + fileName : dlPath; 93}; 94 95/** 从待下载中移除 */ 96function removeFromPendingQueue(item: IDownloadMusicOptions) { 97 const targetIndex = pendingMusicQueue.findIndex(_ => 98 isSameMediaItem(_.musicItem, item.musicItem), 99 ); 100 if (targetIndex !== -1) { 101 pendingMusicQueue = pendingMusicQueue 102 .slice(0, targetIndex) 103 .concat(pendingMusicQueue.slice(targetIndex + 1)); 104 pendingMusicQueueStateMapper.notify(); 105 } 106} 107 108/** 从下载中队列移除 */ 109function removeFromDownloadingQueue(item: IDownloadMusicOptions) { 110 const targetIndex = downloadingMusicQueue.findIndex(_ => 111 isSameMediaItem(_.musicItem, item.musicItem), 112 ); 113 if (targetIndex !== -1) { 114 downloadingMusicQueue = downloadingMusicQueue 115 .slice(0, targetIndex) 116 .concat(downloadingMusicQueue.slice(targetIndex + 1)); 117 downloadingQueueStateMapper.notify(); 118 } 119} 120 121/** 防止高频同步 */ 122let progressNotifyTimer: any = null; 123function startNotifyProgress() { 124 if (progressNotifyTimer) { 125 return; 126 } 127 128 progressNotifyTimer = setTimeout(() => { 129 progressNotifyTimer = null; 130 downloadingProgressStateMapper.notify(); 131 startNotifyProgress(); 132 }, 500); 133} 134 135function stopNotifyProgress() { 136 if (progressNotifyTimer) { 137 clearTimeout(progressNotifyTimer); 138 } 139 progressNotifyTimer = null; 140} 141 142/** 生成下载文件名 */ 143function generateFilename(musicItem: IMusic.IMusicItem) { 144 return `${escapeCharacter(musicItem.platform)}@${escapeCharacter( 145 musicItem.id, 146 )}@${escapeCharacter(musicItem.title)}@${escapeCharacter( 147 musicItem.artist, 148 )}`.slice(0, 200); 149} 150 151/** todo 可以配置一个说明文件 */ 152// async function loadLocalJson(dirBase: string) { 153// const jsonPath = dirBase + 'data.json'; 154// if (await exists(jsonPath)) { 155// try { 156// const result = await readFile(jsonPath, 'utf8'); 157// return JSON.parse(result); 158// } catch { 159// return {}; 160// } 161// } 162// return {}; 163// } 164 165let maxDownload = 3; 166/** 队列下载*/ 167async function downloadNext() { 168 // todo 最大同时下载3个,可设置 169 if ( 170 downloadingMusicQueue.length >= maxDownload || 171 pendingMusicQueue.length === 0 172 ) { 173 return; 174 } 175 // 下一个下载的为pending的第一个 176 let nextDownloadItem = pendingMusicQueue[0]; 177 const musicItem = nextDownloadItem.musicItem; 178 let url = musicItem.url; 179 let headers = musicItem.headers; 180 removeFromPendingQueue(nextDownloadItem); 181 downloadingMusicQueue = produce(downloadingMusicQueue, draft => { 182 draft.push(nextDownloadItem); 183 }); 184 downloadingQueueStateMapper.notify(); 185 const quality = nextDownloadItem.quality; 186 const plugin = PluginManager.getByName(musicItem.platform); 187 // 插件播放 188 try { 189 if (plugin) { 190 const qualityOrder = getQualityOrder( 191 quality ?? 192 Config.get('setting.basic.defaultDownloadQuality') ?? 193 'standard', 194 Config.get('setting.basic.downloadQualityOrder') ?? 'asc', 195 ); 196 let data: IPlugin.IMediaSourceResult | null = null; 197 for (let quality of qualityOrder) { 198 try { 199 data = await plugin.methods.getMediaSource( 200 musicItem, 201 quality, 202 1, 203 true, 204 ); 205 if (!data?.url) { 206 continue; 207 } 208 break; 209 } catch {} 210 } 211 url = data?.url ?? url; 212 headers = data?.headers; 213 } 214 if (!url) { 215 throw new Error('empty'); 216 } 217 } catch (e: any) { 218 /** 无法下载,跳过 */ 219 errorLog('下载失败-无法获取下载链接', { 220 item: { 221 id: nextDownloadItem.musicItem.id, 222 title: nextDownloadItem.musicItem.title, 223 platform: nextDownloadItem.musicItem.platform, 224 quality: nextDownloadItem.quality, 225 }, 226 reason: e?.message ?? e, 227 }); 228 hasError = true; 229 removeFromDownloadingQueue(nextDownloadItem); 230 return; 231 } 232 /** 预处理完成,接下来去下载音乐 */ 233 downloadNextAfterInteraction(); 234 let extension = getExtensionName(url); 235 const extensionWithDot = `.${extension}`; 236 if (supportLocalMediaType.every(_ => _ !== extensionWithDot)) { 237 extension = 'mp3'; 238 } 239 /** 目标下载地址 */ 240 const targetDownloadPath = addFileScheme( 241 getDownloadPath(`${nextDownloadItem.filename}.${extension}`), 242 ); 243 const {promise, jobId} = downloadFile({ 244 fromUrl: url ?? '', 245 toFile: targetDownloadPath, 246 headers: headers, 247 background: true, 248 begin(res) { 249 downloadingProgress = produce(downloadingProgress, _ => { 250 _[nextDownloadItem.filename] = { 251 progress: 0, 252 size: res.contentLength, 253 }; 254 }); 255 startNotifyProgress(); 256 }, 257 progress(res) { 258 downloadingProgress = produce(downloadingProgress, _ => { 259 _[nextDownloadItem.filename] = { 260 progress: res.bytesWritten, 261 size: res.contentLength, 262 }; 263 }); 264 }, 265 }); 266 nextDownloadItem = {...nextDownloadItem, jobId}; 267 268 let folder = path.dirname(targetDownloadPath); 269 let folderExists = await exists(folder); 270 271 if (!folderExists) { 272 await mkdirR(folder); 273 } 274 try { 275 await promise; 276 /** 下载完成 */ 277 LocalMusicSheet.addMusicDraft({ 278 ...musicItem, 279 [internalSerializeKey]: { 280 localPath: targetDownloadPath, 281 }, 282 }); 283 MediaMeta.update(musicItem, { 284 downloaded: true, 285 localPath: targetDownloadPath, 286 }); 287 // const primaryKey = plugin?.instance.primaryKey ?? []; 288 // if (!primaryKey.includes('id')) { 289 // primaryKey.push('id'); 290 // } 291 // const stringifyMeta: Record<string, any> = { 292 // title: musicItem.title, 293 // artist: musicItem.artist, 294 // album: musicItem.album, 295 // lrc: musicItem.lrc, 296 // platform: musicItem.platform, 297 // }; 298 // primaryKey.forEach(_ => { 299 // stringifyMeta[_] = musicItem[_]; 300 // }); 301 302 // await Mp3Util.getMediaTag(filePath).then(_ => { 303 // console.log(_); 304 // }).catch(console.log); 305 } catch (e: any) { 306 console.log(e, 'downloaderror'); 307 /** 下载出错 */ 308 errorLog('下载失败', { 309 item: { 310 id: nextDownloadItem.musicItem.id, 311 title: nextDownloadItem.musicItem.title, 312 platform: nextDownloadItem.musicItem.platform, 313 quality: nextDownloadItem.quality, 314 }, 315 reason: e?.message ?? e, 316 targetDownloadPath: targetDownloadPath, 317 }); 318 hasError = true; 319 } 320 removeFromDownloadingQueue(nextDownloadItem); 321 downloadingProgress = produce(downloadingProgress, draft => { 322 if (draft[nextDownloadItem.filename]) { 323 delete draft[nextDownloadItem.filename]; 324 } 325 }); 326 downloadNextAfterInteraction(); 327 if (downloadingMusicQueue.length === 0) { 328 stopNotifyProgress(); 329 LocalMusicSheet.saveLocalSheet(); 330 if (hasError) { 331 try { 332 const perm = await check( 333 PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE, 334 ); 335 if (perm !== 'granted') { 336 Toast.warn('权限不足,请检查是否授予写入文件的权限'); 337 } else { 338 throw new Error(); 339 } 340 } catch { 341 if (getCurrentDialog()?.name !== 'SimpleDialog') { 342 showDialog('SimpleDialog', { 343 title: '下载失败', 344 content: 345 '部分歌曲下载失败,如果无法下载请检查系统设置中是否授予完整文件读写权限;或者去【侧边栏-权限管理】中查看文件读写权限是否勾选', 346 onOk: hideDialog, 347 }); 348 } 349 } 350 } else { 351 Toast.success('下载完成'); 352 } 353 hasError = false; 354 downloadingMusicQueue = []; 355 pendingMusicQueue = []; 356 downloadingQueueStateMapper.notify(); 357 pendingMusicQueueStateMapper.notify(); 358 } 359} 360 361async function downloadNextAfterInteraction() { 362 InteractionManager.runAfterInteractions(downloadNext); 363} 364 365/** 加入下载队列 */ 366function downloadMusic( 367 musicItems: IMusic.IMusicItem | IMusic.IMusicItem[], 368 quality?: IMusic.IQualityKey, 369) { 370 if (Network.isOffline()) { 371 Toast.warn('当前无网络,无法下载'); 372 return; 373 } 374 if ( 375 Network.isCellular() && 376 !Config.get('setting.basic.useCelluarNetworkDownload') && 377 getCurrentDialog()?.name !== 'SimpleDialog' 378 ) { 379 showDialog('SimpleDialog', { 380 title: '流量提醒', 381 content: 382 '当前非WIFI环境,侧边栏设置中打开【使用移动网络下载】功能后可继续下载', 383 }); 384 return; 385 } 386 // 如果已经在下载中 387 if (!Array.isArray(musicItems)) { 388 musicItems = [musicItems]; 389 } 390 hasError = false; 391 musicItems = musicItems.filter( 392 musicItem => 393 pendingMusicQueue.findIndex(_ => 394 isSameMediaItem(_.musicItem, musicItem), 395 ) === -1 && 396 downloadingMusicQueue.findIndex(_ => 397 isSameMediaItem(_.musicItem, musicItem), 398 ) === -1 && 399 !LocalMusicSheet.isLocalMusic(musicItem), 400 ); 401 const enqueueData = musicItems.map(_ => { 402 return { 403 musicItem: _, 404 filename: generateFilename(_), 405 quality, 406 }; 407 }); 408 if (enqueueData.length) { 409 pendingMusicQueue = pendingMusicQueue.concat(enqueueData); 410 pendingMusicQueueStateMapper.notify(); 411 maxDownload = +(Config.get('setting.basic.maxDownload') ?? 3); 412 downloadNextAfterInteraction(); 413 } 414} 415 416const Download = { 417 downloadMusic, 418 useDownloadingMusic: downloadingQueueStateMapper.useMappedState, 419 usePendingMusic: pendingMusicQueueStateMapper.useMappedState, 420 useDownloadingProgress: downloadingProgressStateMapper.useMappedState, 421}; 422 423export default Download; 424