xref: /MusicFree/src/core/download.ts (revision 9501a53846abeb6e52d388c9c5031f73671944ef)
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