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