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