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