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