xref: /MusicFree/src/core/pluginManager.ts (revision 53f8cd8ed1685781cd5f791d8f5d4319710d9a71)
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 = 1500;
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    /** 用户输入 */
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: 1500})).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: 1500})).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    ): Promise<IAlbum.IAlbumItem | null> {
483        if (!this.plugin.instance.getAlbumInfo) {
484            return {...albumItem, musicList: []};
485        }
486        try {
487            const result = await this.plugin.instance.getAlbumInfo(
488                resetMediaItem(albumItem, undefined, true),
489            );
490            if (!result) {
491                throw new Error();
492            }
493            result?.musicList?.forEach(_ => {
494                resetMediaItem(_, this.plugin.name);
495            });
496
497            return {...albumItem, ...result};
498        } catch (e: any) {
499            trace('获取专辑信息失败', e?.message);
500            devLog('error', '获取专辑信息失败', e, e?.message);
501
502            return {...albumItem, musicList: []};
503        }
504    }
505
506    /** 查询作者信息 */
507    async getArtistWorks<T extends IArtist.ArtistMediaType>(
508        artistItem: IArtist.IArtistItem,
509        page: number,
510        type: T,
511    ): Promise<IPlugin.ISearchResult<T>> {
512        if (!this.plugin.instance.getArtistWorks) {
513            return {
514                isEnd: true,
515                data: [],
516            };
517        }
518        try {
519            const result = await this.plugin.instance.getArtistWorks(
520                artistItem,
521                page,
522                type,
523            );
524            if (!result.data) {
525                return {
526                    isEnd: true,
527                    data: [],
528                };
529            }
530            result.data?.forEach(_ => resetMediaItem(_, this.plugin.name));
531            return {
532                isEnd: result.isEnd ?? true,
533                data: result.data,
534            };
535        } catch (e: any) {
536            trace('查询作者信息失败', e?.message);
537            devLog('error', '查询作者信息失败', e, e?.message);
538
539            throw e;
540        }
541    }
542
543    /** 导入歌单 */
544    async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> {
545        try {
546            const result =
547                (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? [];
548            result.forEach(_ => resetMediaItem(_, this.plugin.name));
549            return result;
550        } catch (e: any) {
551            console.log(e);
552            devLog('error', '导入歌单失败', e, e?.message);
553
554            return [];
555        }
556    }
557    /** 导入单曲 */
558    async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> {
559        try {
560            const result = await this.plugin.instance?.importMusicItem?.(
561                urlLike,
562            );
563            if (!result) {
564                throw new Error();
565            }
566            resetMediaItem(result, this.plugin.name);
567            return result;
568        } catch (e: any) {
569            devLog('error', '导入单曲失败', e, e?.message);
570
571            return null;
572        }
573    }
574    /** 获取榜单 */
575    async getTopLists(): Promise<IMusic.IMusicTopListGroupItem[]> {
576        try {
577            const result = await this.plugin.instance?.getTopLists?.();
578            if (!result) {
579                throw new Error();
580            }
581            return result;
582        } catch (e: any) {
583            devLog('error', '获取榜单失败', e, e?.message);
584            return [];
585        }
586    }
587    /** 获取榜单详情 */
588    async getTopListDetail(
589        topListItem: IMusic.IMusicTopListItem,
590    ): Promise<ICommon.WithMusicList<IMusic.IMusicTopListItem>> {
591        try {
592            const result = await this.plugin.instance?.getTopListDetail?.(
593                topListItem,
594            );
595            if (!result) {
596                throw new Error();
597            }
598            if (result.musicList) {
599                result.musicList.forEach(_ =>
600                    resetMediaItem(_, this.plugin.name),
601                );
602            }
603            return result;
604        } catch (e: any) {
605            devLog('error', '获取榜单详情失败', e, e?.message);
606            return {
607                ...topListItem,
608                musicList: [],
609            };
610        }
611    }
612}
613//#endregion
614
615let plugins: Array<Plugin> = [];
616const pluginStateMapper = new StateMapper(() => plugins);
617
618//#region 本地音乐插件
619/** 本地插件 */
620const localFilePlugin = new Plugin(function () {
621    return {
622        platform: localPluginPlatform,
623        _path: '',
624        async getMusicInfo(musicBase) {
625            const localPath = getInternalData<string>(
626                musicBase,
627                InternalDataType.LOCALPATH,
628            );
629            if (localPath) {
630                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
631                return {
632                    artwork: coverImg,
633                };
634            }
635            return null;
636        },
637        async getLyric(musicBase) {
638            const localPath = getInternalData<string>(
639                musicBase,
640                InternalDataType.LOCALPATH,
641            );
642            let rawLrc: string | null = null;
643            if (localPath) {
644                // 读取内嵌歌词
645                try {
646                    rawLrc = await Mp3Util.getLyric(localPath);
647                } catch (e) {
648                    console.log('e', e);
649                }
650                if (!rawLrc) {
651                    // 读取配置歌词
652                    const lastDot = localPath.lastIndexOf('.');
653                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
654
655                    try {
656                        if (await exists(lrcPath)) {
657                            rawLrc = await readFile(lrcPath, 'utf8');
658                        }
659                    } catch {}
660                }
661            }
662
663            return rawLrc
664                ? {
665                      rawLrc,
666                  }
667                : null;
668        },
669    };
670}, '');
671localFilePlugin.hash = localPluginHash;
672
673//#endregion
674
675async function setup() {
676    const _plugins: Array<Plugin> = [];
677    try {
678        // 加载插件
679        const pluginsPaths = await readDir(pathConst.pluginPath);
680        for (let i = 0; i < pluginsPaths.length; ++i) {
681            const _pluginUrl = pluginsPaths[i];
682            trace('初始化插件', _pluginUrl);
683            if (
684                _pluginUrl.isFile() &&
685                (_pluginUrl.name?.endsWith?.('.js') ||
686                    _pluginUrl.path?.endsWith?.('.js'))
687            ) {
688                const funcCode = await readFile(_pluginUrl.path, 'utf8');
689                const plugin = new Plugin(funcCode, _pluginUrl.path);
690                const _pluginIndex = _plugins.findIndex(
691                    p => p.hash === plugin.hash,
692                );
693                if (_pluginIndex !== -1) {
694                    // 重复插件,直接忽略
695                    return;
696                }
697                plugin.hash !== '' && _plugins.push(plugin);
698            }
699        }
700
701        plugins = _plugins;
702        pluginStateMapper.notify();
703        /** 初始化meta信息 */
704        PluginMeta.setupMeta(plugins.map(_ => _.name));
705    } catch (e: any) {
706        ToastAndroid.show(
707            `插件初始化失败:${e?.message ?? e}`,
708            ToastAndroid.LONG,
709        );
710        errorLog('插件初始化失败', e?.message);
711        throw e;
712    }
713}
714
715// 安装插件
716async function installPlugin(pluginPath: string) {
717    // if (pluginPath.endsWith('.js')) {
718    const funcCode = await readFile(pluginPath, 'utf8');
719    const plugin = new Plugin(funcCode, pluginPath);
720    const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
721    if (_pluginIndex !== -1) {
722        throw new Error('插件已安装');
723    }
724    if (plugin.hash !== '') {
725        const fn = nanoid();
726        const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
727        await copyFile(pluginPath, _pluginPath);
728        plugin.path = _pluginPath;
729        plugins = plugins.concat(plugin);
730        pluginStateMapper.notify();
731        return;
732    }
733    throw new Error('插件无法解析');
734    // }
735    // throw new Error('插件不存在');
736}
737
738async function installPluginFromUrl(url: string) {
739    try {
740        const funcCode = (await axios.get(url)).data;
741        if (funcCode) {
742            const plugin = new Plugin(funcCode, '');
743            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
744            if (_pluginIndex !== -1) {
745                // 静默忽略
746                return;
747            }
748            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
749            if (oldVersionPlugin) {
750                if (
751                    compare(
752                        oldVersionPlugin.instance.version ?? '',
753                        plugin.instance.version ?? '',
754                        '>',
755                    )
756                ) {
757                    throw new Error('已安装更新版本的插件');
758                }
759            }
760
761            if (plugin.hash !== '') {
762                const fn = nanoid();
763                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
764                await writeFile(_pluginPath, funcCode, 'utf8');
765                plugin.path = _pluginPath;
766                plugins = plugins.concat(plugin);
767                if (oldVersionPlugin) {
768                    plugins = plugins.filter(
769                        _ => _.hash !== oldVersionPlugin.hash,
770                    );
771                    try {
772                        await unlink(oldVersionPlugin.path);
773                    } catch {}
774                }
775                pluginStateMapper.notify();
776                return;
777            }
778            throw new Error('插件无法解析!');
779        }
780    } catch (e: any) {
781        devLog('error', 'URL安装插件失败', e, e?.message);
782        errorLog('URL安装插件失败', e);
783        throw new Error(e?.message ?? '');
784    }
785}
786
787/** 卸载插件 */
788async function uninstallPlugin(hash: string) {
789    const targetIndex = plugins.findIndex(_ => _.hash === hash);
790    if (targetIndex !== -1) {
791        try {
792            const pluginName = plugins[targetIndex].name;
793            await unlink(plugins[targetIndex].path);
794            plugins = plugins.filter(_ => _.hash !== hash);
795            pluginStateMapper.notify();
796            if (plugins.every(_ => _.name !== pluginName)) {
797                await MediaMeta.removePlugin(pluginName);
798            }
799        } catch {}
800    }
801}
802
803async function uninstallAllPlugins() {
804    await Promise.all(
805        plugins.map(async plugin => {
806            try {
807                const pluginName = plugin.name;
808                await unlink(plugin.path);
809                await MediaMeta.removePlugin(pluginName);
810            } catch (e) {}
811        }),
812    );
813    plugins = [];
814    pluginStateMapper.notify();
815
816    /** 清除空余文件,异步做就可以了 */
817    readDir(pathConst.pluginPath)
818        .then(fns => {
819            fns.forEach(fn => {
820                unlink(fn.path).catch(emptyFunction);
821            });
822        })
823        .catch(emptyFunction);
824}
825
826async function updatePlugin(plugin: Plugin) {
827    const updateUrl = plugin.instance.srcUrl;
828    if (!updateUrl) {
829        throw new Error('没有更新源');
830    }
831    try {
832        await installPluginFromUrl(updateUrl);
833    } catch (e: any) {
834        if (e.message === '插件已安装') {
835            throw new Error('当前已是最新版本');
836        } else {
837            throw e;
838        }
839    }
840}
841
842function getByMedia(mediaItem: ICommon.IMediaBase) {
843    return getByName(mediaItem?.platform);
844}
845
846function getByHash(hash: string) {
847    return hash === localPluginHash
848        ? localFilePlugin
849        : plugins.find(_ => _.hash === hash);
850}
851
852function getByName(name: string) {
853    return name === localPluginPlatform
854        ? localFilePlugin
855        : plugins.find(_ => _.name === name);
856}
857
858function getValidPlugins() {
859    return plugins.filter(_ => _.state === 'enabled');
860}
861
862function getSearchablePlugins() {
863    return plugins.filter(_ => _.state === 'enabled' && _.instance.search);
864}
865
866function getSortedSearchablePlugins() {
867    return getSearchablePlugins().sort((a, b) =>
868        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
869            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
870        0
871            ? -1
872            : 1,
873    );
874}
875
876function getTopListsablePlugins() {
877    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
878}
879
880function getSortedTopListsablePlugins() {
881    return getTopListsablePlugins().sort((a, b) =>
882        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
883            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
884        0
885            ? -1
886            : 1,
887    );
888}
889
890function useSortedPlugins() {
891    const _plugins = pluginStateMapper.useMappedState();
892    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
893
894    const [sortedPlugins, setSortedPlugins] = useState(
895        [..._plugins].sort((a, b) =>
896            (_pluginMetaAll[a.name]?.order ?? Infinity) -
897                (_pluginMetaAll[b.name]?.order ?? Infinity) <
898            0
899                ? -1
900                : 1,
901        ),
902    );
903
904    useEffect(() => {
905        InteractionManager.runAfterInteractions(() => {
906            setSortedPlugins(
907                [..._plugins].sort((a, b) =>
908                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
909                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
910                    0
911                        ? -1
912                        : 1,
913                ),
914            );
915        });
916    }, [_plugins, _pluginMetaAll]);
917
918    return sortedPlugins;
919}
920
921const PluginManager = {
922    setup,
923    installPlugin,
924    installPluginFromUrl,
925    updatePlugin,
926    uninstallPlugin,
927    getByMedia,
928    getByHash,
929    getByName,
930    getValidPlugins,
931    getSearchablePlugins,
932    getSortedSearchablePlugins,
933    getTopListsablePlugins,
934    getSortedTopListsablePlugins,
935    usePlugins: pluginStateMapper.useMappedState,
936    useSortedPlugins,
937    uninstallAllPlugins,
938};
939
940export default PluginManager;
941