xref: /MusicFree/src/core/download.ts (revision bc3071751dde42f2310c1d49bc94e1362e2a4aa1)
1import {internalSerializeKey} from '@/constants/commonConst';
2import pathConst from '@/constants/pathConst';
3import {errorLog} from '@/utils/log';
4import {isSameMediaItem} from '@/utils/mediaItem';
5import {getQualityOrder} from '@/utils/qualities';
6import StateMapper from '@/utils/stateMapper';
7import Toast from '@/utils/toast';
8import produce from 'immer';
9import {InteractionManager} from 'react-native';
10import {downloadFile} from 'react-native-fs';
11
12import Config from './config';
13import LocalMusicSheet from './localMusicSheet';
14import MediaMeta from './mediaMeta';
15import Network from './network';
16import PluginManager from './pluginManager';
17
18/** 队列中的元素 */
19interface IDownloadMusicOptions {
20    /** 要下载的音乐 */
21    musicItem: IMusic.IMusicItem;
22    /** 目标文件名 */
23    filename: string;
24    /** 下载id */
25    jobId?: number;
26    /** 下载音质 */
27    quality?: IMusic.IQualityKey;
28}
29
30/** 下载中 */
31let downloadingMusicQueue: IDownloadMusicOptions[] = [];
32/** 队列中 */
33let pendingMusicQueue: IDownloadMusicOptions[] = [];
34/** 下载进度 */
35let downloadingProgress: Record<string, {progress: number; size: number}> = {};
36
37const downloadingQueueStateMapper = new StateMapper(
38    () => downloadingMusicQueue,
39);
40const pendingMusicQueueStateMapper = new StateMapper(() => pendingMusicQueue);
41const downloadingProgressStateMapper = new StateMapper(
42    () => downloadingProgress,
43);
44
45/** 匹配文件后缀 */
46const getExtensionName = (url: string) => {
47    const regResult = url.match(
48        /^https?\:\/\/.+\.([^\?\.]+?$)|(?:([^\.]+?)\?.+$)/,
49    );
50    if (regResult) {
51        return regResult[1] ?? regResult[2] ?? 'mp3';
52    } else {
53        return 'mp3';
54    }
55};
56
57/** 生成下载文件 */
58const getDownloadPath = (fileName?: string) => {
59    const dlPath =
60        Config.get('setting.basic.downloadPath') ?? pathConst.downloadMusicPath;
61    if (!dlPath.endsWith('/')) {
62        return `${dlPath}/${fileName ?? ''}`;
63    }
64    return fileName ? dlPath + fileName : dlPath;
65};
66
67/** 从待下载中移除 */
68function removeFromPendingQueue(item: IDownloadMusicOptions) {
69    const targetIndex = pendingMusicQueue.findIndex(_ =>
70        isSameMediaItem(_.musicItem, item.musicItem),
71    );
72    if (targetIndex !== -1) {
73        pendingMusicQueue = pendingMusicQueue
74            .slice(0, targetIndex)
75            .concat(pendingMusicQueue.slice(targetIndex + 1));
76        pendingMusicQueueStateMapper.notify();
77    }
78}
79
80/** 从下载中队列移除 */
81function removeFromDownloadingQueue(item: IDownloadMusicOptions) {
82    const targetIndex = downloadingMusicQueue.findIndex(_ =>
83        isSameMediaItem(_.musicItem, item.musicItem),
84    );
85    if (targetIndex !== -1) {
86        downloadingMusicQueue = downloadingMusicQueue
87            .slice(0, targetIndex)
88            .concat(downloadingMusicQueue.slice(targetIndex + 1));
89        downloadingQueueStateMapper.notify();
90    }
91}
92
93/** 防止高频同步 */
94let progressNotifyTimer: any = null;
95function startNotifyProgress() {
96    if (progressNotifyTimer) {
97        return;
98    }
99
100    progressNotifyTimer = setTimeout(() => {
101        progressNotifyTimer = null;
102        downloadingProgressStateMapper.notify();
103        startNotifyProgress();
104    }, 500);
105}
106
107function stopNotifyProgress() {
108    if (progressNotifyTimer) {
109        clearTimeout(progressNotifyTimer);
110    }
111    progressNotifyTimer = null;
112}
113
114/** 生成下载文件名 */
115function generateFilename(musicItem: IMusic.IMusicItem) {
116    return `${musicItem.platform}@${musicItem.id}@${musicItem.title}@${musicItem.artist}`.slice(
117        0,
118        200,
119    );
120}
121
122/** todo 可以配置一个说明文件 */
123// async function loadLocalJson(dirBase: string) {
124//   const jsonPath = dirBase + 'data.json';
125//   if (await exists(jsonPath)) {
126//     try {
127//       const result = await readFile(jsonPath, 'utf8');
128//       return JSON.parse(result);
129//     } catch {
130//       return {};
131//     }
132//   }
133//   return {};
134// }
135
136let maxDownload = 3;
137/** 队列下载*/
138async function downloadNext() {
139    // todo 最大同时下载3个,可设置
140    if (
141        downloadingMusicQueue.length >= maxDownload ||
142        pendingMusicQueue.length === 0
143    ) {
144        return;
145    }
146    // 下一个下载的为pending的第一个
147    let nextDownloadItem = pendingMusicQueue[0];
148    const musicItem = nextDownloadItem.musicItem;
149    let url = musicItem.url;
150    let headers = musicItem.headers;
151    removeFromPendingQueue(nextDownloadItem);
152    downloadingMusicQueue = produce(downloadingMusicQueue, draft => {
153        draft.push(nextDownloadItem);
154    });
155    downloadingQueueStateMapper.notify();
156    const quality = nextDownloadItem.quality;
157    const plugin = PluginManager.getByName(musicItem.platform);
158    // 插件播放
159    try {
160        if (plugin) {
161            const qualityOrder = getQualityOrder(
162                quality ??
163                    Config.get('setting.basic.defaultDownloadQuality') ??
164                    'standard',
165                Config.get('setting.basic.downloadQualityOrder') ?? 'asc',
166            );
167            let data: IPlugin.IMediaSourceResult | null = null;
168            for (let quality of qualityOrder) {
169                try {
170                    data = await plugin.methods.getMediaSource(
171                        musicItem,
172                        quality,
173                        1,
174                        true,
175                    );
176                    if (!data?.url) {
177                        continue;
178                    }
179                    break;
180                } catch {}
181            }
182            url = data?.url ?? url;
183            headers = data?.headers;
184        }
185        if (!url) {
186            throw new Error('empty');
187        }
188    } catch {
189        /** 无法下载,跳过 */
190        removeFromDownloadingQueue(nextDownloadItem);
191        return;
192    }
193    /** 预处理完成,接下来去下载音乐 */
194    downloadNextAfterInteraction();
195    const extension = getExtensionName(url);
196    /** 目标下载地址 */
197    const targetDownloadPath = getDownloadPath(
198        `${nextDownloadItem.filename}.${extension}`,
199    );
200    const {promise, jobId} = downloadFile({
201        fromUrl: url ?? '',
202        toFile: targetDownloadPath,
203        headers: headers,
204        background: true,
205        begin(res) {
206            downloadingProgress = produce(downloadingProgress, _ => {
207                _[nextDownloadItem.filename] = {
208                    progress: 0,
209                    size: res.contentLength,
210                };
211            });
212            startNotifyProgress();
213        },
214        progress(res) {
215            downloadingProgress = produce(downloadingProgress, _ => {
216                _[nextDownloadItem.filename] = {
217                    progress: res.bytesWritten,
218                    size: res.contentLength,
219                };
220            });
221        },
222    });
223    nextDownloadItem = {...nextDownloadItem, jobId};
224    try {
225        await promise;
226        /** 下载完成 */
227        LocalMusicSheet.addMusicDraft({
228            ...musicItem,
229            [internalSerializeKey]: {
230                localPath: targetDownloadPath,
231            },
232        });
233        MediaMeta.update({
234            ...musicItem,
235            [internalSerializeKey]: {
236                downloaded: true,
237                local: {
238                    localUrl: targetDownloadPath,
239                },
240            },
241        });
242        // const primaryKey = plugin?.instance.primaryKey ?? [];
243        // if (!primaryKey.includes('id')) {
244        //     primaryKey.push('id');
245        // }
246        // const stringifyMeta: Record<string, any> = {
247        //     title: musicItem.title,
248        //     artist: musicItem.artist,
249        //     album: musicItem.album,
250        //     lrc: musicItem.lrc,
251        //     platform: musicItem.platform,
252        // };
253        // primaryKey.forEach(_ => {
254        //     stringifyMeta[_] = musicItem[_];
255        // });
256
257        // await Mp3Util.setMediaMeta(targetDownloadPath, {
258        //     title: musicItem.title,
259        //     artist: musicItem.artist,
260        //     album: musicItem.album,
261        //     lyric: musicItem.rawLrc,
262        //     comment: JSON.stringify(stringifyMeta),
263        // });
264    } catch (e: any) {
265        console.log(e, 'downloaderror');
266        /** 下载出错 */
267        errorLog('下载出错', e?.message);
268    }
269    removeFromDownloadingQueue(nextDownloadItem);
270    downloadingProgress = produce(downloadingProgress, draft => {
271        if (draft[nextDownloadItem.filename]) {
272            delete draft[nextDownloadItem.filename];
273        }
274    });
275    downloadNextAfterInteraction();
276    if (downloadingMusicQueue.length === 0) {
277        stopNotifyProgress();
278        LocalMusicSheet.saveLocalSheet();
279        Toast.success('下载完成');
280        downloadingMusicQueue = [];
281        pendingMusicQueue = [];
282        downloadingQueueStateMapper.notify();
283        pendingMusicQueueStateMapper.notify();
284    }
285}
286
287async function downloadNextAfterInteraction() {
288    InteractionManager.runAfterInteractions(downloadNext);
289}
290
291/** 加入下载队列 */
292function downloadMusic(
293    musicItems: IMusic.IMusicItem | IMusic.IMusicItem[],
294    quality?: IMusic.IQualityKey,
295) {
296    if (Network.isOffline()) {
297        Toast.warn('当前无网络,无法下载');
298        return;
299    }
300    if (
301        Network.isCellular() &&
302        !Config.get('setting.basic.useCelluarNetworkDownload')
303    ) {
304        Toast.warn('当前设置移动网络不可下载,可在侧边栏基本设置修改');
305        return;
306    }
307    // 如果已经在下载中
308    if (!Array.isArray(musicItems)) {
309        musicItems = [musicItems];
310    }
311    musicItems = musicItems.filter(
312        musicItem =>
313            pendingMusicQueue.findIndex(_ =>
314                isSameMediaItem(_.musicItem, musicItem),
315            ) === -1 &&
316            downloadingMusicQueue.findIndex(_ =>
317                isSameMediaItem(_.musicItem, musicItem),
318            ) === -1 &&
319            !LocalMusicSheet.isLocalMusic(musicItem),
320    );
321    const enqueueData = musicItems.map(_ => ({
322        musicItem: _,
323        filename: generateFilename(_),
324        quality,
325    }));
326    if (enqueueData.length) {
327        pendingMusicQueue = pendingMusicQueue.concat(enqueueData);
328        pendingMusicQueueStateMapper.notify();
329        maxDownload = +(Config.get('setting.basic.maxDownload') ?? 3);
330        downloadNextAfterInteraction();
331    }
332}
333
334const Download = {
335    downloadMusic,
336    useDownloadingMusic: downloadingQueueStateMapper.useMappedState,
337    usePendingMusic: pendingMusicQueueStateMapper.useMappedState,
338    useDownloadingProgress: downloadingProgressStateMapper.useMappedState,
339};
340
341export default Download;
342