xref: /MusicFree/src/core/pluginManager.ts (revision 5589cdf32b2bb0f641e5ac7bf1f6152cd6b9b70e)
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 {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 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 (
312            localPath &&
313            (localPath.startsWith('content://') ||
314                (await FileSystem.exists(localPath)))
315        ) {
316            trace('本地播放', localPath);
317            if (mediaExtra && mediaExtra.localPath !== localPath) {
318                // 修正一下本地数据
319                MediaExtra.update(musicItem, {
320                    localPath,
321                });
322            }
323            return {
324                url: addFileScheme(localPath),
325            };
326        } else if (mediaExtra?.localPath) {
327            MediaExtra.update(musicItem, {
328                localPath: undefined,
329            });
330        }
331
332        if (musicItem.platform === localPluginPlatform) {
333            throw new Error('本地音乐不存在');
334        }
335        // 2. 缓存播放
336        const mediaCache = MediaCache.getMediaCache(
337            musicItem,
338        ) as IMusic.IMusicItem | null;
339        const pluginCacheControl =
340            this.plugin.instance.cacheControl ?? 'no-cache';
341        if (
342            mediaCache &&
343            mediaCache?.source?.[quality]?.url &&
344            (pluginCacheControl === CacheControl.Cache ||
345                (pluginCacheControl === CacheControl.NoCache &&
346                    Network.isOffline()))
347        ) {
348            trace('播放', '缓存播放');
349            const qualityInfo = mediaCache.source[quality];
350            return {
351                url: qualityInfo!.url,
352                headers: mediaCache.headers,
353                userAgent:
354                    mediaCache.userAgent ?? mediaCache.headers?.['user-agent'],
355            };
356        }
357        // 3. 插件解析
358        if (!this.plugin.instance.getMediaSource) {
359            const {url, auth} = formatAuthUrl(
360                musicItem?.qualities?.[quality]?.url ?? musicItem.url,
361            );
362            return {
363                url: url,
364                headers: auth
365                    ? {
366                          Authorization: auth,
367                      }
368                    : undefined,
369            };
370        }
371        try {
372            const {url, headers} = (await this.plugin.instance.getMediaSource(
373                musicItem,
374                quality,
375            )) ?? {url: musicItem?.qualities?.[quality]?.url};
376            if (!url) {
377                throw new Error('NOT RETRY');
378            }
379            trace('播放', '插件播放');
380            const result = {
381                url,
382                headers,
383                userAgent: headers?.['user-agent'],
384            } as IPlugin.IMediaSourceResult;
385            const authFormattedResult = formatAuthUrl(result.url!);
386            if (authFormattedResult.auth) {
387                result.url = authFormattedResult.url;
388                result.headers = {
389                    ...(result.headers ?? {}),
390                    Authorization: authFormattedResult.auth,
391                };
392            }
393
394            if (
395                pluginCacheControl !== CacheControl.NoStore &&
396                !notUpdateCache
397            ) {
398                // 更新缓存
399                const cacheSource = {
400                    headers: result.headers,
401                    userAgent: result.userAgent,
402                    url,
403                };
404                let realMusicItem = {
405                    ...musicItem,
406                    ...(mediaCache || {}),
407                };
408                realMusicItem.source = {
409                    ...(realMusicItem.source || {}),
410                    [quality]: cacheSource,
411                };
412
413                MediaCache.setMediaCache(realMusicItem);
414            }
415            return result;
416        } catch (e: any) {
417            if (retryCount > 0 && e?.message !== 'NOT RETRY') {
418                await delay(150);
419                return this.getMediaSource(musicItem, quality, --retryCount);
420            }
421            errorLog('获取真实源失败', e?.message);
422            devLog('error', '获取真实源失败', e, e?.message);
423            return null;
424        }
425    }
426
427    /** 获取音乐详情 */
428    async getMusicInfo(
429        musicItem: ICommon.IMediaBase,
430    ): Promise<Partial<IMusic.IMusicItem> | null> {
431        if (!this.plugin.instance.getMusicInfo) {
432            return null;
433        }
434        try {
435            return (
436                this.plugin.instance.getMusicInfo(
437                    resetMediaItem(musicItem, undefined, true),
438                ) ?? null
439            );
440        } catch (e: any) {
441            devLog('error', '获取音乐详情失败', e, e?.message);
442            return null;
443        }
444    }
445
446    /**
447     *
448     * getLyric(musicItem) => {
449     *      lyric: string;
450     *      trans: string;
451     * }
452     *
453     */
454    /** 获取歌词 */
455    async getLyric(
456        originalMusicItem: IMusic.IMusicItemBase,
457    ): Promise<ILyric.ILyricSource | null> {
458        // 1.额外存储的meta信息(关联歌词)
459        const meta = MediaExtra.get(originalMusicItem);
460        let musicItem: IMusic.IMusicItem;
461        if (meta && meta.associatedLrc) {
462            musicItem = meta.associatedLrc as IMusic.IMusicItem;
463        } else {
464            musicItem = originalMusicItem as IMusic.IMusicItem;
465        }
466
467        const musicItemCache = MediaCache.getMediaCache(
468            musicItem,
469        ) as IMusic.IMusicItemCache | null;
470
471        /** 原始歌词文本 */
472        let rawLrc: string | null = musicItem.rawLrc || null;
473        let translation: string | null = null;
474
475        // 2. 缓存歌词 / 对象上本身的歌词
476        if (musicItemCache?.lyric) {
477            // 缓存的远程结果
478            let cacheLyric: ILyric.ILyricSource | null =
479                musicItemCache.lyric || null;
480            // 缓存的本地结果
481            let localLyric: ILyric.ILyricSource | null =
482                musicItemCache.$localLyric || null;
483
484            // 优先用缓存的结果
485            if (cacheLyric.rawLrc || cacheLyric.translation) {
486                return {
487                    rawLrc: cacheLyric.rawLrc,
488                    translation: cacheLyric.translation,
489                };
490            }
491
492            // 本地其实是缓存的路径
493            if (localLyric) {
494                let needRefetch = false;
495                if (localLyric.rawLrc && (await exists(localLyric.rawLrc))) {
496                    rawLrc = await readFile(localLyric.rawLrc, 'utf8');
497                } else if (localLyric.rawLrc) {
498                    needRefetch = true;
499                }
500                if (
501                    localLyric.translation &&
502                    (await exists(localLyric.translation))
503                ) {
504                    translation = await readFile(
505                        localLyric.translation,
506                        'utf8',
507                    );
508                } else if (localLyric.translation) {
509                    needRefetch = true;
510                }
511
512                if (!needRefetch && (rawLrc || translation)) {
513                    return {
514                        rawLrc: rawLrc || undefined,
515                        translation: translation || undefined,
516                    };
517                }
518            }
519        }
520
521        // 3. 无缓存歌词/无自带歌词/无本地歌词
522        let lrcSource: ILyric.ILyricSource | null;
523        if (isSameMediaItem(originalMusicItem, musicItem)) {
524            lrcSource =
525                (await this.plugin.instance
526                    ?.getLyric?.(resetMediaItem(musicItem, undefined, true))
527                    ?.catch(() => null)) || null;
528        } else {
529            lrcSource =
530                (await PluginManager.getByMedia(musicItem)
531                    ?.instance?.getLyric?.(
532                        resetMediaItem(musicItem, undefined, true),
533                    )
534                    ?.catch(() => null)) || null;
535        }
536
537        if (lrcSource) {
538            rawLrc = lrcSource?.rawLrc || rawLrc;
539            translation = lrcSource?.translation || null;
540
541            const deprecatedLrcUrl = lrcSource?.lrc || musicItem.lrc;
542
543            // 本地的文件名
544            let filename: string | undefined = `${
545                pathConst.lrcCachePath
546            }${nanoid()}.lrc`;
547            let filenameTrans: string | undefined = `${
548                pathConst.lrcCachePath
549            }${nanoid()}.lrc`;
550
551            // 旧版本兼容
552            if (!(rawLrc || translation)) {
553                if (deprecatedLrcUrl) {
554                    rawLrc = (
555                        await axios
556                            .get(deprecatedLrcUrl, {timeout: 3000})
557                            .catch(() => null)
558                    )?.data;
559                } else if (musicItem.rawLrc) {
560                    rawLrc = musicItem.rawLrc;
561                }
562            }
563
564            if (rawLrc) {
565                await writeFile(filename, rawLrc, 'utf8');
566            } else {
567                filename = undefined;
568            }
569            if (translation) {
570                await writeFile(filenameTrans, translation, 'utf8');
571            } else {
572                filenameTrans = undefined;
573            }
574
575            if (rawLrc || translation) {
576                MediaCache.setMediaCache(
577                    produce(musicItemCache || musicItem, draft => {
578                        musicItemCache?.$localLyric?.rawLrc;
579                        objectPath.set(draft, '$localLyric.rawLrc', filename);
580                        objectPath.set(
581                            draft,
582                            '$localLyric.translation',
583                            filenameTrans,
584                        );
585                        return draft;
586                    }),
587                );
588                return {
589                    rawLrc: rawLrc || undefined,
590                    translation: translation || undefined,
591                };
592            }
593        }
594
595        // 6. 如果是本地文件
596        const isDownloaded = LocalMusicSheet.isLocalMusic(originalMusicItem);
597        if (
598            originalMusicItem.platform !== localPluginPlatform &&
599            isDownloaded
600        ) {
601            const res = await localFilePlugin.instance!.getLyric!(isDownloaded);
602
603            console.log('本地文件歌词');
604
605            if (res) {
606                return res;
607            }
608        }
609        devLog('warn', '无歌词');
610
611        return null;
612    }
613
614    /** 获取歌词文本 */
615    async getLyricText(
616        musicItem: IMusic.IMusicItem,
617    ): Promise<string | undefined> {
618        return (await this.getLyric(musicItem))?.rawLrc;
619    }
620
621    /** 获取专辑信息 */
622    async getAlbumInfo(
623        albumItem: IAlbum.IAlbumItemBase,
624        page: number = 1,
625    ): Promise<IPlugin.IAlbumInfoResult | null> {
626        if (!this.plugin.instance.getAlbumInfo) {
627            return {
628                albumItem,
629                musicList: (albumItem?.musicList ?? []).map(
630                    resetMediaItem,
631                    this.plugin.name,
632                    true,
633                ),
634                isEnd: true,
635            };
636        }
637        try {
638            const result = await this.plugin.instance.getAlbumInfo(
639                resetMediaItem(albumItem, undefined, true),
640                page,
641            );
642            if (!result) {
643                throw new Error();
644            }
645            result?.musicList?.forEach(_ => {
646                resetMediaItem(_, this.plugin.name);
647                _.album = albumItem.title;
648            });
649
650            if (page <= 1) {
651                // 合并信息
652                return {
653                    albumItem: {...albumItem, ...(result?.albumItem ?? {})},
654                    isEnd: result.isEnd === false ? false : true,
655                    musicList: result.musicList,
656                };
657            } else {
658                return {
659                    isEnd: result.isEnd === false ? false : true,
660                    musicList: result.musicList,
661                };
662            }
663        } catch (e: any) {
664            trace('获取专辑信息失败', e?.message);
665            devLog('error', '获取专辑信息失败', e, e?.message);
666
667            return null;
668        }
669    }
670
671    /** 获取歌单信息 */
672    async getMusicSheetInfo(
673        sheetItem: IMusic.IMusicSheetItem,
674        page: number = 1,
675    ): Promise<IPlugin.ISheetInfoResult | null> {
676        if (!this.plugin.instance.getMusicSheetInfo) {
677            return {
678                sheetItem,
679                musicList: sheetItem?.musicList ?? [],
680                isEnd: true,
681            };
682        }
683        try {
684            const result = await this.plugin.instance?.getMusicSheetInfo?.(
685                resetMediaItem(sheetItem, undefined, true),
686                page,
687            );
688            if (!result) {
689                throw new Error();
690            }
691            result?.musicList?.forEach(_ => {
692                resetMediaItem(_, this.plugin.name);
693            });
694
695            if (page <= 1) {
696                // 合并信息
697                return {
698                    sheetItem: {...sheetItem, ...(result?.sheetItem ?? {})},
699                    isEnd: result.isEnd === false ? false : true,
700                    musicList: result.musicList,
701                };
702            } else {
703                return {
704                    isEnd: result.isEnd === false ? false : true,
705                    musicList: result.musicList,
706                };
707            }
708        } catch (e: any) {
709            trace('获取歌单信息失败', e, e?.message);
710            devLog('error', '获取歌单信息失败', e, e?.message);
711
712            return null;
713        }
714    }
715
716    /** 查询作者信息 */
717    async getArtistWorks<T extends IArtist.ArtistMediaType>(
718        artistItem: IArtist.IArtistItem,
719        page: number,
720        type: T,
721    ): Promise<IPlugin.ISearchResult<T>> {
722        if (!this.plugin.instance.getArtistWorks) {
723            return {
724                isEnd: true,
725                data: [],
726            };
727        }
728        try {
729            const result = await this.plugin.instance.getArtistWorks(
730                artistItem,
731                page,
732                type,
733            );
734            if (!result.data) {
735                return {
736                    isEnd: true,
737                    data: [],
738                };
739            }
740            result.data?.forEach(_ => resetMediaItem(_, this.plugin.name));
741            return {
742                isEnd: result.isEnd ?? true,
743                data: result.data,
744            };
745        } catch (e: any) {
746            trace('查询作者信息失败', e?.message);
747            devLog('error', '查询作者信息失败', e, e?.message);
748
749            throw e;
750        }
751    }
752
753    /** 导入歌单 */
754    async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> {
755        try {
756            const result =
757                (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? [];
758            result.forEach(_ => resetMediaItem(_, this.plugin.name));
759            return result;
760        } catch (e: any) {
761            console.log(e);
762            devLog('error', '导入歌单失败', e, e?.message);
763
764            return [];
765        }
766    }
767    /** 导入单曲 */
768    async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> {
769        try {
770            const result = await this.plugin.instance?.importMusicItem?.(
771                urlLike,
772            );
773            if (!result) {
774                throw new Error();
775            }
776            resetMediaItem(result, this.plugin.name);
777            return result;
778        } catch (e: any) {
779            devLog('error', '导入单曲失败', e, e?.message);
780
781            return null;
782        }
783    }
784    /** 获取榜单 */
785    async getTopLists(): Promise<IMusic.IMusicSheetGroupItem[]> {
786        try {
787            const result = await this.plugin.instance?.getTopLists?.();
788            if (!result) {
789                throw new Error();
790            }
791            return result;
792        } catch (e: any) {
793            devLog('error', '获取榜单失败', e, e?.message);
794            return [];
795        }
796    }
797    /** 获取榜单详情 */
798    async getTopListDetail(
799        topListItem: IMusic.IMusicSheetItemBase,
800        page: number,
801    ): Promise<IPlugin.ITopListInfoResult> {
802        try {
803            const result = await this.plugin.instance?.getTopListDetail?.(
804                topListItem,
805                page,
806            );
807            if (!result) {
808                throw new Error();
809            }
810            if (result.musicList) {
811                result.musicList.forEach(_ =>
812                    resetMediaItem(_, this.plugin.name),
813                );
814            }
815            if (result.isEnd !== false) {
816                result.isEnd = true;
817            }
818            return result;
819        } catch (e: any) {
820            devLog('error', '获取榜单详情失败', e, e?.message);
821            return {
822                isEnd: true,
823                topListItem: topListItem as IMusic.IMusicSheetItem,
824                musicList: [],
825            };
826        }
827    }
828
829    /** 获取推荐歌单的tag */
830    async getRecommendSheetTags(): Promise<IPlugin.IGetRecommendSheetTagsResult> {
831        try {
832            const result =
833                await this.plugin.instance?.getRecommendSheetTags?.();
834            if (!result) {
835                throw new Error();
836            }
837            return result;
838        } catch (e: any) {
839            devLog('error', '获取推荐歌单失败', e, e?.message);
840            return {
841                data: [],
842            };
843        }
844    }
845    /** 获取某个tag的推荐歌单 */
846    async getRecommendSheetsByTag(
847        tagItem: ICommon.IUnique,
848        page?: number,
849    ): Promise<ICommon.PaginationResponse<IMusic.IMusicSheetItemBase>> {
850        try {
851            const result =
852                await this.plugin.instance?.getRecommendSheetsByTag?.(
853                    tagItem,
854                    page ?? 1,
855                );
856            if (!result) {
857                throw new Error();
858            }
859            if (result.isEnd !== false) {
860                result.isEnd = true;
861            }
862            if (!result.data) {
863                result.data = [];
864            }
865            result.data.forEach(item => resetMediaItem(item, this.plugin.name));
866
867            return result;
868        } catch (e: any) {
869            devLog('error', '获取推荐歌单详情失败', e, e?.message);
870            return {
871                isEnd: true,
872                data: [],
873            };
874        }
875    }
876}
877//#endregion
878
879let plugins: Array<Plugin> = [];
880const pluginStateMapper = new StateMapper(() => plugins);
881
882//#region 本地音乐插件
883/** 本地插件 */
884const localFilePlugin = new Plugin(function () {
885    return {
886        platform: localPluginPlatform,
887        _path: '',
888        async getMusicInfo(musicBase) {
889            const localPath = getInternalData<string>(
890                musicBase,
891                InternalDataType.LOCALPATH,
892            );
893            if (localPath) {
894                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
895                return {
896                    artwork: coverImg,
897                };
898            }
899            return null;
900        },
901        async getLyric(musicBase) {
902            const localPath = getInternalData<string>(
903                musicBase,
904                InternalDataType.LOCALPATH,
905            );
906            let rawLrc: string | null = null;
907            if (localPath) {
908                // 读取内嵌歌词
909                try {
910                    rawLrc = await Mp3Util.getLyric(localPath);
911                } catch (e) {
912                    console.log('读取内嵌歌词失败', e);
913                }
914                if (!rawLrc) {
915                    // 读取配置歌词
916                    const lastDot = localPath.lastIndexOf('.');
917                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
918
919                    try {
920                        if (await exists(lrcPath)) {
921                            rawLrc = await readFile(lrcPath, 'utf8');
922                        }
923                    } catch {}
924                }
925            }
926
927            return rawLrc
928                ? {
929                      rawLrc,
930                  }
931                : null;
932        },
933        async importMusicItem(urlLike) {
934            let meta: any = {};
935            try {
936                meta = await Mp3Util.getBasicMeta(urlLike);
937            } catch {}
938            const id = await FileSystem.hash(urlLike, 'MD5');
939            return {
940                id: id,
941                platform: '本地',
942                title: meta?.title ?? getFileName(urlLike),
943                artist: meta?.artist ?? '未知歌手',
944                duration: parseInt(meta?.duration ?? '0') / 1000,
945                album: meta?.album ?? '未知专辑',
946                artwork: '',
947                [internalSerializeKey]: {
948                    localPath: urlLike,
949                },
950            };
951        },
952        async getMediaSource(musicItem, quality) {
953            if (quality === 'standard') {
954                return {
955                    url: addFileScheme(musicItem.$?.localPath || musicItem.url),
956                };
957            }
958            return null;
959        },
960    };
961}, '');
962localFilePlugin.hash = localPluginHash;
963
964//#endregion
965
966async function setup() {
967    const _plugins: Array<Plugin> = [];
968    try {
969        // 加载插件
970        const pluginsPaths = await readDir(pathConst.pluginPath);
971        for (let i = 0; i < pluginsPaths.length; ++i) {
972            const _pluginUrl = pluginsPaths[i];
973            trace('初始化插件', _pluginUrl);
974            if (
975                _pluginUrl.isFile() &&
976                (_pluginUrl.name?.endsWith?.('.js') ||
977                    _pluginUrl.path?.endsWith?.('.js'))
978            ) {
979                const funcCode = await readFile(_pluginUrl.path, 'utf8');
980                const plugin = new Plugin(funcCode, _pluginUrl.path);
981                const _pluginIndex = _plugins.findIndex(
982                    p => p.hash === plugin.hash,
983                );
984                if (_pluginIndex !== -1) {
985                    // 重复插件,直接忽略
986                    continue;
987                }
988                plugin.hash !== '' && _plugins.push(plugin);
989            }
990        }
991
992        plugins = _plugins;
993        /** 初始化meta信息 */
994        await PluginMeta.setupMeta(plugins.map(_ => _.name));
995        /** 查看一下是否有禁用的标记 */
996        const allMeta = PluginMeta.getPluginMetaAll() ?? {};
997        for (let plugin of plugins) {
998            if (allMeta[plugin.name]?.enabled === false) {
999                plugin.state = 'disabled';
1000            }
1001        }
1002        pluginStateMapper.notify();
1003    } catch (e: any) {
1004        ToastAndroid.show(
1005            `插件初始化失败:${e?.message ?? e}`,
1006            ToastAndroid.LONG,
1007        );
1008        errorLog('插件初始化失败', e?.message);
1009        throw e;
1010    }
1011}
1012
1013interface IInstallPluginConfig {
1014    notCheckVersion?: boolean;
1015}
1016
1017// 安装插件
1018async function installPlugin(
1019    pluginPath: string,
1020    config?: IInstallPluginConfig,
1021) {
1022    // if (pluginPath.endsWith('.js')) {
1023    const funcCode = await readFile(pluginPath, 'utf8');
1024
1025    if (funcCode) {
1026        const plugin = new Plugin(funcCode, pluginPath);
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 copyFile(pluginPath, _pluginPath);
1055            plugin.path = _pluginPath;
1056            plugins = plugins.concat(plugin);
1057            pluginStateMapper.notify();
1058            return plugin;
1059        }
1060        throw new Error('插件无法解析!');
1061    }
1062    throw new Error('插件无法识别!');
1063}
1064
1065const reqHeaders = {
1066    'Cache-Control': 'no-cache',
1067    Pragma: 'no-cache',
1068    Expires: '0',
1069};
1070
1071async function installPluginFromUrl(
1072    url: string,
1073    config?: IInstallPluginConfig,
1074) {
1075    try {
1076        const funcCode = (
1077            await axios.get(url, {
1078                headers: reqHeaders,
1079            })
1080        ).data;
1081        if (funcCode) {
1082            const plugin = new Plugin(funcCode, '');
1083            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1084            if (_pluginIndex !== -1) {
1085                // 静默忽略
1086                return;
1087            }
1088            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1089            if (oldVersionPlugin && !config?.notCheckVersion) {
1090                if (
1091                    compare(
1092                        oldVersionPlugin.instance.version ?? '',
1093                        plugin.instance.version ?? '',
1094                        '>',
1095                    )
1096                ) {
1097                    throw new Error('已安装更新版本的插件');
1098                }
1099            }
1100
1101            if (plugin.hash !== '') {
1102                const fn = nanoid();
1103                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1104                await writeFile(_pluginPath, funcCode, 'utf8');
1105                plugin.path = _pluginPath;
1106                plugins = plugins.concat(plugin);
1107                if (oldVersionPlugin) {
1108                    plugins = plugins.filter(
1109                        _ => _.hash !== oldVersionPlugin.hash,
1110                    );
1111                    try {
1112                        await unlink(oldVersionPlugin.path);
1113                    } catch {}
1114                }
1115                pluginStateMapper.notify();
1116                return;
1117            }
1118            throw new Error('插件无法解析!');
1119        }
1120    } catch (e: any) {
1121        devLog('error', 'URL安装插件失败', e, e?.message);
1122        errorLog('URL安装插件失败', e);
1123        throw new Error(e?.message ?? '');
1124    }
1125}
1126
1127/** 卸载插件 */
1128async function uninstallPlugin(hash: string) {
1129    const targetIndex = plugins.findIndex(_ => _.hash === hash);
1130    if (targetIndex !== -1) {
1131        try {
1132            const pluginName = plugins[targetIndex].name;
1133            await unlink(plugins[targetIndex].path);
1134            plugins = plugins.filter(_ => _.hash !== hash);
1135            pluginStateMapper.notify();
1136            // 防止其他重名
1137            if (plugins.every(_ => _.name !== pluginName)) {
1138                MediaExtra.removeAll(pluginName);
1139            }
1140        } catch {}
1141    }
1142}
1143
1144async function uninstallAllPlugins() {
1145    await Promise.all(
1146        plugins.map(async plugin => {
1147            try {
1148                const pluginName = plugin.name;
1149                await unlink(plugin.path);
1150                MediaExtra.removeAll(pluginName);
1151            } catch (e) {}
1152        }),
1153    );
1154    plugins = [];
1155    pluginStateMapper.notify();
1156
1157    /** 清除空余文件,异步做就可以了 */
1158    readDir(pathConst.pluginPath)
1159        .then(fns => {
1160            fns.forEach(fn => {
1161                unlink(fn.path).catch(emptyFunction);
1162            });
1163        })
1164        .catch(emptyFunction);
1165}
1166
1167async function updatePlugin(plugin: Plugin) {
1168    const updateUrl = plugin.instance.srcUrl;
1169    if (!updateUrl) {
1170        throw new Error('没有更新源');
1171    }
1172    try {
1173        await installPluginFromUrl(updateUrl);
1174    } catch (e: any) {
1175        if (e.message === '插件已安装') {
1176            throw new Error('当前已是最新版本');
1177        } else {
1178            throw e;
1179        }
1180    }
1181}
1182
1183function getByMedia(mediaItem: ICommon.IMediaBase) {
1184    return getByName(mediaItem?.platform);
1185}
1186
1187function getByHash(hash: string) {
1188    return hash === localPluginHash
1189        ? localFilePlugin
1190        : plugins.find(_ => _.hash === hash);
1191}
1192
1193function getByName(name: string) {
1194    return name === localPluginPlatform
1195        ? localFilePlugin
1196        : plugins.find(_ => _.name === name);
1197}
1198
1199function getValidPlugins() {
1200    return plugins.filter(_ => _.state === 'enabled');
1201}
1202
1203function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) {
1204    return plugins.filter(
1205        _ =>
1206            _.state === 'enabled' &&
1207            _.instance.search &&
1208            (supportedSearchType && _.instance.supportedSearchType
1209                ? _.instance.supportedSearchType.includes(supportedSearchType)
1210                : true),
1211    );
1212}
1213
1214function getSortedSearchablePlugins(
1215    supportedSearchType?: ICommon.SupportMediaType,
1216) {
1217    return getSearchablePlugins(supportedSearchType).sort((a, b) =>
1218        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1219            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1220        0
1221            ? -1
1222            : 1,
1223    );
1224}
1225
1226function getTopListsablePlugins() {
1227    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
1228}
1229
1230function getSortedTopListsablePlugins() {
1231    return getTopListsablePlugins().sort((a, b) =>
1232        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1233            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1234        0
1235            ? -1
1236            : 1,
1237    );
1238}
1239
1240function getRecommendSheetablePlugins() {
1241    return plugins.filter(
1242        _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag,
1243    );
1244}
1245
1246function getSortedRecommendSheetablePlugins() {
1247    return getRecommendSheetablePlugins().sort((a, b) =>
1248        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1249            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1250        0
1251            ? -1
1252            : 1,
1253    );
1254}
1255
1256function useSortedPlugins() {
1257    const _plugins = pluginStateMapper.useMappedState();
1258    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
1259
1260    const [sortedPlugins, setSortedPlugins] = useState(
1261        [..._plugins].sort((a, b) =>
1262            (_pluginMetaAll[a.name]?.order ?? Infinity) -
1263                (_pluginMetaAll[b.name]?.order ?? Infinity) <
1264            0
1265                ? -1
1266                : 1,
1267        ),
1268    );
1269
1270    useEffect(() => {
1271        InteractionManager.runAfterInteractions(() => {
1272            setSortedPlugins(
1273                [..._plugins].sort((a, b) =>
1274                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
1275                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
1276                    0
1277                        ? -1
1278                        : 1,
1279                ),
1280            );
1281        });
1282    }, [_plugins, _pluginMetaAll]);
1283
1284    return sortedPlugins;
1285}
1286
1287async function setPluginEnabled(plugin: Plugin, enabled?: boolean) {
1288    const target = plugins.find(it => it.hash === plugin.hash);
1289    if (target) {
1290        target.state = enabled ? 'enabled' : 'disabled';
1291        plugins = [...plugins];
1292        pluginStateMapper.notify();
1293        PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled);
1294    }
1295}
1296
1297const PluginManager = {
1298    setup,
1299    installPlugin,
1300    installPluginFromUrl,
1301    updatePlugin,
1302    uninstallPlugin,
1303    getByMedia,
1304    getByHash,
1305    getByName,
1306    getValidPlugins,
1307    getSearchablePlugins,
1308    getSortedSearchablePlugins,
1309    getTopListsablePlugins,
1310    getSortedRecommendSheetablePlugins,
1311    getSortedTopListsablePlugins,
1312    usePlugins: pluginStateMapper.useMappedState,
1313    useSortedPlugins,
1314    uninstallAllPlugins,
1315    setPluginEnabled,
1316};
1317
1318export default PluginManager;
1319