xref: /MusicFree/src/core/pluginManager.ts (revision 734113be9d256a2b4d36bb272d6d3565beaeb236)
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.IMusicTopListGroupItem[]> {
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.IMusicTopListItem,
609    ): Promise<ICommon.WithMusicList<IMusic.IMusicTopListItem>> {
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//#endregion
633
634let plugins: Array<Plugin> = [];
635const pluginStateMapper = new StateMapper(() => plugins);
636
637//#region 本地音乐插件
638/** 本地插件 */
639const localFilePlugin = new Plugin(function () {
640    return {
641        platform: localPluginPlatform,
642        _path: '',
643        async getMusicInfo(musicBase) {
644            const localPath = getInternalData<string>(
645                musicBase,
646                InternalDataType.LOCALPATH,
647            );
648            if (localPath) {
649                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
650                return {
651                    artwork: coverImg,
652                };
653            }
654            return null;
655        },
656        async getLyric(musicBase) {
657            const localPath = getInternalData<string>(
658                musicBase,
659                InternalDataType.LOCALPATH,
660            );
661            let rawLrc: string | null = null;
662            if (localPath) {
663                // 读取内嵌歌词
664                try {
665                    rawLrc = await Mp3Util.getLyric(localPath);
666                } catch (e) {
667                    console.log('e', e);
668                }
669                if (!rawLrc) {
670                    // 读取配置歌词
671                    const lastDot = localPath.lastIndexOf('.');
672                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
673
674                    try {
675                        if (await exists(lrcPath)) {
676                            rawLrc = await readFile(lrcPath, 'utf8');
677                        }
678                    } catch {}
679                }
680            }
681
682            return rawLrc
683                ? {
684                      rawLrc,
685                  }
686                : null;
687        },
688    };
689}, '');
690localFilePlugin.hash = localPluginHash;
691
692//#endregion
693
694async function setup() {
695    const _plugins: Array<Plugin> = [];
696    try {
697        // 加载插件
698        const pluginsPaths = await readDir(pathConst.pluginPath);
699        for (let i = 0; i < pluginsPaths.length; ++i) {
700            const _pluginUrl = pluginsPaths[i];
701            trace('初始化插件', _pluginUrl);
702            if (
703                _pluginUrl.isFile() &&
704                (_pluginUrl.name?.endsWith?.('.js') ||
705                    _pluginUrl.path?.endsWith?.('.js'))
706            ) {
707                const funcCode = await readFile(_pluginUrl.path, 'utf8');
708                const plugin = new Plugin(funcCode, _pluginUrl.path);
709                const _pluginIndex = _plugins.findIndex(
710                    p => p.hash === plugin.hash,
711                );
712                if (_pluginIndex !== -1) {
713                    // 重复插件,直接忽略
714                    return;
715                }
716                plugin.hash !== '' && _plugins.push(plugin);
717            }
718        }
719
720        plugins = _plugins;
721        pluginStateMapper.notify();
722        /** 初始化meta信息 */
723        PluginMeta.setupMeta(plugins.map(_ => _.name));
724    } catch (e: any) {
725        ToastAndroid.show(
726            `插件初始化失败:${e?.message ?? e}`,
727            ToastAndroid.LONG,
728        );
729        errorLog('插件初始化失败', e?.message);
730        throw e;
731    }
732}
733
734// 安装插件
735async function installPlugin(pluginPath: string) {
736    // if (pluginPath.endsWith('.js')) {
737    const funcCode = await readFile(pluginPath, 'utf8');
738    const plugin = new Plugin(funcCode, pluginPath);
739    const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
740    if (_pluginIndex !== -1) {
741        throw new Error('插件已安装');
742    }
743    if (plugin.hash !== '') {
744        const fn = nanoid();
745        const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
746        await copyFile(pluginPath, _pluginPath);
747        plugin.path = _pluginPath;
748        plugins = plugins.concat(plugin);
749        pluginStateMapper.notify();
750        return;
751    }
752    throw new Error('插件无法解析');
753    // }
754    // throw new Error('插件不存在');
755}
756
757async function installPluginFromUrl(url: string) {
758    try {
759        const funcCode = (await axios.get(url)).data;
760        if (funcCode) {
761            const plugin = new Plugin(funcCode, '');
762            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
763            if (_pluginIndex !== -1) {
764                // 静默忽略
765                return;
766            }
767            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
768            if (oldVersionPlugin) {
769                if (
770                    compare(
771                        oldVersionPlugin.instance.version ?? '',
772                        plugin.instance.version ?? '',
773                        '>',
774                    )
775                ) {
776                    throw new Error('已安装更新版本的插件');
777                }
778            }
779
780            if (plugin.hash !== '') {
781                const fn = nanoid();
782                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
783                await writeFile(_pluginPath, funcCode, 'utf8');
784                plugin.path = _pluginPath;
785                plugins = plugins.concat(plugin);
786                if (oldVersionPlugin) {
787                    plugins = plugins.filter(
788                        _ => _.hash !== oldVersionPlugin.hash,
789                    );
790                    try {
791                        await unlink(oldVersionPlugin.path);
792                    } catch {}
793                }
794                pluginStateMapper.notify();
795                return;
796            }
797            throw new Error('插件无法解析!');
798        }
799    } catch (e: any) {
800        devLog('error', 'URL安装插件失败', e, e?.message);
801        errorLog('URL安装插件失败', e);
802        throw new Error(e?.message ?? '');
803    }
804}
805
806/** 卸载插件 */
807async function uninstallPlugin(hash: string) {
808    const targetIndex = plugins.findIndex(_ => _.hash === hash);
809    if (targetIndex !== -1) {
810        try {
811            const pluginName = plugins[targetIndex].name;
812            await unlink(plugins[targetIndex].path);
813            plugins = plugins.filter(_ => _.hash !== hash);
814            pluginStateMapper.notify();
815            if (plugins.every(_ => _.name !== pluginName)) {
816                await MediaMeta.removePlugin(pluginName);
817            }
818        } catch {}
819    }
820}
821
822async function uninstallAllPlugins() {
823    await Promise.all(
824        plugins.map(async plugin => {
825            try {
826                const pluginName = plugin.name;
827                await unlink(plugin.path);
828                await MediaMeta.removePlugin(pluginName);
829            } catch (e) {}
830        }),
831    );
832    plugins = [];
833    pluginStateMapper.notify();
834
835    /** 清除空余文件,异步做就可以了 */
836    readDir(pathConst.pluginPath)
837        .then(fns => {
838            fns.forEach(fn => {
839                unlink(fn.path).catch(emptyFunction);
840            });
841        })
842        .catch(emptyFunction);
843}
844
845async function updatePlugin(plugin: Plugin) {
846    const updateUrl = plugin.instance.srcUrl;
847    if (!updateUrl) {
848        throw new Error('没有更新源');
849    }
850    try {
851        await installPluginFromUrl(updateUrl);
852    } catch (e: any) {
853        if (e.message === '插件已安装') {
854            throw new Error('当前已是最新版本');
855        } else {
856            throw e;
857        }
858    }
859}
860
861function getByMedia(mediaItem: ICommon.IMediaBase) {
862    return getByName(mediaItem?.platform);
863}
864
865function getByHash(hash: string) {
866    return hash === localPluginHash
867        ? localFilePlugin
868        : plugins.find(_ => _.hash === hash);
869}
870
871function getByName(name: string) {
872    return name === localPluginPlatform
873        ? localFilePlugin
874        : plugins.find(_ => _.name === name);
875}
876
877function getValidPlugins() {
878    return plugins.filter(_ => _.state === 'enabled');
879}
880
881function getSearchablePlugins() {
882    return plugins.filter(_ => _.state === 'enabled' && _.instance.search);
883}
884
885function getSortedSearchablePlugins() {
886    return getSearchablePlugins().sort((a, b) =>
887        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
888            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
889        0
890            ? -1
891            : 1,
892    );
893}
894
895function getTopListsablePlugins() {
896    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
897}
898
899function getSortedTopListsablePlugins() {
900    return getTopListsablePlugins().sort((a, b) =>
901        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
902            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
903        0
904            ? -1
905            : 1,
906    );
907}
908
909function useSortedPlugins() {
910    const _plugins = pluginStateMapper.useMappedState();
911    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
912
913    const [sortedPlugins, setSortedPlugins] = useState(
914        [..._plugins].sort((a, b) =>
915            (_pluginMetaAll[a.name]?.order ?? Infinity) -
916                (_pluginMetaAll[b.name]?.order ?? Infinity) <
917            0
918                ? -1
919                : 1,
920        ),
921    );
922
923    useEffect(() => {
924        InteractionManager.runAfterInteractions(() => {
925            setSortedPlugins(
926                [..._plugins].sort((a, b) =>
927                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
928                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
929                    0
930                        ? -1
931                        : 1,
932                ),
933            );
934        });
935    }, [_plugins, _pluginMetaAll]);
936
937    return sortedPlugins;
938}
939
940const PluginManager = {
941    setup,
942    installPlugin,
943    installPluginFromUrl,
944    updatePlugin,
945    uninstallPlugin,
946    getByMedia,
947    getByHash,
948    getByName,
949    getValidPlugins,
950    getSearchablePlugins,
951    getSortedSearchablePlugins,
952    getTopListsablePlugins,
953    getSortedTopListsablePlugins,
954    usePlugins: pluginStateMapper.useMappedState,
955    useSortedPlugins,
956    uninstallAllPlugins,
957};
958
959export default PluginManager;
960