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