xref: /MusicFree/src/core/pluginManager.ts (revision 4d0d956507a5e90230a0a07fc80821ac4f800408)
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
879interface IInstallPluginConfig {
880    notCheckVersion?: boolean;
881}
882
883// 安装插件
884async function installPlugin(
885    pluginPath: string,
886    config?: IInstallPluginConfig,
887) {
888    // if (pluginPath.endsWith('.js')) {
889    const funcCode = await readFile(pluginPath, 'utf8');
890
891    if (funcCode) {
892        const plugin = new Plugin(funcCode, pluginPath);
893        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
894        if (_pluginIndex !== -1) {
895            // 静默忽略
896            return plugin;
897        }
898        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
899        if (oldVersionPlugin && !config?.notCheckVersion) {
900            if (
901                compare(
902                    oldVersionPlugin.instance.version ?? '',
903                    plugin.instance.version ?? '',
904                    '>',
905                )
906            ) {
907                throw new Error('已安装更新版本的插件');
908            }
909        }
910
911        if (plugin.hash !== '') {
912            const fn = nanoid();
913            if (oldVersionPlugin) {
914                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
915                try {
916                    await unlink(oldVersionPlugin.path);
917                } catch {}
918            }
919            const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
920            await copyFile(pluginPath, _pluginPath);
921            plugin.path = _pluginPath;
922            plugins = plugins.concat(plugin);
923            pluginStateMapper.notify();
924            return plugin;
925        }
926        throw new Error('插件无法解析!');
927    }
928    throw new Error('插件无法识别!');
929}
930
931async function installPluginFromUrl(
932    url: string,
933    config?: IInstallPluginConfig,
934) {
935    try {
936        const funcCode = (await axios.get(url)).data;
937        if (funcCode) {
938            const plugin = new Plugin(funcCode, '');
939            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
940            if (_pluginIndex !== -1) {
941                // 静默忽略
942                return;
943            }
944            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
945            if (oldVersionPlugin && !config?.notCheckVersion) {
946                if (
947                    compare(
948                        oldVersionPlugin.instance.version ?? '',
949                        plugin.instance.version ?? '',
950                        '>',
951                    )
952                ) {
953                    throw new Error('已安装更新版本的插件');
954                }
955            }
956
957            if (plugin.hash !== '') {
958                const fn = nanoid();
959                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
960                await writeFile(_pluginPath, funcCode, 'utf8');
961                plugin.path = _pluginPath;
962                plugins = plugins.concat(plugin);
963                if (oldVersionPlugin) {
964                    plugins = plugins.filter(
965                        _ => _.hash !== oldVersionPlugin.hash,
966                    );
967                    try {
968                        await unlink(oldVersionPlugin.path);
969                    } catch {}
970                }
971                pluginStateMapper.notify();
972                return;
973            }
974            throw new Error('插件无法解析!');
975        }
976    } catch (e: any) {
977        devLog('error', 'URL安装插件失败', e, e?.message);
978        errorLog('URL安装插件失败', e);
979        throw new Error(e?.message ?? '');
980    }
981}
982
983/** 卸载插件 */
984async function uninstallPlugin(hash: string) {
985    const targetIndex = plugins.findIndex(_ => _.hash === hash);
986    if (targetIndex !== -1) {
987        try {
988            const pluginName = plugins[targetIndex].name;
989            await unlink(plugins[targetIndex].path);
990            plugins = plugins.filter(_ => _.hash !== hash);
991            pluginStateMapper.notify();
992            if (plugins.every(_ => _.name !== pluginName)) {
993                await MediaMeta.removePlugin(pluginName);
994            }
995        } catch {}
996    }
997}
998
999async function uninstallAllPlugins() {
1000    await Promise.all(
1001        plugins.map(async plugin => {
1002            try {
1003                const pluginName = plugin.name;
1004                await unlink(plugin.path);
1005                await MediaMeta.removePlugin(pluginName);
1006            } catch (e) {}
1007        }),
1008    );
1009    plugins = [];
1010    pluginStateMapper.notify();
1011
1012    /** 清除空余文件,异步做就可以了 */
1013    readDir(pathConst.pluginPath)
1014        .then(fns => {
1015            fns.forEach(fn => {
1016                unlink(fn.path).catch(emptyFunction);
1017            });
1018        })
1019        .catch(emptyFunction);
1020}
1021
1022async function updatePlugin(plugin: Plugin) {
1023    const updateUrl = plugin.instance.srcUrl;
1024    if (!updateUrl) {
1025        throw new Error('没有更新源');
1026    }
1027    try {
1028        await installPluginFromUrl(updateUrl);
1029    } catch (e: any) {
1030        if (e.message === '插件已安装') {
1031            throw new Error('当前已是最新版本');
1032        } else {
1033            throw e;
1034        }
1035    }
1036}
1037
1038function getByMedia(mediaItem: ICommon.IMediaBase) {
1039    return getByName(mediaItem?.platform);
1040}
1041
1042function getByHash(hash: string) {
1043    return hash === localPluginHash
1044        ? localFilePlugin
1045        : plugins.find(_ => _.hash === hash);
1046}
1047
1048function getByName(name: string) {
1049    return name === localPluginPlatform
1050        ? localFilePlugin
1051        : plugins.find(_ => _.name === name);
1052}
1053
1054function getValidPlugins() {
1055    return plugins.filter(_ => _.state === 'enabled');
1056}
1057
1058function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) {
1059    return plugins.filter(
1060        _ =>
1061            _.state === 'enabled' &&
1062            _.instance.search &&
1063            (supportedSearchType && _.instance.supportedSearchType
1064                ? _.instance.supportedSearchType.includes(supportedSearchType)
1065                : true),
1066    );
1067}
1068
1069function getSortedSearchablePlugins(
1070    supportedSearchType?: ICommon.SupportMediaType,
1071) {
1072    return getSearchablePlugins(supportedSearchType).sort((a, b) =>
1073        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1074            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1075        0
1076            ? -1
1077            : 1,
1078    );
1079}
1080
1081function getTopListsablePlugins() {
1082    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
1083}
1084
1085function getSortedTopListsablePlugins() {
1086    return getTopListsablePlugins().sort((a, b) =>
1087        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1088            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1089        0
1090            ? -1
1091            : 1,
1092    );
1093}
1094
1095function getRecommendSheetablePlugins() {
1096    return plugins.filter(
1097        _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag,
1098    );
1099}
1100
1101function getSortedRecommendSheetablePlugins() {
1102    return getRecommendSheetablePlugins().sort((a, b) =>
1103        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1104            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1105        0
1106            ? -1
1107            : 1,
1108    );
1109}
1110
1111function useSortedPlugins() {
1112    const _plugins = pluginStateMapper.useMappedState();
1113    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
1114
1115    const [sortedPlugins, setSortedPlugins] = useState(
1116        [..._plugins].sort((a, b) =>
1117            (_pluginMetaAll[a.name]?.order ?? Infinity) -
1118                (_pluginMetaAll[b.name]?.order ?? Infinity) <
1119            0
1120                ? -1
1121                : 1,
1122        ),
1123    );
1124
1125    useEffect(() => {
1126        InteractionManager.runAfterInteractions(() => {
1127            setSortedPlugins(
1128                [..._plugins].sort((a, b) =>
1129                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
1130                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
1131                    0
1132                        ? -1
1133                        : 1,
1134                ),
1135            );
1136        });
1137    }, [_plugins, _pluginMetaAll]);
1138
1139    return sortedPlugins;
1140}
1141
1142async function setPluginEnabled(plugin: Plugin, enabled?: boolean) {
1143    const target = plugins.find(it => it.hash === plugin.hash);
1144    if (target) {
1145        target.state = enabled ? 'enabled' : 'disabled';
1146        plugins = [...plugins];
1147        pluginStateMapper.notify();
1148        PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled);
1149    }
1150}
1151
1152const PluginManager = {
1153    setup,
1154    installPlugin,
1155    installPluginFromUrl,
1156    updatePlugin,
1157    uninstallPlugin,
1158    getByMedia,
1159    getByHash,
1160    getByName,
1161    getValidPlugins,
1162    getSearchablePlugins,
1163    getSortedSearchablePlugins,
1164    getTopListsablePlugins,
1165    getSortedRecommendSheetablePlugins,
1166    getSortedTopListsablePlugins,
1167    usePlugins: pluginStateMapper.useMappedState,
1168    useSortedPlugins,
1169    uninstallAllPlugins,
1170    setPluginEnabled,
1171};
1172
1173export default PluginManager;
1174