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