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