xref: /MusicFree/src/core/pluginManager.ts (revision 7fb901109d23a379901d3a07c4fcb24021621c5d)
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    async migrateFromOtherPlugin(
934        mediaItem: ICommon.IMediaBase,
935        fromPlatform: string,
936    ): Promise<{isOk: boolean; data?: ICommon.IMediaBase}> {
937        try {
938            const result = await this.plugin.instance?.migrateFromOtherPlugin(
939                mediaItem,
940                fromPlatform,
941            );
942
943            if (
944                result.isOk &&
945                result.data?.id &&
946                result.data?.platform === this.plugin.platform
947            ) {
948                return {
949                    isOk: result.isOk,
950                    data: result.data,
951                };
952            }
953            return {
954                isOk: false,
955            };
956        } catch {
957            return {
958                isOk: false,
959            };
960        }
961    }
962}
963//#endregion
964
965let plugins: Array<Plugin> = [];
966const pluginStateMapper = new StateMapper(() => plugins);
967
968//#region 本地音乐插件
969/** 本地插件 */
970const localFilePlugin = new Plugin(function () {
971    return {
972        platform: localPluginPlatform,
973        _path: '',
974        async getMusicInfo(musicBase) {
975            const localPath = getInternalData<string>(
976                musicBase,
977                InternalDataType.LOCALPATH,
978            );
979            if (localPath) {
980                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
981                return {
982                    artwork: coverImg,
983                };
984            }
985            return null;
986        },
987        async getLyric(musicBase) {
988            const localPath = getInternalData<string>(
989                musicBase,
990                InternalDataType.LOCALPATH,
991            );
992            let rawLrc: string | null = null;
993            if (localPath) {
994                // 读取内嵌歌词
995                try {
996                    rawLrc = await Mp3Util.getLyric(localPath);
997                } catch (e) {
998                    console.log('读取内嵌歌词失败', e);
999                }
1000                if (!rawLrc) {
1001                    // 读取配置歌词
1002                    const lastDot = localPath.lastIndexOf('.');
1003                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
1004
1005                    try {
1006                        if (await exists(lrcPath)) {
1007                            rawLrc = await readFile(lrcPath, 'utf8');
1008                        }
1009                    } catch {}
1010                }
1011            }
1012
1013            return rawLrc
1014                ? {
1015                      rawLrc,
1016                  }
1017                : null;
1018        },
1019        async importMusicItem(urlLike) {
1020            let meta: any = {};
1021            try {
1022                meta = await Mp3Util.getBasicMeta(urlLike);
1023            } catch {}
1024            const stat = await getInfoAsync(urlLike, {
1025                md5: true,
1026            });
1027            let id: string;
1028            if (stat.exists) {
1029                id = stat.md5 || nanoid();
1030            } else {
1031                id = nanoid();
1032            }
1033            return {
1034                id: id,
1035                platform: '本地',
1036                title: meta?.title ?? getFileName(urlLike),
1037                artist: meta?.artist ?? '未知歌手',
1038                duration: parseInt(meta?.duration ?? '0', 10) / 1000,
1039                album: meta?.album ?? '未知专辑',
1040                artwork: '',
1041                [internalSerializeKey]: {
1042                    localPath: urlLike,
1043                },
1044            };
1045        },
1046        async getMediaSource(musicItem, quality) {
1047            if (quality === 'standard') {
1048                return {
1049                    url: addFileScheme(musicItem.$?.localPath || musicItem.url),
1050                };
1051            }
1052            return null;
1053        },
1054    };
1055}, '');
1056localFilePlugin.hash = localPluginHash;
1057
1058//#endregion
1059
1060async function setup() {
1061    const _plugins: Array<Plugin> = [];
1062    try {
1063        // 加载插件
1064        const pluginsPaths = await readDir(pathConst.pluginPath);
1065        for (let i = 0; i < pluginsPaths.length; ++i) {
1066            const _pluginUrl = pluginsPaths[i];
1067            trace('初始化插件', _pluginUrl);
1068            if (
1069                _pluginUrl.isFile() &&
1070                (_pluginUrl.name?.endsWith?.('.js') ||
1071                    _pluginUrl.path?.endsWith?.('.js'))
1072            ) {
1073                const funcCode = await readFile(_pluginUrl.path, 'utf8');
1074                const plugin = new Plugin(funcCode, _pluginUrl.path);
1075                const _pluginIndex = _plugins.findIndex(
1076                    p => p.hash === plugin.hash,
1077                );
1078                if (_pluginIndex !== -1) {
1079                    // 重复插件,直接忽略
1080                    continue;
1081                }
1082                plugin.hash !== '' && _plugins.push(plugin);
1083            }
1084        }
1085
1086        plugins = _plugins;
1087        /** 初始化meta信息 */
1088        await PluginMeta.setupMeta(plugins.map(_ => _.name));
1089        /** 查看一下是否有禁用的标记 */
1090        const allMeta = PluginMeta.getPluginMetaAll() ?? {};
1091        for (let plugin of plugins) {
1092            if (allMeta[plugin.name]?.enabled === false) {
1093                plugin.state = 'disabled';
1094            }
1095        }
1096        pluginStateMapper.notify();
1097    } catch (e: any) {
1098        ToastAndroid.show(
1099            `插件初始化失败:${e?.message ?? e}`,
1100            ToastAndroid.LONG,
1101        );
1102        errorLog('插件初始化失败', e?.message);
1103        throw e;
1104    }
1105}
1106
1107interface IInstallPluginConfig {
1108    notCheckVersion?: boolean;
1109}
1110
1111async function installPluginFromRawCode(
1112    funcCode: string,
1113    config?: IInstallPluginConfig,
1114) {
1115    if (funcCode) {
1116        const plugin = new Plugin(funcCode, '');
1117        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1118        if (_pluginIndex !== -1) {
1119            // 静默忽略
1120            return plugin;
1121        }
1122        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1123        if (oldVersionPlugin && !config?.notCheckVersion) {
1124            if (
1125                compare(
1126                    oldVersionPlugin.instance.version ?? '',
1127                    plugin.instance.version ?? '',
1128                    '>',
1129                )
1130            ) {
1131                throw new Error('已安装更新版本的插件');
1132            }
1133        }
1134
1135        if (plugin.hash !== '') {
1136            const fn = nanoid();
1137            if (oldVersionPlugin) {
1138                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
1139                try {
1140                    await unlink(oldVersionPlugin.path);
1141                } catch {}
1142            }
1143            const pluginPath = `${pathConst.pluginPath}${fn}.js`;
1144            await writeFile(pluginPath, funcCode, 'utf8');
1145            plugin.path = pluginPath;
1146            plugins = plugins.concat(plugin);
1147            pluginStateMapper.notify();
1148            return plugin;
1149        }
1150        throw new Error('插件无法解析!');
1151    }
1152}
1153
1154// 安装插件
1155async function installPlugin(
1156    pluginPath: string,
1157    config?: IInstallPluginConfig,
1158) {
1159    // if (pluginPath.endsWith('.js')) {
1160    const funcCode = await readFile(pluginPath, 'utf8');
1161
1162    if (funcCode) {
1163        const plugin = new Plugin(funcCode, pluginPath);
1164        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1165        if (_pluginIndex !== -1) {
1166            // 静默忽略
1167            return plugin;
1168        }
1169        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1170        if (oldVersionPlugin && !config?.notCheckVersion) {
1171            if (
1172                compare(
1173                    oldVersionPlugin.instance.version ?? '',
1174                    plugin.instance.version ?? '',
1175                    '>',
1176                )
1177            ) {
1178                throw new Error('已安装更新版本的插件');
1179            }
1180        }
1181
1182        if (plugin.hash !== '') {
1183            const fn = nanoid();
1184            if (oldVersionPlugin) {
1185                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
1186                try {
1187                    await unlink(oldVersionPlugin.path);
1188                } catch {}
1189            }
1190            const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1191            await copyFile(pluginPath, _pluginPath);
1192            plugin.path = _pluginPath;
1193            plugins = plugins.concat(plugin);
1194            pluginStateMapper.notify();
1195            return plugin;
1196        }
1197        throw new Error('插件无法解析!');
1198    }
1199    throw new Error('插件无法识别!');
1200}
1201
1202const reqHeaders = {
1203    'Cache-Control': 'no-cache',
1204    Pragma: 'no-cache',
1205    Expires: '0',
1206};
1207
1208async function installPluginFromUrl(
1209    url: string,
1210    config?: IInstallPluginConfig,
1211) {
1212    try {
1213        const funcCode = (
1214            await axios.get(url, {
1215                headers: reqHeaders,
1216            })
1217        ).data;
1218        if (funcCode) {
1219            const plugin = new Plugin(funcCode, '');
1220            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1221            if (_pluginIndex !== -1) {
1222                // 静默忽略
1223                return;
1224            }
1225            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1226            if (oldVersionPlugin && !config?.notCheckVersion) {
1227                if (
1228                    compare(
1229                        oldVersionPlugin.instance.version ?? '',
1230                        plugin.instance.version ?? '',
1231                        '>',
1232                    )
1233                ) {
1234                    throw new Error('已安装更新版本的插件');
1235                }
1236            }
1237
1238            if (plugin.hash !== '') {
1239                const fn = nanoid();
1240                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1241                await writeFile(_pluginPath, funcCode, 'utf8');
1242                plugin.path = _pluginPath;
1243                plugins = plugins.concat(plugin);
1244                if (oldVersionPlugin) {
1245                    plugins = plugins.filter(
1246                        _ => _.hash !== oldVersionPlugin.hash,
1247                    );
1248                    try {
1249                        await unlink(oldVersionPlugin.path);
1250                    } catch {}
1251                }
1252                pluginStateMapper.notify();
1253                return;
1254            }
1255            throw new Error('插件无法解析!');
1256        }
1257    } catch (e: any) {
1258        devLog('error', 'URL安装插件失败', e, e?.message);
1259        errorLog('URL安装插件失败', e);
1260        throw new Error(e?.message ?? '');
1261    }
1262}
1263
1264/** 卸载插件 */
1265async function uninstallPlugin(hash: string) {
1266    const targetIndex = plugins.findIndex(_ => _.hash === hash);
1267    if (targetIndex !== -1) {
1268        try {
1269            const pluginName = plugins[targetIndex].name;
1270            await unlink(plugins[targetIndex].path);
1271            plugins = plugins.filter(_ => _.hash !== hash);
1272            pluginStateMapper.notify();
1273            // 防止其他重名
1274            if (plugins.every(_ => _.name !== pluginName)) {
1275                MediaExtra.removeAll(pluginName);
1276            }
1277        } catch {}
1278    }
1279}
1280
1281async function uninstallAllPlugins() {
1282    await Promise.all(
1283        plugins.map(async plugin => {
1284            try {
1285                const pluginName = plugin.name;
1286                await unlink(plugin.path);
1287                MediaExtra.removeAll(pluginName);
1288            } catch (e) {}
1289        }),
1290    );
1291    plugins = [];
1292    pluginStateMapper.notify();
1293
1294    /** 清除空余文件,异步做就可以了 */
1295    readDir(pathConst.pluginPath)
1296        .then(fns => {
1297            fns.forEach(fn => {
1298                unlink(fn.path).catch(emptyFunction);
1299            });
1300        })
1301        .catch(emptyFunction);
1302}
1303
1304async function updatePlugin(plugin: Plugin) {
1305    const updateUrl = plugin.instance.srcUrl;
1306    if (!updateUrl) {
1307        throw new Error('没有更新源');
1308    }
1309    try {
1310        await installPluginFromUrl(updateUrl);
1311    } catch (e: any) {
1312        if (e.message === '插件已安装') {
1313            throw new Error('当前已是最新版本');
1314        } else {
1315            throw e;
1316        }
1317    }
1318}
1319
1320function getByMedia(mediaItem: ICommon.IMediaBase) {
1321    return getByName(mediaItem?.platform);
1322}
1323
1324function getByHash(hash: string) {
1325    return hash === localPluginHash
1326        ? localFilePlugin
1327        : plugins.find(_ => _.hash === hash);
1328}
1329
1330function getByName(name: string) {
1331    return name === localPluginPlatform
1332        ? localFilePlugin
1333        : plugins.find(_ => _.name === name);
1334}
1335
1336function getValidPlugins() {
1337    return plugins.filter(_ => _.state === 'enabled');
1338}
1339
1340function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) {
1341    return plugins.filter(
1342        _ =>
1343            _.state === 'enabled' &&
1344            _.instance.search &&
1345            (supportedSearchType && _.instance.supportedSearchType
1346                ? _.instance.supportedSearchType.includes(supportedSearchType)
1347                : true),
1348    );
1349}
1350
1351function getSortedSearchablePlugins(
1352    supportedSearchType?: ICommon.SupportMediaType,
1353) {
1354    return getSearchablePlugins(supportedSearchType).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 getTopListsablePlugins() {
1364    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
1365}
1366
1367function getSortedTopListsablePlugins() {
1368    return getTopListsablePlugins().sort((a, b) =>
1369        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1370            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1371        0
1372            ? -1
1373            : 1,
1374    );
1375}
1376
1377function getRecommendSheetablePlugins() {
1378    return plugins.filter(
1379        _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag,
1380    );
1381}
1382
1383function getSortedRecommendSheetablePlugins() {
1384    return getRecommendSheetablePlugins().sort((a, b) =>
1385        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1386            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1387        0
1388            ? -1
1389            : 1,
1390    );
1391}
1392
1393function useSortedPlugins() {
1394    const _plugins = pluginStateMapper.useMappedState();
1395    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
1396
1397    const [sortedPlugins, setSortedPlugins] = useState(
1398        [..._plugins].sort((a, b) =>
1399            (_pluginMetaAll[a.name]?.order ?? Infinity) -
1400                (_pluginMetaAll[b.name]?.order ?? Infinity) <
1401            0
1402                ? -1
1403                : 1,
1404        ),
1405    );
1406
1407    useEffect(() => {
1408        InteractionManager.runAfterInteractions(() => {
1409            setSortedPlugins(
1410                [..._plugins].sort((a, b) =>
1411                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
1412                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
1413                    0
1414                        ? -1
1415                        : 1,
1416                ),
1417            );
1418        });
1419    }, [_plugins, _pluginMetaAll]);
1420
1421    return sortedPlugins;
1422}
1423
1424async function setPluginEnabled(plugin: Plugin, enabled?: boolean) {
1425    const target = plugins.find(it => it.hash === plugin.hash);
1426    if (target) {
1427        target.state = enabled ? 'enabled' : 'disabled';
1428        plugins = [...plugins];
1429        pluginStateMapper.notify();
1430        PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled);
1431    }
1432}
1433
1434const PluginManager = {
1435    setup,
1436    installPlugin,
1437    installPluginFromRawCode,
1438    installPluginFromUrl,
1439    updatePlugin,
1440    uninstallPlugin,
1441    getByMedia,
1442    getByHash,
1443    getByName,
1444    getValidPlugins,
1445    getSearchablePlugins,
1446    getSortedSearchablePlugins,
1447    getTopListsablePlugins,
1448    getSortedRecommendSheetablePlugins,
1449    getSortedTopListsablePlugins,
1450    usePlugins: pluginStateMapper.useMappedState,
1451    useSortedPlugins,
1452    uninstallAllPlugins,
1453    setPluginEnabled,
1454};
1455
1456export default PluginManager;
1457