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