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