1import {internalSerializeKey} from '@/constants/commonConst'; 2import pathConst from '@/constants/pathConst'; 3import {isSameMediaItem} from '@/utils/mediaItem'; 4import StateMapper from '@/utils/stateMapper'; 5import Toast from '@/utils/toast'; 6import produce from 'immer'; 7import {downloadFile} from 'react-native-fs'; 8 9import Config from './config'; 10import LocalMusicSheet from './localMusicSheet'; 11import MediaMeta from './mediaMeta'; 12import Network from './network'; 13import PluginManager from './pluginManager'; 14 15interface IDownloadMusicOptions { 16 musicItem: IMusic.IMusicItem; 17 filename: string; 18 jobId?: number; 19} 20// todo: 直接把下载信息写在meta里面就好了 21/** 下载中 */ 22let downloadingMusicQueue: IDownloadMusicOptions[] = []; 23/** 队列中 */ 24let pendingMusicQueue: IDownloadMusicOptions[] = []; 25 26/** 进度 */ 27let downloadingProgress: Record<string, {progress: number; size: number}> = {}; 28 29const downloadingQueueStateMapper = new StateMapper( 30 () => downloadingMusicQueue, 31); 32const pendingMusicQueueStateMapper = new StateMapper(() => pendingMusicQueue); 33const downloadingProgressStateMapper = new StateMapper( 34 () => downloadingProgress, 35); 36 37/** 从待下载中移除 */ 38function removeFromPendingQueue(item: IDownloadMusicOptions) { 39 pendingMusicQueue = pendingMusicQueue.filter( 40 _ => !isSameMediaItem(_.musicItem, item.musicItem), 41 ); 42 pendingMusicQueueStateMapper.notify(); 43} 44 45/** 从下载中队列移除 */ 46function removeFromDownloadingQueue(item: IDownloadMusicOptions) { 47 downloadingMusicQueue = downloadingMusicQueue.filter( 48 _ => !isSameMediaItem(_.musicItem, item.musicItem), 49 ); 50 downloadingQueueStateMapper.notify(); 51} 52 53/** 防止高频同步 */ 54let progressNotifyTimer: any = null; 55function startNotifyProgress() { 56 if (progressNotifyTimer) { 57 return; 58 } 59 60 progressNotifyTimer = setTimeout(() => { 61 progressNotifyTimer = null; 62 downloadingProgressStateMapper.notify(); 63 startNotifyProgress(); 64 }, 400); 65} 66 67function stopNotifyProgress() { 68 if (progressNotifyTimer) { 69 clearInterval(progressNotifyTimer); 70 } 71 progressNotifyTimer = null; 72} 73 74/** 生成下载文件名 */ 75function generateFilename(musicItem: IMusic.IMusicItem) { 76 return ( 77 `${musicItem.platform}@${musicItem.id}@${musicItem.title}@${musicItem.artist}`.slice( 78 0, 79 200, 80 ) + '.mp3' 81 ); 82} 83 84/** todo 可以配置一个说明文件 */ 85// async function loadLocalJson(dirBase: string) { 86// const jsonPath = dirBase + 'data.json'; 87// if (await exists(jsonPath)) { 88// try { 89// const result = await readFile(jsonPath, 'utf8'); 90// return JSON.parse(result); 91// } catch { 92// return {}; 93// } 94// } 95// return {}; 96// } 97 98let maxDownload = 3; 99/** 从队列取出下一个要下载的 */ 100async function downloadNext() { 101 // todo 最大同时下载3个,可设置 102 if ( 103 downloadingMusicQueue.length >= maxDownload || 104 pendingMusicQueue.length === 0 105 ) { 106 return; 107 } 108 const nextItem = pendingMusicQueue[0]; 109 const musicItem = nextItem.musicItem; 110 let url = musicItem.url; 111 let headers = musicItem.headers; 112 removeFromPendingQueue(nextItem); 113 downloadingMusicQueue = produce(downloadingMusicQueue, draft => { 114 draft.push(nextItem); 115 }); 116 downloadingQueueStateMapper.notify(); 117 if (!url || !url?.startsWith('http')) { 118 // 插件播放 119 const plugin = PluginManager.getByName(musicItem.platform); 120 if (plugin) { 121 try { 122 const data = await plugin.methods.getMediaSource(musicItem); 123 url = data?.url; 124 headers = data?.headers; 125 } catch { 126 /** 无法下载,跳过 */ 127 removeFromDownloadingQueue(nextItem); 128 return; 129 } 130 } 131 } 132 133 downloadNext(); 134 const {promise, jobId} = downloadFile({ 135 fromUrl: url ?? '', 136 toFile: pathConst.downloadMusicPath + nextItem.filename, 137 headers: headers, 138 background: true, 139 begin(res) { 140 downloadingProgress = produce(downloadingProgress, _ => { 141 _[nextItem.filename] = { 142 progress: 0, 143 size: res.contentLength, 144 }; 145 }); 146 startNotifyProgress(); 147 }, 148 progress(res) { 149 downloadingProgress = produce(downloadingProgress, _ => { 150 _[nextItem.filename] = { 151 progress: res.bytesWritten, 152 size: res.contentLength, 153 }; 154 }); 155 }, 156 }); 157 nextItem.jobId = jobId; 158 try { 159 await promise; 160 LocalMusicSheet.addMusic({ 161 ...musicItem, 162 [internalSerializeKey]: { 163 localPath: pathConst.downloadMusicPath + nextItem.filename, 164 }, 165 }); 166 removeFromDownloadingQueue(nextItem); 167 MediaMeta.update({ 168 ...musicItem, 169 [internalSerializeKey]: { 170 downloaded: true, 171 local: { 172 localUrl: pathConst.downloadMusicPath + nextItem.filename, 173 }, 174 }, 175 }); 176 if (downloadingMusicQueue.length === 0) { 177 stopNotifyProgress(); 178 Toast.success('下载完成'); 179 downloadingMusicQueue = []; 180 pendingMusicQueue = []; 181 downloadingQueueStateMapper.notify(); 182 pendingMusicQueueStateMapper.notify(); 183 } 184 delete downloadingProgress[nextItem.filename]; 185 downloadNext(); 186 } catch { 187 downloadingMusicQueue = produce(downloadingMusicQueue, _ => 188 _.filter(item => !isSameMediaItem(item.musicItem, musicItem)), 189 ); 190 } 191} 192 193/** 下载音乐 */ 194function downloadMusic(musicItems: IMusic.IMusicItem | IMusic.IMusicItem[]) { 195 if (Network.isOffline()) { 196 Toast.warn('当前无网络,无法下载'); 197 return; 198 } 199 if ( 200 Network.isCellular() && 201 !Config.get('setting.basic.useCelluarNetworkDownload') 202 ) { 203 Toast.warn('当前设置移动网络不可下载,可在侧边栏基本设置修改'); 204 return; 205 } 206 // 如果已经在下载中 207 if (!Array.isArray(musicItems)) { 208 musicItems = [musicItems]; 209 } 210 musicItems = musicItems.filter( 211 musicItem => 212 pendingMusicQueue.findIndex(_ => 213 isSameMediaItem(_.musicItem, musicItem), 214 ) === -1 && 215 downloadingMusicQueue.findIndex(_ => 216 isSameMediaItem(_.musicItem, musicItem), 217 ) === -1, 218 ); 219 const enqueueData = musicItems.map(_ => ({ 220 musicItem: _, 221 filename: generateFilename(_), 222 })); 223 if (enqueueData.length) { 224 pendingMusicQueue = pendingMusicQueue.concat(enqueueData); 225 pendingMusicQueueStateMapper.notify(); 226 maxDownload = +(Config.get('setting.basic.maxDownload') ?? 3); 227 downloadNext(); 228 } 229} 230 231const Download = { 232 downloadMusic, 233 useDownloadingMusic: downloadingQueueStateMapper.useMappedState, 234 usePendingMusic: pendingMusicQueueStateMapper.useMappedState, 235 useDownloadingProgress: downloadingProgressStateMapper.useMappedState, 236}; 237 238export default Download; 239