xref: /MusicFree/src/core/download.ts (revision d1a36a67ac3d03c61d8bafd8ac2bc121ea4f6f3e)
1import {
2    internalSerializeKey,
3    supportLocalMediaType,
4} from '@/constants/commonConst';
5import pathConst from '@/constants/pathConst';
6import {addFileScheme, escapeCharacter, mkdirR} 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, exists} from 'react-native-fs';
15
16import Config from './config';
17import LocalMusicSheet from './localMusicSheet';
18import MediaMeta from './mediaExtra';
19import Network from './network';
20import PluginManager from './pluginManager';
21import {check, PERMISSIONS} from 'react-native-permissions';
22import path from 'path-browserify';
23import {
24    getCurrentDialog,
25    hideDialog,
26    showDialog,
27} from '@/components/dialogs/useDialog';
28
29/** 队列中的元素 */
30interface IDownloadMusicOptions {
31    /** 要下载的音乐 */
32    musicItem: IMusic.IMusicItem;
33    /** 目标文件名 */
34    filename: string;
35    /** 下载id */
36    jobId?: number;
37    /** 下载音质 */
38    quality?: IMusic.IQualityKey;
39}
40
41/** 下载中 */
42let downloadingMusicQueue: IDownloadMusicOptions[] = [];
43/** 队列中 */
44let pendingMusicQueue: IDownloadMusicOptions[] = [];
45/** 下载进度 */
46let downloadingProgress: Record<string, {progress: number; size: number}> = {};
47/** 错误信息 */
48let hasError: boolean = false;
49
50const downloadingQueueStateMapper = new StateMapper(
51    () => downloadingMusicQueue,
52);
53const pendingMusicQueueStateMapper = new StateMapper(() => pendingMusicQueue);
54const downloadingProgressStateMapper = new StateMapper(
55    () => downloadingProgress,
56);
57
58/** 匹配文件后缀 */
59const getExtensionName = (url: string) => {
60    const regResult = url.match(
61        /^https?\:\/\/.+\.([^\?\.]+?$)|(?:([^\.]+?)\?.+$)/,
62    );
63    if (regResult) {
64        return regResult[1] ?? regResult[2] ?? 'mp3';
65    } else {
66        return 'mp3';
67    }
68};
69
70/** 生成下载文件 */
71const getDownloadPath = (fileName?: string) => {
72    const dlPath =
73        Config.get('setting.basic.downloadPath') ?? pathConst.downloadMusicPath;
74    if (!dlPath.endsWith('/')) {
75        return `${dlPath}/${fileName ?? ''}`;
76    }
77    return fileName ? dlPath + fileName : dlPath;
78};
79
80/** 从待下载中移除 */
81function removeFromPendingQueue(item: IDownloadMusicOptions) {
82    const targetIndex = pendingMusicQueue.findIndex(_ =>
83        isSameMediaItem(_.musicItem, item.musicItem),
84    );
85    if (targetIndex !== -1) {
86        pendingMusicQueue = pendingMusicQueue
87            .slice(0, targetIndex)
88            .concat(pendingMusicQueue.slice(targetIndex + 1));
89        pendingMusicQueueStateMapper.notify();
90    }
91}
92
93/** 从下载中队列移除 */
94function removeFromDownloadingQueue(item: IDownloadMusicOptions) {
95    const targetIndex = downloadingMusicQueue.findIndex(_ =>
96        isSameMediaItem(_.musicItem, item.musicItem),
97    );
98    if (targetIndex !== -1) {
99        downloadingMusicQueue = downloadingMusicQueue
100            .slice(0, targetIndex)
101            .concat(downloadingMusicQueue.slice(targetIndex + 1));
102        downloadingQueueStateMapper.notify();
103    }
104}
105
106/** 防止高频同步 */
107let progressNotifyTimer: any = null;
108function startNotifyProgress() {
109    if (progressNotifyTimer) {
110        return;
111    }
112
113    progressNotifyTimer = setTimeout(() => {
114        progressNotifyTimer = null;
115        downloadingProgressStateMapper.notify();
116        startNotifyProgress();
117    }, 500);
118}
119
120function stopNotifyProgress() {
121    if (progressNotifyTimer) {
122        clearTimeout(progressNotifyTimer);
123    }
124    progressNotifyTimer = null;
125}
126
127/** 生成下载文件名 */
128function generateFilename(musicItem: IMusic.IMusicItem) {
129    return `${escapeCharacter(musicItem.platform)}@${escapeCharacter(
130        musicItem.id,
131    )}@${escapeCharacter(musicItem.title)}@${escapeCharacter(
132        musicItem.artist,
133    )}`.slice(0, 200);
134}
135
136/** todo 可以配置一个说明文件 */
137// async function loadLocalJson(dirBase: string) {
138//   const jsonPath = dirBase + 'data.json';
139//   if (await exists(jsonPath)) {
140//     try {
141//       const result = await readFile(jsonPath, 'utf8');
142//       return JSON.parse(result);
143//     } catch {
144//       return {};
145//     }
146//   }
147//   return {};
148// }
149
150let maxDownload = 3;
151/** 队列下载*/
152async function downloadNext() {
153    // todo 最大同时下载3个,可设置
154    if (
155        downloadingMusicQueue.length >= maxDownload ||
156        pendingMusicQueue.length === 0
157    ) {
158        return;
159    }
160    // 下一个下载的为pending的第一个
161    let nextDownloadItem = pendingMusicQueue[0];
162    const musicItem = nextDownloadItem.musicItem;
163    let url = musicItem.url;
164    let headers = musicItem.headers;
165    removeFromPendingQueue(nextDownloadItem);
166    downloadingMusicQueue = produce(downloadingMusicQueue, draft => {
167        draft.push(nextDownloadItem);
168    });
169    downloadingQueueStateMapper.notify();
170    const quality = nextDownloadItem.quality;
171    const plugin = PluginManager.getByName(musicItem.platform);
172    // 插件播放
173    try {
174        if (plugin) {
175            const qualityOrder = getQualityOrder(
176                quality ??
177                    Config.get('setting.basic.defaultDownloadQuality') ??
178                    'standard',
179                Config.get('setting.basic.downloadQualityOrder') ?? 'asc',
180            );
181            let data: IPlugin.IMediaSourceResult | null = null;
182            for (let quality of qualityOrder) {
183                try {
184                    data = await plugin.methods.getMediaSource(
185                        musicItem,
186                        quality,
187                        1,
188                        true,
189                    );
190                    if (!data?.url) {
191                        continue;
192                    }
193                    break;
194                } catch {}
195            }
196            url = data?.url ?? url;
197            headers = data?.headers;
198        }
199        if (!url) {
200            throw new Error('empty');
201        }
202    } catch (e: any) {
203        /** 无法下载,跳过 */
204        errorLog('下载失败-无法获取下载链接', {
205            item: {
206                id: nextDownloadItem.musicItem.id,
207                title: nextDownloadItem.musicItem.title,
208                platform: nextDownloadItem.musicItem.platform,
209                quality: nextDownloadItem.quality,
210            },
211            reason: e?.message ?? e,
212        });
213        hasError = true;
214        removeFromDownloadingQueue(nextDownloadItem);
215        return;
216    }
217    /** 预处理完成,接下来去下载音乐 */
218    downloadNextAfterInteraction();
219    let extension = getExtensionName(url);
220    const extensionWithDot = `.${extension}`;
221    if (supportLocalMediaType.every(_ => _ !== extensionWithDot)) {
222        extension = 'mp3';
223    }
224    /** 目标下载地址 */
225    const targetDownloadPath = addFileScheme(
226        getDownloadPath(`${nextDownloadItem.filename}.${extension}`),
227    );
228    const {promise, jobId} = downloadFile({
229        fromUrl: url ?? '',
230        toFile: targetDownloadPath,
231        headers: headers,
232        background: true,
233        begin(res) {
234            downloadingProgress = produce(downloadingProgress, _ => {
235                _[nextDownloadItem.filename] = {
236                    progress: 0,
237                    size: res.contentLength,
238                };
239            });
240            startNotifyProgress();
241        },
242        progress(res) {
243            downloadingProgress = produce(downloadingProgress, _ => {
244                _[nextDownloadItem.filename] = {
245                    progress: res.bytesWritten,
246                    size: res.contentLength,
247                };
248            });
249        },
250    });
251    nextDownloadItem = {...nextDownloadItem, jobId};
252
253    let folder = path.dirname(targetDownloadPath);
254    let folderExists = await exists(folder);
255
256    if (!folderExists) {
257        await mkdirR(folder);
258    }
259    try {
260        await promise;
261        /** 下载完成 */
262        LocalMusicSheet.addMusicDraft({
263            ...musicItem,
264            [internalSerializeKey]: {
265                localPath: targetDownloadPath,
266            },
267        });
268        MediaMeta.update(musicItem, {
269            downloaded: true,
270            localPath: targetDownloadPath,
271        });
272        // const primaryKey = plugin?.instance.primaryKey ?? [];
273        // if (!primaryKey.includes('id')) {
274        //     primaryKey.push('id');
275        // }
276        // const stringifyMeta: Record<string, any> = {
277        //     title: musicItem.title,
278        //     artist: musicItem.artist,
279        //     album: musicItem.album,
280        //     lrc: musicItem.lrc,
281        //     platform: musicItem.platform,
282        // };
283        // primaryKey.forEach(_ => {
284        //     stringifyMeta[_] = musicItem[_];
285        // });
286
287        // await Mp3Util.getMediaTag(filePath).then(_ => {
288        //     console.log(_);
289        // }).catch(console.log);
290    } catch (e: any) {
291        console.log(e, 'downloaderror');
292        /** 下载出错 */
293        errorLog('下载失败', {
294            item: {
295                id: nextDownloadItem.musicItem.id,
296                title: nextDownloadItem.musicItem.title,
297                platform: nextDownloadItem.musicItem.platform,
298                quality: nextDownloadItem.quality,
299            },
300            reason: e?.message ?? e,
301            targetDownloadPath: targetDownloadPath,
302        });
303        hasError = true;
304    }
305    removeFromDownloadingQueue(nextDownloadItem);
306    downloadingProgress = produce(downloadingProgress, draft => {
307        if (draft[nextDownloadItem.filename]) {
308            delete draft[nextDownloadItem.filename];
309        }
310    });
311    downloadNextAfterInteraction();
312    if (downloadingMusicQueue.length === 0) {
313        stopNotifyProgress();
314        LocalMusicSheet.saveLocalSheet();
315        if (hasError) {
316            try {
317                const perm = await check(
318                    PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
319                );
320                if (perm !== 'granted') {
321                    Toast.warn('权限不足,请检查是否授予写入文件的权限');
322                } else {
323                    throw new Error();
324                }
325            } catch {
326                if (getCurrentDialog()?.name !== 'SimpleDialog') {
327                    showDialog('SimpleDialog', {
328                        title: '下载失败',
329                        content:
330                            '部分歌曲下载失败,如果无法下载请检查系统设置中是否授予完整文件读写权限;或者去【侧边栏-权限管理】中查看文件读写权限是否勾选',
331                        onOk: hideDialog,
332                    });
333                }
334            }
335        } else {
336            Toast.success('下载完成');
337        }
338        hasError = false;
339        downloadingMusicQueue = [];
340        pendingMusicQueue = [];
341        downloadingQueueStateMapper.notify();
342        pendingMusicQueueStateMapper.notify();
343    }
344}
345
346async function downloadNextAfterInteraction() {
347    InteractionManager.runAfterInteractions(downloadNext);
348}
349
350/** 加入下载队列 */
351function downloadMusic(
352    musicItems: IMusic.IMusicItem | IMusic.IMusicItem[],
353    quality?: IMusic.IQualityKey,
354) {
355    if (Network.isOffline()) {
356        Toast.warn('当前无网络,无法下载');
357        return;
358    }
359    if (
360        Network.isCellular() &&
361        !Config.get('setting.basic.useCelluarNetworkDownload') &&
362        getCurrentDialog()?.name !== 'SimpleDialog'
363    ) {
364        showDialog('SimpleDialog', {
365            title: '流量提醒',
366            content:
367                '当前非WIFI环境,侧边栏设置中打开【使用移动网络下载】功能后可继续下载',
368        });
369        return;
370    }
371    // 如果已经在下载中
372    if (!Array.isArray(musicItems)) {
373        musicItems = [musicItems];
374    }
375    hasError = false;
376    musicItems = musicItems.filter(
377        musicItem =>
378            pendingMusicQueue.findIndex(_ =>
379                isSameMediaItem(_.musicItem, musicItem),
380            ) === -1 &&
381            downloadingMusicQueue.findIndex(_ =>
382                isSameMediaItem(_.musicItem, musicItem),
383            ) === -1 &&
384            !LocalMusicSheet.isLocalMusic(musicItem),
385    );
386    const enqueueData = musicItems.map(_ => {
387        return {
388            musicItem: _,
389            filename: generateFilename(_),
390            quality,
391        };
392    });
393    if (enqueueData.length) {
394        pendingMusicQueue = pendingMusicQueue.concat(enqueueData);
395        pendingMusicQueueStateMapper.notify();
396        maxDownload = +(Config.get('setting.basic.maxDownload') ?? 3);
397        downloadNextAfterInteraction();
398    }
399}
400
401const Download = {
402    downloadMusic,
403    useDownloadingMusic: downloadingQueueStateMapper.useMappedState,
404    usePendingMusic: pendingMusicQueueStateMapper.useMappedState,
405    useDownloadingProgress: downloadingProgressStateMapper.useMappedState,
406};
407
408export default Download;
409