xref: /MusicFree/src/core/pluginManager.ts (revision adf41771e5c3ca7c27879b461cece7e444d1dc58)
1import RNFS, {
2    copyFile,
3    exists,
4    readDir,
5    readFile,
6    unlink,
7    writeFile,
8} from 'react-native-fs';
9import CryptoJs from 'crypto-js';
10import dayjs from 'dayjs';
11import axios from 'axios';
12import bigInt from 'big-integer';
13import qs from 'qs';
14import * as webdav from 'webdav';
15import {InteractionManager, ToastAndroid} from 'react-native';
16import pathConst from '@/constants/pathConst';
17import {compare, satisfies} from 'compare-versions';
18import DeviceInfo from 'react-native-device-info';
19import StateMapper from '@/utils/stateMapper';
20import MediaExtra from './mediaExtra';
21import {nanoid} from 'nanoid';
22import {devLog, errorLog, trace} from '../utils/log';
23import {
24    getInternalData,
25    InternalDataType,
26    isSameMediaItem,
27    resetMediaItem,
28} from '@/utils/mediaItem';
29import {
30    CacheControl,
31    emptyFunction,
32    internalSerializeKey,
33    localPluginHash,
34    localPluginPlatform,
35} from '@/constants/commonConst';
36import delay from '@/utils/delay';
37import * as cheerio from 'cheerio';
38import CookieManager from '@react-native-cookies/cookies';
39import he from 'he';
40import Network from './network';
41import LocalMusicSheet from './localMusicSheet';
42import {getInfoAsync} from 'expo-file-system';
43import Mp3Util from '@/native/mp3Util';
44import {PluginMeta} from './pluginMeta';
45import {useEffect, useState} from 'react';
46import {addFileScheme, getFileName} from '@/utils/fileUtils';
47import {URL} from 'react-native-url-polyfill';
48import Base64 from '@/utils/base64';
49import MediaCache from './mediaCache';
50import {produce} from 'immer';
51import objectPath from 'object-path';
52
53axios.defaults.timeout = 2000;
54
55const sha256 = CryptoJs.SHA256;
56
57export enum PluginStateCode {
58    /** 版本不匹配 */
59    VersionNotMatch = 'VERSION NOT MATCH',
60    /** 无法解析 */
61    CannotParse = 'CANNOT PARSE',
62}
63
64const packages: Record<string, any> = {
65    cheerio,
66    'crypto-js': CryptoJs,
67    axios,
68    dayjs,
69    'big-integer': bigInt,
70    qs,
71    he,
72    '@react-native-cookies/cookies': CookieManager,
73    webdav,
74};
75
76const _require = (packageName: string) => {
77    let pkg = packages[packageName];
78    pkg.default = pkg;
79    return pkg;
80};
81
82const _consoleBind = function (
83    method: 'log' | 'error' | 'info' | 'warn',
84    ...args: any
85) {
86    const fn = console[method];
87    if (fn) {
88        fn(...args);
89        devLog(method, ...args);
90    }
91};
92
93const _console = {
94    log: _consoleBind.bind(null, 'log'),
95    warn: _consoleBind.bind(null, 'warn'),
96    info: _consoleBind.bind(null, 'info'),
97    error: _consoleBind.bind(null, 'error'),
98};
99
100function formatAuthUrl(url: string) {
101    const urlObj = new URL(url);
102
103    try {
104        if (urlObj.username && urlObj.password) {
105            const auth = `Basic ${Base64.btoa(
106                `${decodeURIComponent(urlObj.username)}:${decodeURIComponent(
107                    urlObj.password,
108                )}`,
109            )}`;
110            urlObj.username = '';
111            urlObj.password = '';
112
113            return {
114                url: urlObj.toString(),
115                auth,
116            };
117        }
118    } catch (e) {
119        return {
120            url,
121        };
122    }
123    return {
124        url,
125    };
126}
127
128//#region 插件类
129export class Plugin {
130    /** 插件名 */
131    public name: string;
132    /** 插件的hash,作为唯一id */
133    public hash: string;
134    /** 插件状态:激活、关闭、错误 */
135    public state: 'enabled' | 'disabled' | 'error';
136    /** 插件状态信息 */
137    public stateCode?: PluginStateCode;
138    /** 插件的实例 */
139    public instance: IPlugin.IPluginInstance;
140    /** 插件路径 */
141    public path: string;
142    /** 插件方法 */
143    public methods: PluginMethods;
144
145    constructor(
146        funcCode: string | (() => IPlugin.IPluginInstance),
147        pluginPath: string,
148    ) {
149        this.state = 'enabled';
150        let _instance: IPlugin.IPluginInstance;
151        const _module: any = {exports: {}};
152        try {
153            if (typeof funcCode === 'string') {
154                // 插件的环境变量
155                const env = {
156                    getUserVariables: () => {
157                        return (
158                            PluginMeta.getPluginMeta(this)?.userVariables ?? {}
159                        );
160                    },
161                    os: 'android',
162                };
163
164                // eslint-disable-next-line no-new-func
165                _instance = Function(`
166                    'use strict';
167                    return function(require, __musicfree_require, module, exports, console, env, URL) {
168                        ${funcCode}
169                    }
170                `)()(
171                    _require,
172                    _require,
173                    _module,
174                    _module.exports,
175                    _console,
176                    env,
177                    URL,
178                );
179                if (_module.exports.default) {
180                    _instance = _module.exports
181                        .default as IPlugin.IPluginInstance;
182                } else {
183                    _instance = _module.exports as IPlugin.IPluginInstance;
184                }
185            } else {
186                _instance = funcCode();
187            }
188            // 插件初始化后的一些操作
189            if (Array.isArray(_instance.userVariables)) {
190                _instance.userVariables = _instance.userVariables.filter(
191                    it => it?.key,
192                );
193            }
194            this.checkValid(_instance);
195        } catch (e: any) {
196            console.log(e);
197            this.state = 'error';
198            this.stateCode = PluginStateCode.CannotParse;
199            if (e?.stateCode) {
200                this.stateCode = e.stateCode;
201            }
202            errorLog(`${pluginPath}插件无法解析 `, {
203                stateCode: this.stateCode,
204                message: e?.message,
205                stack: e?.stack,
206            });
207            _instance = e?.instance ?? {
208                _path: '',
209                platform: '',
210                appVersion: '',
211                async getMediaSource() {
212                    return null;
213                },
214                async search() {
215                    return {};
216                },
217                async getAlbumInfo() {
218                    return null;
219                },
220            };
221        }
222        this.instance = _instance;
223        this.path = pluginPath;
224        this.name = _instance.platform;
225        if (
226            this.instance.platform === '' ||
227            this.instance.platform === undefined
228        ) {
229            this.hash = '';
230        } else {
231            if (typeof funcCode === 'string') {
232                this.hash = sha256(funcCode).toString();
233            } else {
234                this.hash = sha256(funcCode.toString()).toString();
235            }
236        }
237
238        // 放在最后
239        this.methods = new PluginMethods(this);
240    }
241
242    private checkValid(_instance: IPlugin.IPluginInstance) {
243        /** 版本号校验 */
244        if (
245            _instance.appVersion &&
246            !satisfies(DeviceInfo.getVersion(), _instance.appVersion)
247        ) {
248            throw {
249                instance: _instance,
250                stateCode: PluginStateCode.VersionNotMatch,
251            };
252        }
253        return true;
254    }
255}
256//#endregion
257
258//#region 基于插件类封装的方法,供给APP侧直接调用
259/** 有缓存等信息 */
260class PluginMethods implements IPlugin.IPluginInstanceMethods {
261    private plugin;
262    constructor(plugin: Plugin) {
263        this.plugin = plugin;
264    }
265    /** 搜索 */
266    async search<T extends ICommon.SupportMediaType>(
267        query: string,
268        page: number,
269        type: T,
270    ): Promise<IPlugin.ISearchResult<T>> {
271        if (!this.plugin.instance.search) {
272            return {
273                isEnd: true,
274                data: [],
275            };
276        }
277
278        const result =
279            (await this.plugin.instance.search(query, page, type)) ?? {};
280        if (Array.isArray(result.data)) {
281            result.data.forEach(_ => {
282                resetMediaItem(_, this.plugin.name);
283            });
284            return {
285                isEnd: result.isEnd ?? true,
286                data: result.data,
287            };
288        }
289        return {
290            isEnd: true,
291            data: [],
292        };
293    }
294
295    /** 获取真实源 */
296    async getMediaSource(
297        musicItem: IMusic.IMusicItemBase,
298        quality: IMusic.IQualityKey = 'standard',
299        retryCount = 1,
300        notUpdateCache = false,
301    ): Promise<IPlugin.IMediaSourceResult | null> {
302        // 1. 本地搜索 其实直接读mediameta就好了
303        const mediaExtra = MediaExtra.get(musicItem);
304        const localPath =
305            mediaExtra?.localPath ||
306            getInternalData<string>(musicItem, InternalDataType.LOCALPATH) ||
307            getInternalData<string>(
308                LocalMusicSheet.isLocalMusic(musicItem),
309                InternalDataType.LOCALPATH,
310            );
311        if (localPath && (await getInfoAsync(localPath)).exists) {
312            trace('本地播放', localPath);
313            if (mediaExtra && mediaExtra.localPath !== localPath) {
314                // 修正一下本地数据
315                MediaExtra.update(musicItem, {
316                    localPath,
317                });
318            }
319            return {
320                url: addFileScheme(localPath),
321            };
322        } else if (mediaExtra?.localPath) {
323            MediaExtra.update(musicItem, {
324                localPath: undefined,
325            });
326        }
327
328        if (musicItem.platform === localPluginPlatform) {
329            throw new Error('本地音乐不存在');
330        }
331        // 2. 缓存播放
332        const mediaCache = MediaCache.getMediaCache(
333            musicItem,
334        ) as IMusic.IMusicItem | null;
335        const pluginCacheControl =
336            this.plugin.instance.cacheControl ?? 'no-cache';
337        if (
338            mediaCache &&
339            mediaCache?.source?.[quality]?.url &&
340            (pluginCacheControl === CacheControl.Cache ||
341                (pluginCacheControl === CacheControl.NoCache &&
342                    Network.isOffline()))
343        ) {
344            trace('播放', '缓存播放');
345            const qualityInfo = mediaCache.source[quality];
346            return {
347                url: qualityInfo!.url,
348                headers: mediaCache.headers,
349                userAgent:
350                    mediaCache.userAgent ?? mediaCache.headers?.['user-agent'],
351            };
352        }
353        // 3. 插件解析
354        if (!this.plugin.instance.getMediaSource) {
355            const {url, auth} = formatAuthUrl(
356                musicItem?.qualities?.[quality]?.url ?? musicItem.url,
357            );
358            return {
359                url: url,
360                headers: auth
361                    ? {
362                          Authorization: auth,
363                      }
364                    : undefined,
365            };
366        }
367        try {
368            const {url, headers} = (await this.plugin.instance.getMediaSource(
369                musicItem,
370                quality,
371            )) ?? {url: musicItem?.qualities?.[quality]?.url};
372            if (!url) {
373                throw new Error('NOT RETRY');
374            }
375            trace('播放', '插件播放');
376            const result = {
377                url,
378                headers,
379                userAgent: headers?.['user-agent'],
380            } as IPlugin.IMediaSourceResult;
381            const authFormattedResult = formatAuthUrl(result.url!);
382            if (authFormattedResult.auth) {
383                result.url = authFormattedResult.url;
384                result.headers = {
385                    ...(result.headers ?? {}),
386                    Authorization: authFormattedResult.auth,
387                };
388            }
389
390            if (
391                pluginCacheControl !== CacheControl.NoStore &&
392                !notUpdateCache
393            ) {
394                // 更新缓存
395                const cacheSource = {
396                    headers: result.headers,
397                    userAgent: result.userAgent,
398                    url,
399                };
400                let realMusicItem = {
401                    ...musicItem,
402                    ...(mediaCache || {}),
403                };
404                realMusicItem.source = {
405                    ...(realMusicItem.source || {}),
406                    [quality]: cacheSource,
407                };
408
409                MediaCache.setMediaCache(realMusicItem);
410            }
411            return result;
412        } catch (e: any) {
413            if (retryCount > 0 && e?.message !== 'NOT RETRY') {
414                await delay(150);
415                return this.getMediaSource(musicItem, quality, --retryCount);
416            }
417            errorLog('获取真实源失败', e?.message);
418            devLog('error', '获取真实源失败', e, e?.message);
419            return null;
420        }
421    }
422
423    /** 获取音乐详情 */
424    async getMusicInfo(
425        musicItem: ICommon.IMediaBase,
426    ): Promise<Partial<IMusic.IMusicItem> | null> {
427        if (!this.plugin.instance.getMusicInfo) {
428            return null;
429        }
430        try {
431            return (
432                this.plugin.instance.getMusicInfo(
433                    resetMediaItem(musicItem, undefined, true),
434                ) ?? null
435            );
436        } catch (e: any) {
437            devLog('error', '获取音乐详情失败', e, e?.message);
438            return null;
439        }
440    }
441
442    /**
443     *
444     * getLyric(musicItem) => {
445     *      lyric: string;
446     *      trans: string;
447     * }
448     *
449     */
450    /** 获取歌词 */
451    async getLyric(
452        originalMusicItem: IMusic.IMusicItemBase,
453    ): Promise<ILyric.ILyricSource | null> {
454        // 1.额外存储的meta信息(关联歌词)
455        const meta = MediaExtra.get(originalMusicItem);
456        let musicItem: IMusic.IMusicItem;
457        if (meta && meta.associatedLrc) {
458            musicItem = meta.associatedLrc as IMusic.IMusicItem;
459        } else {
460            musicItem = originalMusicItem as IMusic.IMusicItem;
461        }
462
463        const musicItemCache = MediaCache.getMediaCache(
464            musicItem,
465        ) as IMusic.IMusicItemCache | null;
466
467        /** 原始歌词文本 */
468        let rawLrc: string | null = musicItem.rawLrc || null;
469        let translation: string | null = null;
470
471        // 2. 本地手动设置的歌词
472        const platformHash = CryptoJs.MD5(musicItem.platform).toString(
473            CryptoJs.enc.Hex,
474        );
475        const idHash = CryptoJs.MD5(musicItem.id).toString(CryptoJs.enc.Hex);
476        if (
477            await RNFS.exists(
478                pathConst.localLrcPath + platformHash + '/' + idHash + '.lrc',
479            )
480        ) {
481            rawLrc = await RNFS.readFile(
482                pathConst.localLrcPath + platformHash + '/' + idHash + '.lrc',
483                'utf8',
484            );
485
486            if (
487                await RNFS.exists(
488                    pathConst.localLrcPath +
489                        platformHash +
490                        '/' +
491                        idHash +
492                        '.tran.lrc',
493                )
494            ) {
495                translation =
496                    (await RNFS.readFile(
497                        pathConst.localLrcPath +
498                            platformHash +
499                            '/' +
500                            idHash +
501                            '.tran.lrc',
502                        'utf8',
503                    )) || null;
504            }
505
506            return {
507                rawLrc,
508                translation: translation || undefined, // TODO: 这里写的不好
509            };
510        }
511
512        // 2. 缓存歌词 / 对象上本身的歌词
513        if (musicItemCache?.lyric) {
514            // 缓存的远程结果
515            let cacheLyric: ILyric.ILyricSource | null =
516                musicItemCache.lyric || null;
517            // 缓存的本地结果
518            let localLyric: ILyric.ILyricSource | null =
519                musicItemCache.$localLyric || null;
520
521            // 优先用缓存的结果
522            if (cacheLyric.rawLrc || cacheLyric.translation) {
523                return {
524                    rawLrc: cacheLyric.rawLrc,
525                    translation: cacheLyric.translation,
526                };
527            }
528
529            // 本地其实是缓存的路径
530            if (localLyric) {
531                let needRefetch = false;
532                if (localLyric.rawLrc && (await exists(localLyric.rawLrc))) {
533                    rawLrc = await readFile(localLyric.rawLrc, 'utf8');
534                } else if (localLyric.rawLrc) {
535                    needRefetch = true;
536                }
537                if (
538                    localLyric.translation &&
539                    (await exists(localLyric.translation))
540                ) {
541                    translation = await readFile(
542                        localLyric.translation,
543                        'utf8',
544                    );
545                } else if (localLyric.translation) {
546                    needRefetch = true;
547                }
548
549                if (!needRefetch && (rawLrc || translation)) {
550                    return {
551                        rawLrc: rawLrc || undefined,
552                        translation: translation || undefined,
553                    };
554                }
555            }
556        }
557
558        // 3. 无缓存歌词/无自带歌词/无本地歌词
559        let lrcSource: ILyric.ILyricSource | null;
560        if (isSameMediaItem(originalMusicItem, musicItem)) {
561            lrcSource =
562                (await this.plugin.instance
563                    ?.getLyric?.(resetMediaItem(musicItem, undefined, true))
564                    ?.catch(() => null)) || null;
565        } else {
566            lrcSource =
567                (await PluginManager.getByMedia(musicItem)
568                    ?.instance?.getLyric?.(
569                        resetMediaItem(musicItem, undefined, true),
570                    )
571                    ?.catch(() => null)) || null;
572        }
573
574        if (lrcSource) {
575            rawLrc = lrcSource?.rawLrc || rawLrc;
576            translation = lrcSource?.translation || null;
577
578            const deprecatedLrcUrl = lrcSource?.lrc || musicItem.lrc;
579
580            // 本地的文件名
581            let filename: string | undefined = `${
582                pathConst.lrcCachePath
583            }${nanoid()}.lrc`;
584            let filenameTrans: string | undefined = `${
585                pathConst.lrcCachePath
586            }${nanoid()}.lrc`;
587
588            // 旧版本兼容
589            if (!(rawLrc || translation)) {
590                if (deprecatedLrcUrl) {
591                    rawLrc = (
592                        await axios
593                            .get(deprecatedLrcUrl, {timeout: 3000})
594                            .catch(() => null)
595                    )?.data;
596                } else if (musicItem.rawLrc) {
597                    rawLrc = musicItem.rawLrc;
598                }
599            }
600
601            if (rawLrc) {
602                await writeFile(filename, rawLrc, 'utf8');
603            } else {
604                filename = undefined;
605            }
606            if (translation) {
607                await writeFile(filenameTrans, translation, 'utf8');
608            } else {
609                filenameTrans = undefined;
610            }
611
612            if (rawLrc || translation) {
613                MediaCache.setMediaCache(
614                    produce(musicItemCache || musicItem, draft => {
615                        musicItemCache?.$localLyric?.rawLrc;
616                        objectPath.set(draft, '$localLyric.rawLrc', filename);
617                        objectPath.set(
618                            draft,
619                            '$localLyric.translation',
620                            filenameTrans,
621                        );
622                        return draft;
623                    }),
624                );
625                return {
626                    rawLrc: rawLrc || undefined,
627                    translation: translation || undefined,
628                };
629            }
630        }
631
632        // 6. 如果是本地文件
633        const isDownloaded = LocalMusicSheet.isLocalMusic(originalMusicItem);
634        if (
635            originalMusicItem.platform !== localPluginPlatform &&
636            isDownloaded
637        ) {
638            const res = await localFilePlugin.instance!.getLyric!(isDownloaded);
639
640            console.log('本地文件歌词');
641
642            if (res) {
643                return res;
644            }
645        }
646        devLog('warn', '无歌词');
647
648        return null;
649    }
650
651    /** 获取歌词文本 */
652    async getLyricText(
653        musicItem: IMusic.IMusicItem,
654    ): Promise<string | undefined> {
655        return (await this.getLyric(musicItem))?.rawLrc;
656    }
657
658    /** 获取专辑信息 */
659    async getAlbumInfo(
660        albumItem: IAlbum.IAlbumItemBase,
661        page: number = 1,
662    ): Promise<IPlugin.IAlbumInfoResult | null> {
663        if (!this.plugin.instance.getAlbumInfo) {
664            return {
665                albumItem,
666                musicList: (albumItem?.musicList ?? []).map(
667                    resetMediaItem,
668                    this.plugin.name,
669                    true,
670                ),
671                isEnd: true,
672            };
673        }
674        try {
675            const result = await this.plugin.instance.getAlbumInfo(
676                resetMediaItem(albumItem, undefined, true),
677                page,
678            );
679            if (!result) {
680                throw new Error();
681            }
682            result?.musicList?.forEach(_ => {
683                resetMediaItem(_, this.plugin.name);
684                _.album = albumItem.title;
685            });
686
687            if (page <= 1) {
688                // 合并信息
689                return {
690                    albumItem: {...albumItem, ...(result?.albumItem ?? {})},
691                    isEnd: result.isEnd === false ? false : true,
692                    musicList: result.musicList,
693                };
694            } else {
695                return {
696                    isEnd: result.isEnd === false ? false : true,
697                    musicList: result.musicList,
698                };
699            }
700        } catch (e: any) {
701            trace('获取专辑信息失败', e?.message);
702            devLog('error', '获取专辑信息失败', e, e?.message);
703
704            return null;
705        }
706    }
707
708    /** 获取歌单信息 */
709    async getMusicSheetInfo(
710        sheetItem: IMusic.IMusicSheetItem,
711        page: number = 1,
712    ): Promise<IPlugin.ISheetInfoResult | null> {
713        if (!this.plugin.instance.getMusicSheetInfo) {
714            return {
715                sheetItem,
716                musicList: sheetItem?.musicList ?? [],
717                isEnd: true,
718            };
719        }
720        try {
721            const result = await this.plugin.instance?.getMusicSheetInfo?.(
722                resetMediaItem(sheetItem, undefined, true),
723                page,
724            );
725            if (!result) {
726                throw new Error();
727            }
728            result?.musicList?.forEach(_ => {
729                resetMediaItem(_, this.plugin.name);
730            });
731
732            if (page <= 1) {
733                // 合并信息
734                return {
735                    sheetItem: {...sheetItem, ...(result?.sheetItem ?? {})},
736                    isEnd: result.isEnd === false ? false : true,
737                    musicList: result.musicList,
738                };
739            } else {
740                return {
741                    isEnd: result.isEnd === false ? false : true,
742                    musicList: result.musicList,
743                };
744            }
745        } catch (e: any) {
746            trace('获取歌单信息失败', e, e?.message);
747            devLog('error', '获取歌单信息失败', e, e?.message);
748
749            return null;
750        }
751    }
752
753    /** 查询作者信息 */
754    async getArtistWorks<T extends IArtist.ArtistMediaType>(
755        artistItem: IArtist.IArtistItem,
756        page: number,
757        type: T,
758    ): Promise<IPlugin.ISearchResult<T>> {
759        if (!this.plugin.instance.getArtistWorks) {
760            return {
761                isEnd: true,
762                data: [],
763            };
764        }
765        try {
766            const result = await this.plugin.instance.getArtistWorks(
767                artistItem,
768                page,
769                type,
770            );
771            if (!result.data) {
772                return {
773                    isEnd: true,
774                    data: [],
775                };
776            }
777            result.data?.forEach(_ => resetMediaItem(_, this.plugin.name));
778            return {
779                isEnd: result.isEnd ?? true,
780                data: result.data,
781            };
782        } catch (e: any) {
783            trace('查询作者信息失败', e?.message);
784            devLog('error', '查询作者信息失败', e, e?.message);
785
786            throw e;
787        }
788    }
789
790    /** 导入歌单 */
791    async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> {
792        try {
793            const result =
794                (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? [];
795            result.forEach(_ => resetMediaItem(_, this.plugin.name));
796            return result;
797        } catch (e: any) {
798            console.log(e);
799            devLog('error', '导入歌单失败', e, e?.message);
800
801            return [];
802        }
803    }
804    /** 导入单曲 */
805    async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> {
806        try {
807            const result = await this.plugin.instance?.importMusicItem?.(
808                urlLike,
809            );
810            if (!result) {
811                throw new Error();
812            }
813            resetMediaItem(result, this.plugin.name);
814            return result;
815        } catch (e: any) {
816            devLog('error', '导入单曲失败', e, e?.message);
817
818            return null;
819        }
820    }
821    /** 获取榜单 */
822    async getTopLists(): Promise<IMusic.IMusicSheetGroupItem[]> {
823        try {
824            const result = await this.plugin.instance?.getTopLists?.();
825            if (!result) {
826                throw new Error();
827            }
828            return result;
829        } catch (e: any) {
830            devLog('error', '获取榜单失败', e, e?.message);
831            return [];
832        }
833    }
834    /** 获取榜单详情 */
835    async getTopListDetail(
836        topListItem: IMusic.IMusicSheetItemBase,
837        page: number,
838    ): Promise<IPlugin.ITopListInfoResult> {
839        try {
840            const result = await this.plugin.instance?.getTopListDetail?.(
841                topListItem,
842                page,
843            );
844            if (!result) {
845                throw new Error();
846            }
847            if (result.musicList) {
848                result.musicList.forEach(_ =>
849                    resetMediaItem(_, this.plugin.name),
850                );
851            }
852            if (result.isEnd !== false) {
853                result.isEnd = true;
854            }
855            return result;
856        } catch (e: any) {
857            devLog('error', '获取榜单详情失败', e, e?.message);
858            return {
859                isEnd: true,
860                topListItem: topListItem as IMusic.IMusicSheetItem,
861                musicList: [],
862            };
863        }
864    }
865
866    /** 获取推荐歌单的tag */
867    async getRecommendSheetTags(): Promise<IPlugin.IGetRecommendSheetTagsResult> {
868        try {
869            const result =
870                await this.plugin.instance?.getRecommendSheetTags?.();
871            if (!result) {
872                throw new Error();
873            }
874            return result;
875        } catch (e: any) {
876            devLog('error', '获取推荐歌单失败', e, e?.message);
877            return {
878                data: [],
879            };
880        }
881    }
882    /** 获取某个tag的推荐歌单 */
883    async getRecommendSheetsByTag(
884        tagItem: ICommon.IUnique,
885        page?: number,
886    ): Promise<ICommon.PaginationResponse<IMusic.IMusicSheetItemBase>> {
887        try {
888            const result =
889                await this.plugin.instance?.getRecommendSheetsByTag?.(
890                    tagItem,
891                    page ?? 1,
892                );
893            if (!result) {
894                throw new Error();
895            }
896            if (result.isEnd !== false) {
897                result.isEnd = true;
898            }
899            if (!result.data) {
900                result.data = [];
901            }
902            result.data.forEach(item => resetMediaItem(item, this.plugin.name));
903
904            return result;
905        } catch (e: any) {
906            devLog('error', '获取推荐歌单详情失败', e, e?.message);
907            return {
908                isEnd: true,
909                data: [],
910            };
911        }
912    }
913
914    async getMusicComments(
915        musicItem: IMusic.IMusicItem,
916    ): Promise<ICommon.PaginationResponse<IMedia.IComment>> {
917        const result = await this.plugin.instance?.getMusicComments?.(
918            musicItem,
919        );
920        if (!result) {
921            throw new Error();
922        }
923        if (result.isEnd !== false) {
924            result.isEnd = true;
925        }
926        if (!result.data) {
927            result.data = [];
928        }
929
930        return result;
931    }
932}
933//#endregion
934
935let plugins: Array<Plugin> = [];
936const pluginStateMapper = new StateMapper(() => plugins);
937
938//#region 本地音乐插件
939/** 本地插件 */
940const localFilePlugin = new Plugin(function () {
941    return {
942        platform: localPluginPlatform,
943        _path: '',
944        async getMusicInfo(musicBase) {
945            const localPath = getInternalData<string>(
946                musicBase,
947                InternalDataType.LOCALPATH,
948            );
949            if (localPath) {
950                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
951                return {
952                    artwork: coverImg,
953                };
954            }
955            return null;
956        },
957        async getLyric(musicBase) {
958            const localPath = getInternalData<string>(
959                musicBase,
960                InternalDataType.LOCALPATH,
961            );
962            let rawLrc: string | null = null;
963            if (localPath) {
964                // 读取内嵌歌词
965                try {
966                    rawLrc = await Mp3Util.getLyric(localPath);
967                } catch (e) {
968                    console.log('读取内嵌歌词失败', e);
969                }
970                if (!rawLrc) {
971                    // 读取配置歌词
972                    const lastDot = localPath.lastIndexOf('.');
973                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
974
975                    try {
976                        if (await exists(lrcPath)) {
977                            rawLrc = await readFile(lrcPath, 'utf8');
978                        }
979                    } catch {}
980                }
981            }
982
983            return rawLrc
984                ? {
985                      rawLrc,
986                  }
987                : null;
988        },
989        async importMusicItem(urlLike) {
990            let meta: any = {};
991            try {
992                meta = await Mp3Util.getBasicMeta(urlLike);
993            } catch {}
994            const stat = await getInfoAsync(urlLike, {
995                md5: true,
996            });
997            let id: string;
998            if (stat.exists) {
999                id = stat.md5 || nanoid();
1000            } else {
1001                id = nanoid();
1002            }
1003            return {
1004                id: id,
1005                platform: '本地',
1006                title: meta?.title ?? getFileName(urlLike),
1007                artist: meta?.artist ?? '未知歌手',
1008                duration: parseInt(meta?.duration ?? '0', 10) / 1000,
1009                album: meta?.album ?? '未知专辑',
1010                artwork: '',
1011                [internalSerializeKey]: {
1012                    localPath: urlLike,
1013                },
1014            };
1015        },
1016        async getMediaSource(musicItem, quality) {
1017            if (quality === 'standard') {
1018                return {
1019                    url: addFileScheme(musicItem.$?.localPath || musicItem.url),
1020                };
1021            }
1022            return null;
1023        },
1024    };
1025}, '');
1026localFilePlugin.hash = localPluginHash;
1027
1028//#endregion
1029
1030async function setup() {
1031    const _plugins: Array<Plugin> = [];
1032    try {
1033        // 加载插件
1034        const pluginsPaths = await readDir(pathConst.pluginPath);
1035        for (let i = 0; i < pluginsPaths.length; ++i) {
1036            const _pluginUrl = pluginsPaths[i];
1037            trace('初始化插件', _pluginUrl);
1038            if (
1039                _pluginUrl.isFile() &&
1040                (_pluginUrl.name?.endsWith?.('.js') ||
1041                    _pluginUrl.path?.endsWith?.('.js'))
1042            ) {
1043                const funcCode = await readFile(_pluginUrl.path, 'utf8');
1044                const plugin = new Plugin(funcCode, _pluginUrl.path);
1045                const _pluginIndex = _plugins.findIndex(
1046                    p => p.hash === plugin.hash,
1047                );
1048                if (_pluginIndex !== -1) {
1049                    // 重复插件,直接忽略
1050                    continue;
1051                }
1052                plugin.hash !== '' && _plugins.push(plugin);
1053            }
1054        }
1055
1056        plugins = _plugins;
1057        /** 初始化meta信息 */
1058        await PluginMeta.setupMeta(plugins.map(_ => _.name));
1059        /** 查看一下是否有禁用的标记 */
1060        const allMeta = PluginMeta.getPluginMetaAll() ?? {};
1061        for (let plugin of plugins) {
1062            if (allMeta[plugin.name]?.enabled === false) {
1063                plugin.state = 'disabled';
1064            }
1065        }
1066        pluginStateMapper.notify();
1067    } catch (e: any) {
1068        ToastAndroid.show(
1069            `插件初始化失败:${e?.message ?? e}`,
1070            ToastAndroid.LONG,
1071        );
1072        errorLog('插件初始化失败', e?.message);
1073        throw e;
1074    }
1075}
1076
1077interface IInstallPluginConfig {
1078    notCheckVersion?: boolean;
1079}
1080
1081async function installPluginFromRawCode(
1082    funcCode: string,
1083    config?: IInstallPluginConfig,
1084) {
1085    if (funcCode) {
1086        const plugin = new Plugin(funcCode, '');
1087        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1088        if (_pluginIndex !== -1) {
1089            // 静默忽略
1090            return plugin;
1091        }
1092        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1093        if (oldVersionPlugin && !config?.notCheckVersion) {
1094            if (
1095                compare(
1096                    oldVersionPlugin.instance.version ?? '',
1097                    plugin.instance.version ?? '',
1098                    '>',
1099                )
1100            ) {
1101                throw new Error('已安装更新版本的插件');
1102            }
1103        }
1104
1105        if (plugin.hash !== '') {
1106            const fn = nanoid();
1107            if (oldVersionPlugin) {
1108                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
1109                try {
1110                    await unlink(oldVersionPlugin.path);
1111                } catch {}
1112            }
1113            const pluginPath = `${pathConst.pluginPath}${fn}.js`;
1114            await writeFile(pluginPath, funcCode, 'utf8');
1115            plugin.path = pluginPath;
1116            plugins = plugins.concat(plugin);
1117            pluginStateMapper.notify();
1118            return plugin;
1119        }
1120        throw new Error('插件无法解析!');
1121    }
1122}
1123
1124// 安装插件
1125async function installPlugin(
1126    pluginPath: string,
1127    config?: IInstallPluginConfig,
1128) {
1129    // if (pluginPath.endsWith('.js')) {
1130    const funcCode = await readFile(pluginPath, 'utf8');
1131
1132    if (funcCode) {
1133        const plugin = new Plugin(funcCode, pluginPath);
1134        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1135        if (_pluginIndex !== -1) {
1136            // 静默忽略
1137            return plugin;
1138        }
1139        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1140        if (oldVersionPlugin && !config?.notCheckVersion) {
1141            if (
1142                compare(
1143                    oldVersionPlugin.instance.version ?? '',
1144                    plugin.instance.version ?? '',
1145                    '>',
1146                )
1147            ) {
1148                throw new Error('已安装更新版本的插件');
1149            }
1150        }
1151
1152        if (plugin.hash !== '') {
1153            const fn = nanoid();
1154            if (oldVersionPlugin) {
1155                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
1156                try {
1157                    await unlink(oldVersionPlugin.path);
1158                } catch {}
1159            }
1160            const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1161            await copyFile(pluginPath, _pluginPath);
1162            plugin.path = _pluginPath;
1163            plugins = plugins.concat(plugin);
1164            pluginStateMapper.notify();
1165            return plugin;
1166        }
1167        throw new Error('插件无法解析!');
1168    }
1169    throw new Error('插件无法识别!');
1170}
1171
1172const reqHeaders = {
1173    'Cache-Control': 'no-cache',
1174    Pragma: 'no-cache',
1175    Expires: '0',
1176};
1177
1178async function installPluginFromUrl(
1179    url: string,
1180    config?: IInstallPluginConfig,
1181) {
1182    try {
1183        const funcCode = (
1184            await axios.get(url, {
1185                headers: reqHeaders,
1186            })
1187        ).data;
1188        if (funcCode) {
1189            const plugin = new Plugin(funcCode, '');
1190            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1191            if (_pluginIndex !== -1) {
1192                // 静默忽略
1193                return;
1194            }
1195            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1196            if (oldVersionPlugin && !config?.notCheckVersion) {
1197                if (
1198                    compare(
1199                        oldVersionPlugin.instance.version ?? '',
1200                        plugin.instance.version ?? '',
1201                        '>',
1202                    )
1203                ) {
1204                    throw new Error('已安装更新版本的插件');
1205                }
1206            }
1207
1208            if (plugin.hash !== '') {
1209                const fn = nanoid();
1210                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1211                await writeFile(_pluginPath, funcCode, 'utf8');
1212                plugin.path = _pluginPath;
1213                plugins = plugins.concat(plugin);
1214                if (oldVersionPlugin) {
1215                    plugins = plugins.filter(
1216                        _ => _.hash !== oldVersionPlugin.hash,
1217                    );
1218                    try {
1219                        await unlink(oldVersionPlugin.path);
1220                    } catch {}
1221                }
1222                pluginStateMapper.notify();
1223                return;
1224            }
1225            throw new Error('插件无法解析!');
1226        }
1227    } catch (e: any) {
1228        devLog('error', 'URL安装插件失败', e, e?.message);
1229        errorLog('URL安装插件失败', e);
1230        throw new Error(e?.message ?? '');
1231    }
1232}
1233
1234/** 卸载插件 */
1235async function uninstallPlugin(hash: string) {
1236    const targetIndex = plugins.findIndex(_ => _.hash === hash);
1237    if (targetIndex !== -1) {
1238        try {
1239            const pluginName = plugins[targetIndex].name;
1240            await unlink(plugins[targetIndex].path);
1241            plugins = plugins.filter(_ => _.hash !== hash);
1242            pluginStateMapper.notify();
1243            // 防止其他重名
1244            if (plugins.every(_ => _.name !== pluginName)) {
1245                MediaExtra.removeAll(pluginName);
1246            }
1247        } catch {}
1248    }
1249}
1250
1251async function uninstallAllPlugins() {
1252    await Promise.all(
1253        plugins.map(async plugin => {
1254            try {
1255                const pluginName = plugin.name;
1256                await unlink(plugin.path);
1257                MediaExtra.removeAll(pluginName);
1258            } catch (e) {}
1259        }),
1260    );
1261    plugins = [];
1262    pluginStateMapper.notify();
1263
1264    /** 清除空余文件,异步做就可以了 */
1265    readDir(pathConst.pluginPath)
1266        .then(fns => {
1267            fns.forEach(fn => {
1268                unlink(fn.path).catch(emptyFunction);
1269            });
1270        })
1271        .catch(emptyFunction);
1272}
1273
1274async function updatePlugin(plugin: Plugin) {
1275    const updateUrl = plugin.instance.srcUrl;
1276    if (!updateUrl) {
1277        throw new Error('没有更新源');
1278    }
1279    try {
1280        await installPluginFromUrl(updateUrl);
1281    } catch (e: any) {
1282        if (e.message === '插件已安装') {
1283            throw new Error('当前已是最新版本');
1284        } else {
1285            throw e;
1286        }
1287    }
1288}
1289
1290function getByMedia(mediaItem: ICommon.IMediaBase) {
1291    return getByName(mediaItem?.platform);
1292}
1293
1294function getByHash(hash: string) {
1295    return hash === localPluginHash
1296        ? localFilePlugin
1297        : plugins.find(_ => _.hash === hash);
1298}
1299
1300function getByName(name: string) {
1301    return name === localPluginPlatform
1302        ? localFilePlugin
1303        : plugins.find(_ => _.name === name);
1304}
1305
1306function getValidPlugins() {
1307    return plugins.filter(_ => _.state === 'enabled');
1308}
1309
1310function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) {
1311    return plugins.filter(
1312        _ =>
1313            _.state === 'enabled' &&
1314            _.instance.search &&
1315            (supportedSearchType && _.instance.supportedSearchType
1316                ? _.instance.supportedSearchType.includes(supportedSearchType)
1317                : true),
1318    );
1319}
1320
1321function getSortedSearchablePlugins(
1322    supportedSearchType?: ICommon.SupportMediaType,
1323) {
1324    return getSearchablePlugins(supportedSearchType).sort((a, b) =>
1325        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1326            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1327        0
1328            ? -1
1329            : 1,
1330    );
1331}
1332
1333function getTopListsablePlugins() {
1334    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
1335}
1336
1337function getSortedTopListsablePlugins() {
1338    return getTopListsablePlugins().sort((a, b) =>
1339        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1340            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1341        0
1342            ? -1
1343            : 1,
1344    );
1345}
1346
1347function getRecommendSheetablePlugins() {
1348    return plugins.filter(
1349        _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag,
1350    );
1351}
1352
1353function getSortedRecommendSheetablePlugins() {
1354    return getRecommendSheetablePlugins().sort((a, b) =>
1355        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1356            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1357        0
1358            ? -1
1359            : 1,
1360    );
1361}
1362
1363function useSortedPlugins() {
1364    const _plugins = pluginStateMapper.useMappedState();
1365    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
1366
1367    const [sortedPlugins, setSortedPlugins] = useState(
1368        [..._plugins].sort((a, b) =>
1369            (_pluginMetaAll[a.name]?.order ?? Infinity) -
1370                (_pluginMetaAll[b.name]?.order ?? Infinity) <
1371            0
1372                ? -1
1373                : 1,
1374        ),
1375    );
1376
1377    useEffect(() => {
1378        InteractionManager.runAfterInteractions(() => {
1379            setSortedPlugins(
1380                [..._plugins].sort((a, b) =>
1381                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
1382                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
1383                    0
1384                        ? -1
1385                        : 1,
1386                ),
1387            );
1388        });
1389    }, [_plugins, _pluginMetaAll]);
1390
1391    return sortedPlugins;
1392}
1393
1394async function setPluginEnabled(plugin: Plugin, enabled?: boolean) {
1395    const target = plugins.find(it => it.hash === plugin.hash);
1396    if (target) {
1397        target.state = enabled ? 'enabled' : 'disabled';
1398        plugins = [...plugins];
1399        pluginStateMapper.notify();
1400        PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled);
1401    }
1402}
1403
1404const PluginManager = {
1405    setup,
1406    installPlugin,
1407    installPluginFromRawCode,
1408    installPluginFromUrl,
1409    updatePlugin,
1410    uninstallPlugin,
1411    getByMedia,
1412    getByHash,
1413    getByName,
1414    getValidPlugins,
1415    getSearchablePlugins,
1416    getSortedSearchablePlugins,
1417    getTopListsablePlugins,
1418    getSortedRecommendSheetablePlugins,
1419    getSortedTopListsablePlugins,
1420    usePlugins: pluginStateMapper.useMappedState,
1421    useSortedPlugins,
1422    uninstallAllPlugins,
1423    setPluginEnabled,
1424};
1425
1426export default PluginManager;
1427