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