1import {internalSerialzeKey, internalSymbolKey} from '@/constants/commonConst'; 2import pathConst from '@/constants/pathConst'; 3import {checkAndCreateDir} from '@/utils/fileUtils'; 4import {errorLog} from '@/utils/log'; 5import {isSameMediaItem} from '@/utils/mediaItem'; 6import StateMapper from '@/utils/stateMapper'; 7import {setStorage} from '@/utils/storage'; 8import Toast from '@/utils/toast'; 9import produce from 'immer'; 10import {useEffect, useState} from 'react'; 11import {unlink, downloadFile, readDir} from 'react-native-fs'; 12 13import Config from './config'; 14import MediaMeta from './mediaMeta'; 15import Network from './network'; 16import PluginManager from './pluginManager'; 17 18interface IDownloadMusicOptions { 19 musicItem: IMusic.IMusicItem; 20 filename: string; 21 jobId?: number; 22} 23// todo: 直接把下载信息写在meta里面就好了 24/** 已下载 */ 25let downloadedMusic: IMusic.IMusicItem[] = []; 26/** 下载中 */ 27let downloadingMusicQueue: IDownloadMusicOptions[] = []; 28/** 队列中 */ 29let pendingMusicQueue: IDownloadMusicOptions[] = []; 30 31/** 进度 */ 32let downloadingProgress: Record<string, {progress: number; size: number}> = {}; 33 34const downloadedStateMapper = new StateMapper(() => downloadedMusic); 35const downloadingQueueStateMapper = new StateMapper( 36 () => downloadingMusicQueue, 37); 38const pendingMusicQueueStateMapper = new StateMapper(() => pendingMusicQueue); 39const downloadingProgressStateMapper = new StateMapper( 40 () => downloadingProgress, 41); 42 43/** 从待下载中移除 */ 44function removeFromPendingQueue(item: IDownloadMusicOptions) { 45 pendingMusicQueue = pendingMusicQueue.filter( 46 _ => !isSameMediaItem(_.musicItem, item.musicItem), 47 ); 48 pendingMusicQueueStateMapper.notify(); 49} 50 51/** 从下载中队列移除 */ 52function removeFromDownloadingQueue(item: IDownloadMusicOptions) { 53 downloadingMusicQueue = downloadingMusicQueue.filter( 54 _ => !isSameMediaItem(_.musicItem, item.musicItem), 55 ); 56 downloadingQueueStateMapper.notify(); 57} 58 59/** 防止高频同步 */ 60let progressNotifyTimer: any = null; 61function startNotifyProgress() { 62 if (progressNotifyTimer) { 63 return; 64 } 65 66 progressNotifyTimer = setTimeout(() => { 67 progressNotifyTimer = null; 68 downloadingProgressStateMapper.notify(); 69 startNotifyProgress(); 70 }, 400); 71} 72 73function stopNotifyProgress() { 74 if (progressNotifyTimer) { 75 clearInterval(progressNotifyTimer); 76 } 77 progressNotifyTimer = null; 78} 79 80/** 根据文件名解析 */ 81function parseFilename(fn: string): IMusic.IMusicItemBase | null { 82 const data = fn.slice(0, fn.lastIndexOf('.')).split('@'); 83 const [platform, id, title, artist] = data; 84 if (!platform || !id) { 85 return null; 86 } 87 return { 88 id, 89 platform, 90 title, 91 artist, 92 }; 93} 94 95/** 生成下载文件名 */ 96function generateFilename(musicItem: IMusic.IMusicItem) { 97 return ( 98 `${musicItem.platform}@${musicItem.id}@${musicItem.title}@${musicItem.artist}`.slice( 99 0, 100 200, 101 ) + '.mp3' 102 ); 103} 104 105/** todo 可以配置一个说明文件 */ 106// async function loadLocalJson(dirBase: string) { 107// const jsonPath = dirBase + 'data.json'; 108// if (await exists(jsonPath)) { 109// try { 110// const result = await readFile(jsonPath, 'utf8'); 111// return JSON.parse(result); 112// } catch { 113// return {}; 114// } 115// } 116// return {}; 117// } 118 119/** 初始化 */ 120async function setupDownload() { 121 await checkAndCreateDir(pathConst.downloadPath); 122 // const jsonData = await loadLocalJson(pathConst.downloadPath); 123 124 const newDownloadedData: Record<string, IMusic.IMusicItem> = {}; 125 downloadedMusic = []; 126 127 try { 128 const downloads = await readDir(pathConst.downloadPath); 129 for (let i = 0; i < downloads.length; ++i) { 130 const data = parseFilename(downloads[i].name); 131 if (data) { 132 const platform = data?.platform; 133 const id = data?.id; 134 if (platform && id) { 135 const mi = MediaMeta.get(data) ?? {}; 136 mi.id = id; 137 mi.platform = platform; 138 mi.title = mi.title ?? data.title; 139 mi.artist = mi.artist ?? data.artist; 140 mi[internalSymbolKey] = { 141 localPath: downloads[i].path, 142 }; 143 downloadedMusic.push(mi as IMusic.IMusicItem); 144 } 145 } 146 } 147 148 downloadedStateMapper.notify(); 149 // 去掉冗余数据 150 setStorage('download-music', newDownloadedData); 151 } catch (e) { 152 errorLog('本地下载初始化失败', e); 153 } 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 const nextItem = pendingMusicQueue[0]; 167 const musicItem = nextItem.musicItem; 168 let url = musicItem.url; 169 let headers = musicItem.headers; 170 removeFromPendingQueue(nextItem); 171 downloadingMusicQueue = produce(downloadingMusicQueue, draft => { 172 draft.push(nextItem); 173 }); 174 downloadingQueueStateMapper.notify(); 175 if (!url || !url?.startsWith('http')) { 176 // 插件播放 177 const plugin = PluginManager.getByName(musicItem.platform); 178 if (plugin) { 179 try { 180 const data = await plugin.methods.getMediaSource(musicItem); 181 url = data?.url; 182 headers = data?.headers; 183 } catch { 184 /** 无法下载,跳过 */ 185 removeFromDownloadingQueue(nextItem); 186 return; 187 } 188 } 189 } 190 191 downloadNext(); 192 const {promise, jobId} = downloadFile({ 193 fromUrl: url ?? '', 194 toFile: pathConst.downloadPath + nextItem.filename, 195 headers: headers, 196 background: true, 197 begin(res) { 198 downloadingProgress = produce(downloadingProgress, _ => { 199 _[nextItem.filename] = { 200 progress: 0, 201 size: res.contentLength, 202 }; 203 }); 204 startNotifyProgress(); 205 }, 206 progress(res) { 207 downloadingProgress = produce(downloadingProgress, _ => { 208 _[nextItem.filename] = { 209 progress: res.bytesWritten, 210 size: res.contentLength, 211 }; 212 }); 213 }, 214 }); 215 nextItem.jobId = jobId; 216 try { 217 await promise; 218 // 下载完成 219 downloadedMusic = produce(downloadedMusic, _ => { 220 if ( 221 downloadedMusic.findIndex(_ => 222 isSameMediaItem(musicItem, _), 223 ) === -1 224 ) { 225 _.push({ 226 ...musicItem, 227 [internalSymbolKey]: { 228 localPath: pathConst.downloadPath + nextItem.filename, 229 }, 230 }); 231 } 232 return _; 233 }); 234 removeFromDownloadingQueue(nextItem); 235 MediaMeta.update({ 236 ...musicItem, 237 [internalSerialzeKey]: { 238 downloaded: true, 239 local: { 240 localUrl: pathConst.downloadPath + nextItem.filename, 241 }, 242 }, 243 }); 244 if (downloadingMusicQueue.length === 0) { 245 stopNotifyProgress(); 246 Toast.success('下载完成'); 247 downloadingMusicQueue = []; 248 pendingMusicQueue = []; 249 downloadingQueueStateMapper.notify(); 250 pendingMusicQueueStateMapper.notify(); 251 } 252 delete downloadingProgress[nextItem.filename]; 253 downloadedStateMapper.notify(); 254 downloadNext(); 255 } catch { 256 downloadingMusicQueue = produce(downloadingMusicQueue, _ => 257 _.filter(item => !isSameMediaItem(item.musicItem, musicItem)), 258 ); 259 } 260} 261 262/** 下载音乐 */ 263function downloadMusic(musicItems: IMusic.IMusicItem | IMusic.IMusicItem[]) { 264 if (Network.isOffline()) { 265 Toast.warn('当前无网络,无法下载'); 266 return; 267 } 268 if ( 269 Network.isCellular() && 270 !Config.get('setting.basic.useCelluarNetworkDownload') 271 ) { 272 Toast.warn('当前设置移动网络不可下载,可在侧边栏基本设置修改'); 273 return; 274 } 275 // 如果已经在下载中 276 if (!Array.isArray(musicItems)) { 277 musicItems = [musicItems]; 278 } 279 musicItems = musicItems.filter( 280 musicItem => 281 pendingMusicQueue.findIndex(_ => 282 isSameMediaItem(_.musicItem, musicItem), 283 ) === -1 && 284 downloadingMusicQueue.findIndex(_ => 285 isSameMediaItem(_.musicItem, musicItem), 286 ) === -1, 287 ); 288 const enqueueData = musicItems.map(_ => ({ 289 musicItem: _, 290 filename: generateFilename(_), 291 })); 292 if (enqueueData.length) { 293 pendingMusicQueue = pendingMusicQueue.concat(enqueueData); 294 pendingMusicQueueStateMapper.notify(); 295 maxDownload = +(Config.get('setting.basic.maxDownload') ?? 3); 296 downloadNext(); 297 } 298} 299 300/** 是否下载 */ 301function isDownloaded(mi: IMusic.IMusicItem | null) { 302 return mi 303 ? downloadedMusic.findIndex(_ => isSameMediaItem(_, mi)) !== -1 304 : false; 305} 306 307/** 获取下载的音乐 */ 308function getDownloaded(mi: ICommon.IMediaBase | null) { 309 return mi ? downloadedMusic.find(_ => isSameMediaItem(_, mi)) : null; 310} 311 312/** 移除下载的文件 */ 313async function removeDownloaded(mi: IMusic.IMusicItem) { 314 const localPath = getDownloaded(mi)?.[internalSymbolKey]?.localPath; 315 if (localPath) { 316 await unlink(localPath); 317 downloadedMusic = downloadedMusic.filter(_ => !isSameMediaItem(_, mi)); 318 MediaMeta.update(mi, undefined); 319 downloadedStateMapper.notify(); 320 } 321} 322 323/** 某个音乐是否被下载-状态 */ 324function useIsDownloaded(mi: IMusic.IMusicItem | null) { 325 const downloadedMusicState = downloadedStateMapper.useMappedState(); 326 const [downloaded, setDownloaded] = useState<boolean>(isDownloaded(mi)); 327 useEffect(() => { 328 if (!mi) { 329 setDownloaded(false); 330 } else { 331 setDownloaded( 332 downloadedMusicState.findIndex(_ => isSameMediaItem(mi, _)) !== 333 -1, 334 ); 335 } 336 }, [downloadedMusicState, mi]); 337 return downloaded; 338} 339 340const Download = { 341 downloadMusic, 342 setup: setupDownload, 343 useDownloadedMusic: downloadedStateMapper.useMappedState, 344 useDownloadingMusic: downloadingQueueStateMapper.useMappedState, 345 usePendingMusic: pendingMusicQueueStateMapper.useMappedState, 346 useDownloadingProgress: downloadingProgressStateMapper.useMappedState, 347 isDownloaded, 348 useIsDownloaded, 349 getDownloaded, 350 removeDownloaded, 351}; 352 353export default Download; 354