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