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