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