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