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