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