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