xref: /MusicFree/src/core/pluginManager.ts (revision e3fa9b3cddf0bd38f98a9e25d7a3db17f438e281)
1import {
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 {InteractionManager, ToastAndroid} from 'react-native';
15import pathConst from '@/constants/pathConst';
16import {compare, satisfies} from 'compare-versions';
17import DeviceInfo from 'react-native-device-info';
18import StateMapper from '@/utils/stateMapper';
19import MediaMeta from './mediaMeta';
20import {nanoid} from 'nanoid';
21import {devLog, errorLog, trace} from '../utils/log';
22import Cache from './cache';
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 {FileSystem} from 'react-native-file-access';
43import Mp3Util from '@/native/mp3Util';
44import {PluginMeta} from './pluginMeta';
45import {useEffect, useState} from 'react';
46import {getFileName} from '@/utils/fileUtils';
47
48axios.defaults.timeout = 2000;
49
50const sha256 = CryptoJs.SHA256;
51
52export enum PluginStateCode {
53    /** 版本不匹配 */
54    VersionNotMatch = 'VERSION NOT MATCH',
55    /** 无法解析 */
56    CannotParse = 'CANNOT PARSE',
57}
58
59const packages: Record<string, any> = {
60    cheerio,
61    'crypto-js': CryptoJs,
62    axios,
63    dayjs,
64    'big-integer': bigInt,
65    qs,
66    he,
67    '@react-native-cookies/cookies': CookieManager,
68};
69
70const _require = (packageName: string) => {
71    let pkg = packages[packageName];
72    pkg.default = pkg;
73    return pkg;
74};
75
76const _consoleBind = function (
77    method: 'log' | 'error' | 'info' | 'warn',
78    ...args: any
79) {
80    const fn = console[method];
81    if (fn) {
82        fn(...args);
83        devLog(method, ...args);
84    }
85};
86
87const _console = {
88    log: _consoleBind.bind(null, 'log'),
89    warn: _consoleBind.bind(null, 'warn'),
90    info: _consoleBind.bind(null, 'info'),
91    error: _consoleBind.bind(null, 'error'),
92};
93
94//#region 插件类
95export class Plugin {
96    /** 插件名 */
97    public name: string;
98    /** 插件的hash,作为唯一id */
99    public hash: string;
100    /** 插件状态:激活、关闭、错误 */
101    public state: 'enabled' | 'disabled' | 'error';
102    /** 插件状态信息 */
103    public stateCode?: PluginStateCode;
104    /** 插件的实例 */
105    public instance: IPlugin.IPluginInstance;
106    /** 插件路径 */
107    public path: string;
108    /** 插件方法 */
109    public methods: PluginMethods;
110
111    constructor(
112        funcCode: string | (() => IPlugin.IPluginInstance),
113        pluginPath: string,
114    ) {
115        this.state = 'enabled';
116        let _instance: IPlugin.IPluginInstance;
117        const _module: any = {exports: {}};
118        try {
119            if (typeof funcCode === 'string') {
120                // 插件的环境变量
121                const env = {
122                    getUserVariables: () => {
123                        return (
124                            PluginMeta.getPluginMeta(this)?.userVariables ?? {}
125                        );
126                    },
127                    os: 'android',
128                };
129
130                // eslint-disable-next-line no-new-func
131                _instance = Function(`
132                    'use strict';
133                    return function(require, __musicfree_require, module, exports, console, env) {
134                        ${funcCode}
135                    }
136                `)()(
137                    _require,
138                    _require,
139                    _module,
140                    _module.exports,
141                    _console,
142                    env,
143                );
144                if (_module.exports.default) {
145                    _instance = _module.exports
146                        .default as IPlugin.IPluginInstance;
147                } else {
148                    _instance = _module.exports as IPlugin.IPluginInstance;
149                }
150            } else {
151                _instance = funcCode();
152            }
153            // 插件初始化后的一些操作
154            if (Array.isArray(_instance.userVariables)) {
155                _instance.userVariables = _instance.userVariables.filter(
156                    it => it?.key,
157                );
158            }
159            this.checkValid(_instance);
160        } catch (e: any) {
161            console.log(e);
162            this.state = 'error';
163            this.stateCode = PluginStateCode.CannotParse;
164            if (e?.stateCode) {
165                this.stateCode = e.stateCode;
166            }
167            errorLog(`${pluginPath}插件无法解析 `, {
168                stateCode: this.stateCode,
169                message: e?.message,
170                stack: e?.stack,
171            });
172            _instance = e?.instance ?? {
173                _path: '',
174                platform: '',
175                appVersion: '',
176                async getMediaSource() {
177                    return null;
178                },
179                async search() {
180                    return {};
181                },
182                async getAlbumInfo() {
183                    return null;
184                },
185            };
186        }
187        this.instance = _instance;
188        this.path = pluginPath;
189        this.name = _instance.platform;
190        if (
191            this.instance.platform === '' ||
192            this.instance.platform === undefined
193        ) {
194            this.hash = '';
195        } else {
196            if (typeof funcCode === 'string') {
197                this.hash = sha256(funcCode).toString();
198            } else {
199                this.hash = sha256(funcCode.toString()).toString();
200            }
201        }
202
203        // 放在最后
204        this.methods = new PluginMethods(this);
205    }
206
207    private checkValid(_instance: IPlugin.IPluginInstance) {
208        /** 版本号校验 */
209        if (
210            _instance.appVersion &&
211            !satisfies(DeviceInfo.getVersion(), _instance.appVersion)
212        ) {
213            throw {
214                instance: _instance,
215                stateCode: PluginStateCode.VersionNotMatch,
216            };
217        }
218        return true;
219    }
220}
221//#endregion
222
223//#region 基于插件类封装的方法,供给APP侧直接调用
224/** 有缓存等信息 */
225class PluginMethods implements IPlugin.IPluginInstanceMethods {
226    private plugin;
227    constructor(plugin: Plugin) {
228        this.plugin = plugin;
229    }
230    /** 搜索 */
231    async search<T extends ICommon.SupportMediaType>(
232        query: string,
233        page: number,
234        type: T,
235    ): Promise<IPlugin.ISearchResult<T>> {
236        if (!this.plugin.instance.search) {
237            return {
238                isEnd: true,
239                data: [],
240            };
241        }
242
243        const result =
244            (await this.plugin.instance.search(query, page, type)) ?? {};
245        if (Array.isArray(result.data)) {
246            result.data.forEach(_ => {
247                resetMediaItem(_, this.plugin.name);
248            });
249            return {
250                isEnd: result.isEnd ?? true,
251                data: result.data,
252            };
253        }
254        return {
255            isEnd: true,
256            data: [],
257        };
258    }
259
260    /** 获取真实源 */
261    async getMediaSource(
262        musicItem: IMusic.IMusicItemBase,
263        quality: IMusic.IQualityKey = 'standard',
264        retryCount = 1,
265        notUpdateCache = false,
266    ): Promise<IPlugin.IMediaSourceResult | null> {
267        // 1. 本地搜索 其实直接读mediameta就好了
268        const localPath =
269            getInternalData<string>(musicItem, InternalDataType.LOCALPATH) ??
270            getInternalData<string>(
271                LocalMusicSheet.isLocalMusic(musicItem),
272                InternalDataType.LOCALPATH,
273            );
274        if (
275            localPath &&
276            (localPath.startsWith('content://') ||
277                (await FileSystem.exists(localPath)))
278        ) {
279            trace('本地播放', localPath);
280            return {
281                url: localPath,
282            };
283        }
284        console.log('BFFF2');
285
286        if (musicItem.platform === localPluginPlatform) {
287            throw new Error('本地音乐不存在');
288        }
289        // 2. 缓存播放
290        const mediaCache = Cache.get(musicItem);
291        const pluginCacheControl =
292            this.plugin.instance.cacheControl ?? 'no-cache';
293        if (
294            mediaCache &&
295            mediaCache?.qualities?.[quality]?.url &&
296            (pluginCacheControl === CacheControl.Cache ||
297                (pluginCacheControl === CacheControl.NoCache &&
298                    Network.isOffline()))
299        ) {
300            trace('播放', '缓存播放');
301            const qualityInfo = mediaCache.qualities[quality];
302            return {
303                url: qualityInfo.url,
304                headers: mediaCache.headers,
305                userAgent:
306                    mediaCache.userAgent ?? mediaCache.headers?.['user-agent'],
307            };
308        }
309        // 3. 插件解析
310        if (!this.plugin.instance.getMediaSource) {
311            return {url: musicItem?.qualities?.[quality]?.url ?? musicItem.url};
312        }
313        try {
314            const {url, headers} = (await this.plugin.instance.getMediaSource(
315                musicItem,
316                quality,
317            )) ?? {url: musicItem?.qualities?.[quality]?.url};
318            if (!url) {
319                throw new Error('NOT RETRY');
320            }
321            trace('播放', '插件播放');
322            const result = {
323                url,
324                headers,
325                userAgent: headers?.['user-agent'],
326            } as IPlugin.IMediaSourceResult;
327
328            if (
329                pluginCacheControl !== CacheControl.NoStore &&
330                !notUpdateCache
331            ) {
332                Cache.update(musicItem, [
333                    ['headers', result.headers],
334                    ['userAgent', result.userAgent],
335                    [`qualities.${quality}.url`, url],
336                ]);
337            }
338
339            return result;
340        } catch (e: any) {
341            if (retryCount > 0 && e?.message !== 'NOT RETRY') {
342                await delay(150);
343                return this.getMediaSource(musicItem, quality, --retryCount);
344            }
345            errorLog('获取真实源失败', e?.message);
346            devLog('error', '获取真实源失败', e, e?.message);
347            return null;
348        }
349    }
350
351    /** 获取音乐详情 */
352    async getMusicInfo(
353        musicItem: ICommon.IMediaBase,
354    ): Promise<Partial<IMusic.IMusicItem> | null> {
355        if (!this.plugin.instance.getMusicInfo) {
356            return null;
357        }
358        try {
359            return (
360                this.plugin.instance.getMusicInfo(
361                    resetMediaItem(musicItem, undefined, true),
362                ) ?? null
363            );
364        } catch (e: any) {
365            devLog('error', '获取音乐详情失败', e, e?.message);
366            return null;
367        }
368    }
369
370    /** 获取歌词 */
371    async getLyric(
372        musicItem: IMusic.IMusicItemBase,
373        from?: IMusic.IMusicItemBase,
374    ): Promise<ILyric.ILyricSource | null> {
375        // 1.额外存储的meta信息
376        const meta = MediaMeta.get(musicItem);
377        if (meta && meta.associatedLrc) {
378            // 有关联歌词
379            if (
380                isSameMediaItem(musicItem, from) ||
381                isSameMediaItem(meta.associatedLrc, musicItem)
382            ) {
383                // 形成环路,断开当前的环
384                await MediaMeta.update(musicItem, {
385                    associatedLrc: undefined,
386                });
387                // 无歌词
388                return null;
389            }
390            // 获取关联歌词
391            const associatedMeta = MediaMeta.get(meta.associatedLrc) ?? {};
392            const result = await this.getLyric(
393                {...meta.associatedLrc, ...associatedMeta},
394                from ?? musicItem,
395            );
396            if (result) {
397                // 如果有关联歌词,就返回关联歌词,深度优先
398                return result;
399            }
400        }
401        const cache = Cache.get(musicItem);
402        let rawLrc = meta?.rawLrc || musicItem.rawLrc || cache?.rawLrc;
403        let lrcUrl = meta?.lrc || musicItem.lrc || cache?.lrc;
404        // 如果存在文本
405        if (rawLrc) {
406            return {
407                rawLrc,
408                lrc: lrcUrl,
409            };
410        }
411        // 2.本地缓存
412        const localLrc =
413            meta?.[internalSerializeKey]?.local?.localLrc ||
414            cache?.[internalSerializeKey]?.local?.localLrc;
415        if (localLrc && (await exists(localLrc))) {
416            rawLrc = await readFile(localLrc, 'utf8');
417            return {
418                rawLrc,
419                lrc: lrcUrl,
420            };
421        }
422        // 3.优先使用url
423        if (lrcUrl) {
424            try {
425                // 需要超时时间 axios timeout 但是没生效
426                rawLrc = (await axios.get(lrcUrl, {timeout: 2000})).data;
427                return {
428                    rawLrc,
429                    lrc: lrcUrl,
430                };
431            } catch {
432                lrcUrl = undefined;
433            }
434        }
435        // 4. 如果地址失效
436        if (!lrcUrl) {
437            // 插件获得url
438            try {
439                let lrcSource;
440                if (from) {
441                    lrcSource = await PluginManager.getByMedia(
442                        musicItem,
443                    )?.instance?.getLyric?.(
444                        resetMediaItem(musicItem, undefined, true),
445                    );
446                } else {
447                    lrcSource = await this.plugin.instance?.getLyric?.(
448                        resetMediaItem(musicItem, undefined, true),
449                    );
450                }
451
452                rawLrc = lrcSource?.rawLrc;
453                lrcUrl = lrcSource?.lrc;
454            } catch (e: any) {
455                trace('插件获取歌词失败', e?.message, 'error');
456                devLog('error', '插件获取歌词失败', e, e?.message);
457            }
458        }
459        // 5. 最后一次请求
460        if (rawLrc || lrcUrl) {
461            const filename = `${pathConst.lrcCachePath}${nanoid()}.lrc`;
462            if (lrcUrl) {
463                try {
464                    rawLrc = (await axios.get(lrcUrl, {timeout: 2000})).data;
465                } catch {}
466            }
467            if (rawLrc) {
468                await writeFile(filename, rawLrc, 'utf8');
469                // 写入缓存
470                Cache.update(musicItem, [
471                    [`${internalSerializeKey}.local.localLrc`, filename],
472                ]);
473                // 如果有meta
474                if (meta) {
475                    MediaMeta.update(musicItem, [
476                        [`${internalSerializeKey}.local.localLrc`, filename],
477                    ]);
478                }
479                return {
480                    rawLrc,
481                    lrc: lrcUrl,
482                };
483            }
484        }
485        // 6. 如果是本地文件
486        const isDownloaded = LocalMusicSheet.isLocalMusic(musicItem);
487        if (musicItem.platform !== localPluginPlatform && isDownloaded) {
488            const res = await localFilePlugin.instance!.getLyric!(isDownloaded);
489            if (res) {
490                return res;
491            }
492        }
493        devLog('warn', '无歌词');
494
495        return null;
496    }
497
498    /** 获取歌词文本 */
499    async getLyricText(
500        musicItem: IMusic.IMusicItem,
501    ): Promise<string | undefined> {
502        return (await this.getLyric(musicItem))?.rawLrc;
503    }
504
505    /** 获取专辑信息 */
506    async getAlbumInfo(
507        albumItem: IAlbum.IAlbumItemBase,
508        page: number = 1,
509    ): Promise<IPlugin.IAlbumInfoResult | null> {
510        if (!this.plugin.instance.getAlbumInfo) {
511            return {
512                albumItem,
513                musicList: albumItem?.musicList ?? [],
514                isEnd: true,
515            };
516        }
517        try {
518            const result = await this.plugin.instance.getAlbumInfo(
519                resetMediaItem(albumItem, undefined, true),
520                page,
521            );
522            if (!result) {
523                throw new Error();
524            }
525            result?.musicList?.forEach(_ => {
526                resetMediaItem(_, this.plugin.name);
527                _.album = albumItem.title;
528            });
529
530            if (page <= 1) {
531                // 合并信息
532                return {
533                    albumItem: {...albumItem, ...(result?.albumItem ?? {})},
534                    isEnd: result.isEnd === false ? false : true,
535                    musicList: result.musicList,
536                };
537            } else {
538                return {
539                    isEnd: result.isEnd === false ? false : true,
540                    musicList: result.musicList,
541                };
542            }
543        } catch (e: any) {
544            trace('获取专辑信息失败', e?.message);
545            devLog('error', '获取专辑信息失败', e, e?.message);
546
547            return null;
548        }
549    }
550
551    /** 获取歌单信息 */
552    async getMusicSheetInfo(
553        sheetItem: IMusic.IMusicSheetItem,
554        page: number = 1,
555    ): Promise<IPlugin.ISheetInfoResult | null> {
556        if (!this.plugin.instance.getMusicSheetInfo) {
557            return {
558                sheetItem,
559                musicList: sheetItem?.musicList ?? [],
560                isEnd: true,
561            };
562        }
563        try {
564            const result = await this.plugin.instance?.getMusicSheetInfo?.(
565                resetMediaItem(sheetItem, undefined, true),
566                page,
567            );
568            if (!result) {
569                throw new Error();
570            }
571            result?.musicList?.forEach(_ => {
572                resetMediaItem(_, this.plugin.name);
573            });
574
575            if (page <= 1) {
576                // 合并信息
577                return {
578                    sheetItem: {...sheetItem, ...(result?.sheetItem ?? {})},
579                    isEnd: result.isEnd === false ? false : true,
580                    musicList: result.musicList,
581                };
582            } else {
583                return {
584                    isEnd: result.isEnd === false ? false : true,
585                    musicList: result.musicList,
586                };
587            }
588        } catch (e: any) {
589            trace('获取歌单信息失败', e, e?.message);
590            devLog('error', '获取歌单信息失败', e, e?.message);
591
592            return null;
593        }
594    }
595
596    /** 查询作者信息 */
597    async getArtistWorks<T extends IArtist.ArtistMediaType>(
598        artistItem: IArtist.IArtistItem,
599        page: number,
600        type: T,
601    ): Promise<IPlugin.ISearchResult<T>> {
602        if (!this.plugin.instance.getArtistWorks) {
603            return {
604                isEnd: true,
605                data: [],
606            };
607        }
608        try {
609            const result = await this.plugin.instance.getArtistWorks(
610                artistItem,
611                page,
612                type,
613            );
614            if (!result.data) {
615                return {
616                    isEnd: true,
617                    data: [],
618                };
619            }
620            result.data?.forEach(_ => resetMediaItem(_, this.plugin.name));
621            return {
622                isEnd: result.isEnd ?? true,
623                data: result.data,
624            };
625        } catch (e: any) {
626            trace('查询作者信息失败', e?.message);
627            devLog('error', '查询作者信息失败', e, e?.message);
628
629            throw e;
630        }
631    }
632
633    /** 导入歌单 */
634    async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> {
635        try {
636            const result =
637                (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? [];
638            result.forEach(_ => resetMediaItem(_, this.plugin.name));
639            return result;
640        } catch (e: any) {
641            console.log(e);
642            devLog('error', '导入歌单失败', e, e?.message);
643
644            return [];
645        }
646    }
647    /** 导入单曲 */
648    async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> {
649        try {
650            const result = await this.plugin.instance?.importMusicItem?.(
651                urlLike,
652            );
653            if (!result) {
654                throw new Error();
655            }
656            resetMediaItem(result, this.plugin.name);
657            return result;
658        } catch (e: any) {
659            devLog('error', '导入单曲失败', e, e?.message);
660
661            return null;
662        }
663    }
664    /** 获取榜单 */
665    async getTopLists(): Promise<IMusic.IMusicSheetGroupItem[]> {
666        try {
667            const result = await this.plugin.instance?.getTopLists?.();
668            if (!result) {
669                throw new Error();
670            }
671            return result;
672        } catch (e: any) {
673            devLog('error', '获取榜单失败', e, e?.message);
674            return [];
675        }
676    }
677    /** 获取榜单详情 */
678    async getTopListDetail(
679        topListItem: IMusic.IMusicSheetItemBase,
680    ): Promise<ICommon.WithMusicList<IMusic.IMusicSheetItemBase>> {
681        try {
682            const result = await this.plugin.instance?.getTopListDetail?.(
683                topListItem,
684            );
685            if (!result) {
686                throw new Error();
687            }
688            if (result.musicList) {
689                result.musicList.forEach(_ =>
690                    resetMediaItem(_, this.plugin.name),
691                );
692            }
693            return result;
694        } catch (e: any) {
695            devLog('error', '获取榜单详情失败', e, e?.message);
696            return {
697                ...topListItem,
698                musicList: [],
699            };
700        }
701    }
702
703    /** 获取推荐歌单的tag */
704    async getRecommendSheetTags(): Promise<IPlugin.IGetRecommendSheetTagsResult> {
705        try {
706            const result =
707                await this.plugin.instance?.getRecommendSheetTags?.();
708            if (!result) {
709                throw new Error();
710            }
711            return result;
712        } catch (e: any) {
713            devLog('error', '获取推荐歌单失败', e, e?.message);
714            return {
715                data: [],
716            };
717        }
718    }
719    /** 获取某个tag的推荐歌单 */
720    async getRecommendSheetsByTag(
721        tagItem: ICommon.IUnique,
722        page?: number,
723    ): Promise<ICommon.PaginationResponse<IMusic.IMusicSheetItemBase>> {
724        try {
725            const result =
726                await this.plugin.instance?.getRecommendSheetsByTag?.(
727                    tagItem,
728                    page ?? 1,
729                );
730            if (!result) {
731                throw new Error();
732            }
733            if (result.isEnd !== false) {
734                result.isEnd = true;
735            }
736            if (!result.data) {
737                result.data = [];
738            }
739            result.data.forEach(item => resetMediaItem(item, this.plugin.name));
740
741            return result;
742        } catch (e: any) {
743            devLog('error', '获取推荐歌单详情失败', e, e?.message);
744            return {
745                isEnd: true,
746                data: [],
747            };
748        }
749    }
750}
751//#endregion
752
753let plugins: Array<Plugin> = [];
754const pluginStateMapper = new StateMapper(() => plugins);
755
756//#region 本地音乐插件
757/** 本地插件 */
758const localFilePlugin = new Plugin(function () {
759    return {
760        platform: localPluginPlatform,
761        _path: '',
762        async getMusicInfo(musicBase) {
763            const localPath = getInternalData<string>(
764                musicBase,
765                InternalDataType.LOCALPATH,
766            );
767            if (localPath) {
768                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
769                return {
770                    artwork: coverImg,
771                };
772            }
773            return null;
774        },
775        async getLyric(musicBase) {
776            const localPath = getInternalData<string>(
777                musicBase,
778                InternalDataType.LOCALPATH,
779            );
780            let rawLrc: string | null = null;
781            if (localPath) {
782                // 读取内嵌歌词
783                try {
784                    rawLrc = await Mp3Util.getLyric(localPath);
785                } catch (e) {
786                    console.log('读取内嵌歌词失败', e);
787                }
788                if (!rawLrc) {
789                    // 读取配置歌词
790                    const lastDot = localPath.lastIndexOf('.');
791                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
792
793                    try {
794                        if (await exists(lrcPath)) {
795                            rawLrc = await readFile(lrcPath, 'utf8');
796                        }
797                    } catch {}
798                }
799            }
800
801            return rawLrc
802                ? {
803                      rawLrc,
804                  }
805                : null;
806        },
807        async importMusicItem(urlLike) {
808            let meta: any = {};
809            try {
810                meta = await Mp3Util.getBasicMeta(urlLike);
811            } catch {}
812            const id = await FileSystem.hash(urlLike, 'MD5');
813            return {
814                id: id,
815                platform: '本地',
816                title: meta?.title ?? getFileName(urlLike),
817                artist: meta?.artist ?? '未知歌手',
818                duration: parseInt(meta?.duration ?? '0') / 1000,
819                album: meta?.album ?? '未知专辑',
820                artwork: '',
821                [internalSerializeKey]: {
822                    localPath: urlLike,
823                },
824            };
825        },
826    };
827}, '');
828localFilePlugin.hash = localPluginHash;
829
830//#endregion
831
832async function setup() {
833    const _plugins: Array<Plugin> = [];
834    try {
835        // 加载插件
836        const pluginsPaths = await readDir(pathConst.pluginPath);
837        for (let i = 0; i < pluginsPaths.length; ++i) {
838            const _pluginUrl = pluginsPaths[i];
839            trace('初始化插件', _pluginUrl);
840            if (
841                _pluginUrl.isFile() &&
842                (_pluginUrl.name?.endsWith?.('.js') ||
843                    _pluginUrl.path?.endsWith?.('.js'))
844            ) {
845                const funcCode = await readFile(_pluginUrl.path, 'utf8');
846                const plugin = new Plugin(funcCode, _pluginUrl.path);
847                const _pluginIndex = _plugins.findIndex(
848                    p => p.hash === plugin.hash,
849                );
850                if (_pluginIndex !== -1) {
851                    // 重复插件,直接忽略
852                    continue;
853                }
854                plugin.hash !== '' && _plugins.push(plugin);
855            }
856        }
857
858        plugins = _plugins;
859        /** 初始化meta信息 */
860        await PluginMeta.setupMeta(plugins.map(_ => _.name));
861        /** 查看一下是否有禁用的标记 */
862        const allMeta = PluginMeta.getPluginMetaAll() ?? {};
863        for (let plugin of plugins) {
864            if (allMeta[plugin.name]?.enabled === false) {
865                plugin.state = 'disabled';
866            }
867        }
868        pluginStateMapper.notify();
869    } catch (e: any) {
870        ToastAndroid.show(
871            `插件初始化失败:${e?.message ?? e}`,
872            ToastAndroid.LONG,
873        );
874        errorLog('插件初始化失败', e?.message);
875        throw e;
876    }
877}
878
879// 安装插件
880async function installPlugin(pluginPath: string) {
881    // if (pluginPath.endsWith('.js')) {
882    const funcCode = await readFile(pluginPath, 'utf8');
883    const plugin = new Plugin(funcCode, pluginPath);
884    const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
885    if (_pluginIndex !== -1) {
886        throw new Error('插件已安装');
887    }
888    if (plugin.hash !== '') {
889        const fn = nanoid();
890        const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
891        await copyFile(pluginPath, _pluginPath);
892        plugin.path = _pluginPath;
893        plugins = plugins.concat(plugin);
894        pluginStateMapper.notify();
895        return plugin;
896    }
897    throw new Error('插件无法解析');
898    // }
899    // throw new Error('插件不存在');
900}
901
902interface IInstallPluginConfig {
903    notCheckVersion?: boolean;
904}
905
906async function installPluginFromUrl(
907    url: string,
908    config?: IInstallPluginConfig,
909) {
910    try {
911        const funcCode = (await axios.get(url)).data;
912        if (funcCode) {
913            const plugin = new Plugin(funcCode, '');
914            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
915            if (_pluginIndex !== -1) {
916                // 静默忽略
917                return;
918            }
919            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
920            if (oldVersionPlugin && !config?.notCheckVersion) {
921                if (
922                    compare(
923                        oldVersionPlugin.instance.version ?? '',
924                        plugin.instance.version ?? '',
925                        '>',
926                    )
927                ) {
928                    throw new Error('已安装更新版本的插件');
929                }
930            }
931
932            if (plugin.hash !== '') {
933                const fn = nanoid();
934                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
935                await writeFile(_pluginPath, funcCode, 'utf8');
936                plugin.path = _pluginPath;
937                plugins = plugins.concat(plugin);
938                if (oldVersionPlugin) {
939                    plugins = plugins.filter(
940                        _ => _.hash !== oldVersionPlugin.hash,
941                    );
942                    try {
943                        await unlink(oldVersionPlugin.path);
944                    } catch {}
945                }
946                pluginStateMapper.notify();
947                return;
948            }
949            throw new Error('插件无法解析!');
950        }
951    } catch (e: any) {
952        devLog('error', 'URL安装插件失败', e, e?.message);
953        errorLog('URL安装插件失败', e);
954        throw new Error(e?.message ?? '');
955    }
956}
957
958/** 卸载插件 */
959async function uninstallPlugin(hash: string) {
960    const targetIndex = plugins.findIndex(_ => _.hash === hash);
961    if (targetIndex !== -1) {
962        try {
963            const pluginName = plugins[targetIndex].name;
964            await unlink(plugins[targetIndex].path);
965            plugins = plugins.filter(_ => _.hash !== hash);
966            pluginStateMapper.notify();
967            if (plugins.every(_ => _.name !== pluginName)) {
968                await MediaMeta.removePlugin(pluginName);
969            }
970        } catch {}
971    }
972}
973
974async function uninstallAllPlugins() {
975    await Promise.all(
976        plugins.map(async plugin => {
977            try {
978                const pluginName = plugin.name;
979                await unlink(plugin.path);
980                await MediaMeta.removePlugin(pluginName);
981            } catch (e) {}
982        }),
983    );
984    plugins = [];
985    pluginStateMapper.notify();
986
987    /** 清除空余文件,异步做就可以了 */
988    readDir(pathConst.pluginPath)
989        .then(fns => {
990            fns.forEach(fn => {
991                unlink(fn.path).catch(emptyFunction);
992            });
993        })
994        .catch(emptyFunction);
995}
996
997async function updatePlugin(plugin: Plugin) {
998    const updateUrl = plugin.instance.srcUrl;
999    if (!updateUrl) {
1000        throw new Error('没有更新源');
1001    }
1002    try {
1003        await installPluginFromUrl(updateUrl);
1004    } catch (e: any) {
1005        if (e.message === '插件已安装') {
1006            throw new Error('当前已是最新版本');
1007        } else {
1008            throw e;
1009        }
1010    }
1011}
1012
1013function getByMedia(mediaItem: ICommon.IMediaBase) {
1014    return getByName(mediaItem?.platform);
1015}
1016
1017function getByHash(hash: string) {
1018    return hash === localPluginHash
1019        ? localFilePlugin
1020        : plugins.find(_ => _.hash === hash);
1021}
1022
1023function getByName(name: string) {
1024    return name === localPluginPlatform
1025        ? localFilePlugin
1026        : plugins.find(_ => _.name === name);
1027}
1028
1029function getValidPlugins() {
1030    return plugins.filter(_ => _.state === 'enabled');
1031}
1032
1033function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) {
1034    return plugins.filter(
1035        _ =>
1036            _.state === 'enabled' &&
1037            _.instance.search &&
1038            (supportedSearchType && _.instance.supportedSearchType
1039                ? _.instance.supportedSearchType.includes(supportedSearchType)
1040                : true),
1041    );
1042}
1043
1044function getSortedSearchablePlugins(
1045    supportedSearchType?: ICommon.SupportMediaType,
1046) {
1047    return getSearchablePlugins(supportedSearchType).sort((a, b) =>
1048        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1049            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1050        0
1051            ? -1
1052            : 1,
1053    );
1054}
1055
1056function getTopListsablePlugins() {
1057    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
1058}
1059
1060function getSortedTopListsablePlugins() {
1061    return getTopListsablePlugins().sort((a, b) =>
1062        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1063            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1064        0
1065            ? -1
1066            : 1,
1067    );
1068}
1069
1070function getRecommendSheetablePlugins() {
1071    return plugins.filter(
1072        _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag,
1073    );
1074}
1075
1076function getSortedRecommendSheetablePlugins() {
1077    return getRecommendSheetablePlugins().sort((a, b) =>
1078        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1079            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1080        0
1081            ? -1
1082            : 1,
1083    );
1084}
1085
1086function useSortedPlugins() {
1087    const _plugins = pluginStateMapper.useMappedState();
1088    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
1089
1090    const [sortedPlugins, setSortedPlugins] = useState(
1091        [..._plugins].sort((a, b) =>
1092            (_pluginMetaAll[a.name]?.order ?? Infinity) -
1093                (_pluginMetaAll[b.name]?.order ?? Infinity) <
1094            0
1095                ? -1
1096                : 1,
1097        ),
1098    );
1099
1100    useEffect(() => {
1101        InteractionManager.runAfterInteractions(() => {
1102            setSortedPlugins(
1103                [..._plugins].sort((a, b) =>
1104                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
1105                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
1106                    0
1107                        ? -1
1108                        : 1,
1109                ),
1110            );
1111        });
1112    }, [_plugins, _pluginMetaAll]);
1113
1114    return sortedPlugins;
1115}
1116
1117async function setPluginEnabled(plugin: Plugin, enabled?: boolean) {
1118    const target = plugins.find(it => it.hash === plugin.hash);
1119    if (target) {
1120        target.state = enabled ? 'enabled' : 'disabled';
1121        plugins = [...plugins];
1122        pluginStateMapper.notify();
1123        PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled);
1124    }
1125}
1126
1127const PluginManager = {
1128    setup,
1129    installPlugin,
1130    installPluginFromUrl,
1131    updatePlugin,
1132    uninstallPlugin,
1133    getByMedia,
1134    getByHash,
1135    getByName,
1136    getValidPlugins,
1137    getSearchablePlugins,
1138    getSortedSearchablePlugins,
1139    getTopListsablePlugins,
1140    getSortedRecommendSheetablePlugins,
1141    getSortedTopListsablePlugins,
1142    usePlugins: pluginStateMapper.useMappedState,
1143    useSortedPlugins,
1144    uninstallAllPlugins,
1145    setPluginEnabled,
1146};
1147
1148export default PluginManager;
1149