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