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