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