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