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