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