xref: /MusicFree/src/core/download.ts (revision 3d4d06d97c4d26f92447d0589edfb5b2cfaed4d5)
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 `${encodeURIComponent(musicItem.platform)}@${encodeURIComponent(
121        musicItem.id,
122    )}@${encodeURIComponent(musicItem.title)}@${encodeURIComponent(
123        musicItem.artist,
124    )}`.slice(0, 200);
125}
126
127/** todo 可以配置一个说明文件 */
128// async function loadLocalJson(dirBase: string) {
129//   const jsonPath = dirBase + 'data.json';
130//   if (await exists(jsonPath)) {
131//     try {
132//       const result = await readFile(jsonPath, 'utf8');
133//       return JSON.parse(result);
134//     } catch {
135//       return {};
136//     }
137//   }
138//   return {};
139// }
140
141let maxDownload = 3;
142/** 队列下载*/
143async function downloadNext() {
144    // todo 最大同时下载3个,可设置
145    if (
146        downloadingMusicQueue.length >= maxDownload ||
147        pendingMusicQueue.length === 0
148    ) {
149        return;
150    }
151    // 下一个下载的为pending的第一个
152    let nextDownloadItem = pendingMusicQueue[0];
153    const musicItem = nextDownloadItem.musicItem;
154    let url = musicItem.url;
155    let headers = musicItem.headers;
156    removeFromPendingQueue(nextDownloadItem);
157    downloadingMusicQueue = produce(downloadingMusicQueue, draft => {
158        draft.push(nextDownloadItem);
159    });
160    downloadingQueueStateMapper.notify();
161    const quality = nextDownloadItem.quality;
162    const plugin = PluginManager.getByName(musicItem.platform);
163    // 插件播放
164    try {
165        if (plugin) {
166            const qualityOrder = getQualityOrder(
167                quality ??
168                    Config.get('setting.basic.defaultDownloadQuality') ??
169                    'standard',
170                Config.get('setting.basic.downloadQualityOrder') ?? 'asc',
171            );
172            let data: IPlugin.IMediaSourceResult | null = null;
173            for (let quality of qualityOrder) {
174                try {
175                    data = await plugin.methods.getMediaSource(
176                        musicItem,
177                        quality,
178                        1,
179                        true,
180                    );
181                    if (!data?.url) {
182                        continue;
183                    }
184                    break;
185                } catch {}
186            }
187            url = data?.url ?? url;
188            headers = data?.headers;
189        }
190        if (!url) {
191            throw new Error('empty');
192        }
193    } catch {
194        /** 无法下载,跳过 */
195        removeFromDownloadingQueue(nextDownloadItem);
196        return;
197    }
198    /** 预处理完成,接下来去下载音乐 */
199    downloadNextAfterInteraction();
200    let extension = getExtensionName(url);
201    const extensionWithDot = `.${extension}`;
202    if (supportLocalMediaType.every(_ => _ !== extensionWithDot)) {
203        extension = 'mp3';
204    }
205    /** 目标下载地址 */
206    const targetDownloadPath = addFileScheme(
207        getDownloadPath(`${nextDownloadItem.filename}.${extension}`),
208    );
209    const {promise, jobId} = downloadFile({
210        fromUrl: url ?? '',
211        toFile: targetDownloadPath,
212        headers: headers,
213        background: true,
214        begin(res) {
215            downloadingProgress = produce(downloadingProgress, _ => {
216                _[nextDownloadItem.filename] = {
217                    progress: 0,
218                    size: res.contentLength,
219                };
220            });
221            startNotifyProgress();
222        },
223        progress(res) {
224            downloadingProgress = produce(downloadingProgress, _ => {
225                _[nextDownloadItem.filename] = {
226                    progress: res.bytesWritten,
227                    size: res.contentLength,
228                };
229            });
230        },
231    });
232    nextDownloadItem = {...nextDownloadItem, jobId};
233    try {
234        await promise;
235        /** 下载完成 */
236        LocalMusicSheet.addMusicDraft({
237            ...musicItem,
238            [internalSerializeKey]: {
239                localPath: targetDownloadPath,
240            },
241        });
242        MediaMeta.update({
243            ...musicItem,
244            [internalSerializeKey]: {
245                downloaded: true,
246                local: {
247                    localUrl: targetDownloadPath,
248                },
249            },
250        });
251        // const primaryKey = plugin?.instance.primaryKey ?? [];
252        // if (!primaryKey.includes('id')) {
253        //     primaryKey.push('id');
254        // }
255        // const stringifyMeta: Record<string, any> = {
256        //     title: musicItem.title,
257        //     artist: musicItem.artist,
258        //     album: musicItem.album,
259        //     lrc: musicItem.lrc,
260        //     platform: musicItem.platform,
261        // };
262        // primaryKey.forEach(_ => {
263        //     stringifyMeta[_] = musicItem[_];
264        // });
265
266        // await Mp3Util.getMediaTag(filePath).then(_ => {
267        //     console.log(_);
268        // }).catch(console.log);
269    } catch (e: any) {
270        console.log(e, 'downloaderror');
271        /** 下载出错 */
272        errorLog('下载出错', e?.message);
273    }
274    removeFromDownloadingQueue(nextDownloadItem);
275    downloadingProgress = produce(downloadingProgress, draft => {
276        if (draft[nextDownloadItem.filename]) {
277            delete draft[nextDownloadItem.filename];
278        }
279    });
280    downloadNextAfterInteraction();
281    if (downloadingMusicQueue.length === 0) {
282        stopNotifyProgress();
283        LocalMusicSheet.saveLocalSheet();
284        Toast.success('下载完成');
285        downloadingMusicQueue = [];
286        pendingMusicQueue = [];
287        downloadingQueueStateMapper.notify();
288        pendingMusicQueueStateMapper.notify();
289    }
290}
291
292async function downloadNextAfterInteraction() {
293    InteractionManager.runAfterInteractions(downloadNext);
294}
295
296/** 加入下载队列 */
297function downloadMusic(
298    musicItems: IMusic.IMusicItem | IMusic.IMusicItem[],
299    quality?: IMusic.IQualityKey,
300) {
301    if (Network.isOffline()) {
302        Toast.warn('当前无网络,无法下载');
303        return;
304    }
305    if (
306        Network.isCellular() &&
307        !Config.get('setting.basic.useCelluarNetworkDownload')
308    ) {
309        Toast.warn('当前设置移动网络不可下载,可在侧边栏基本设置修改');
310        return;
311    }
312    // 如果已经在下载中
313    if (!Array.isArray(musicItems)) {
314        musicItems = [musicItems];
315    }
316    musicItems = musicItems.filter(
317        musicItem =>
318            pendingMusicQueue.findIndex(_ =>
319                isSameMediaItem(_.musicItem, musicItem),
320            ) === -1 &&
321            downloadingMusicQueue.findIndex(_ =>
322                isSameMediaItem(_.musicItem, musicItem),
323            ) === -1 &&
324            !LocalMusicSheet.isLocalMusic(musicItem),
325    );
326    const enqueueData = musicItems.map(_ => ({
327        musicItem: _,
328        filename: generateFilename(_),
329        quality,
330    }));
331    if (enqueueData.length) {
332        pendingMusicQueue = pendingMusicQueue.concat(enqueueData);
333        pendingMusicQueueStateMapper.notify();
334        maxDownload = +(Config.get('setting.basic.maxDownload') ?? 3);
335        downloadNextAfterInteraction();
336    }
337}
338
339const Download = {
340    downloadMusic,
341    useDownloadingMusic: downloadingQueueStateMapper.useMappedState,
342    usePendingMusic: pendingMusicQueueStateMapper.useMappedState,
343    useDownloadingProgress: downloadingProgressStateMapper.useMappedState,
344};
345
346export default Download;
347