xref: /MusicFree/src/core/pluginManager.ts (revision da2a2959bbf8b96423ead2452a5f60867d05a8b3)
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    async getMusicComments(
874        musicItem: IMusic.IMusicItem,
875    ): Promise<ICommon.PaginationResponse<IMedia.IComment>> {
876        const result = await this.plugin.instance?.getMusicComments?.(
877            musicItem,
878        );
879        if (!result) {
880            throw new Error();
881        }
882        if (result.isEnd !== false) {
883            result.isEnd = true;
884        }
885        if (!result.data) {
886            result.data = [];
887        }
888
889        return result;
890    }
891}
892//#endregion
893
894let plugins: Array<Plugin> = [];
895const pluginStateMapper = new StateMapper(() => plugins);
896
897//#region 本地音乐插件
898/** 本地插件 */
899const localFilePlugin = new Plugin(function () {
900    return {
901        platform: localPluginPlatform,
902        _path: '',
903        async getMusicInfo(musicBase) {
904            const localPath = getInternalData<string>(
905                musicBase,
906                InternalDataType.LOCALPATH,
907            );
908            if (localPath) {
909                const coverImg = await Mp3Util.getMediaCoverImg(localPath);
910                return {
911                    artwork: coverImg,
912                };
913            }
914            return null;
915        },
916        async getLyric(musicBase) {
917            const localPath = getInternalData<string>(
918                musicBase,
919                InternalDataType.LOCALPATH,
920            );
921            let rawLrc: string | null = null;
922            if (localPath) {
923                // 读取内嵌歌词
924                try {
925                    rawLrc = await Mp3Util.getLyric(localPath);
926                } catch (e) {
927                    console.log('读取内嵌歌词失败', e);
928                }
929                if (!rawLrc) {
930                    // 读取配置歌词
931                    const lastDot = localPath.lastIndexOf('.');
932                    const lrcPath = localPath.slice(0, lastDot) + '.lrc';
933
934                    try {
935                        if (await exists(lrcPath)) {
936                            rawLrc = await readFile(lrcPath, 'utf8');
937                        }
938                    } catch {}
939                }
940            }
941
942            return rawLrc
943                ? {
944                      rawLrc,
945                  }
946                : null;
947        },
948        async importMusicItem(urlLike) {
949            let meta: any = {};
950            try {
951                meta = await Mp3Util.getBasicMeta(urlLike);
952            } catch {}
953            const stat = await getInfoAsync(urlLike, {
954                md5: true,
955            });
956            let id: string;
957            if (stat.exists) {
958                id = stat.md5 || nanoid();
959            } else {
960                id = nanoid();
961            }
962            return {
963                id: id,
964                platform: '本地',
965                title: meta?.title ?? getFileName(urlLike),
966                artist: meta?.artist ?? '未知歌手',
967                duration: parseInt(meta?.duration ?? '0', 10) / 1000,
968                album: meta?.album ?? '未知专辑',
969                artwork: '',
970                [internalSerializeKey]: {
971                    localPath: urlLike,
972                },
973            };
974        },
975        async getMediaSource(musicItem, quality) {
976            if (quality === 'standard') {
977                return {
978                    url: addFileScheme(musicItem.$?.localPath || musicItem.url),
979                };
980            }
981            return null;
982        },
983    };
984}, '');
985localFilePlugin.hash = localPluginHash;
986
987//#endregion
988
989async function setup() {
990    const _plugins: Array<Plugin> = [];
991    try {
992        // 加载插件
993        const pluginsPaths = await readDir(pathConst.pluginPath);
994        for (let i = 0; i < pluginsPaths.length; ++i) {
995            const _pluginUrl = pluginsPaths[i];
996            trace('初始化插件', _pluginUrl);
997            if (
998                _pluginUrl.isFile() &&
999                (_pluginUrl.name?.endsWith?.('.js') ||
1000                    _pluginUrl.path?.endsWith?.('.js'))
1001            ) {
1002                const funcCode = await readFile(_pluginUrl.path, 'utf8');
1003                const plugin = new Plugin(funcCode, _pluginUrl.path);
1004                const _pluginIndex = _plugins.findIndex(
1005                    p => p.hash === plugin.hash,
1006                );
1007                if (_pluginIndex !== -1) {
1008                    // 重复插件,直接忽略
1009                    continue;
1010                }
1011                plugin.hash !== '' && _plugins.push(plugin);
1012            }
1013        }
1014
1015        plugins = _plugins;
1016        /** 初始化meta信息 */
1017        await PluginMeta.setupMeta(plugins.map(_ => _.name));
1018        /** 查看一下是否有禁用的标记 */
1019        const allMeta = PluginMeta.getPluginMetaAll() ?? {};
1020        for (let plugin of plugins) {
1021            if (allMeta[plugin.name]?.enabled === false) {
1022                plugin.state = 'disabled';
1023            }
1024        }
1025        pluginStateMapper.notify();
1026    } catch (e: any) {
1027        ToastAndroid.show(
1028            `插件初始化失败:${e?.message ?? e}`,
1029            ToastAndroid.LONG,
1030        );
1031        errorLog('插件初始化失败', e?.message);
1032        throw e;
1033    }
1034}
1035
1036interface IInstallPluginConfig {
1037    notCheckVersion?: boolean;
1038}
1039
1040async function installPluginFromRawCode(
1041    funcCode: string,
1042    config?: IInstallPluginConfig,
1043) {
1044    if (funcCode) {
1045        const plugin = new Plugin(funcCode, '');
1046        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1047        if (_pluginIndex !== -1) {
1048            // 静默忽略
1049            return plugin;
1050        }
1051        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1052        if (oldVersionPlugin && !config?.notCheckVersion) {
1053            if (
1054                compare(
1055                    oldVersionPlugin.instance.version ?? '',
1056                    plugin.instance.version ?? '',
1057                    '>',
1058                )
1059            ) {
1060                throw new Error('已安装更新版本的插件');
1061            }
1062        }
1063
1064        if (plugin.hash !== '') {
1065            const fn = nanoid();
1066            if (oldVersionPlugin) {
1067                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
1068                try {
1069                    await unlink(oldVersionPlugin.path);
1070                } catch {}
1071            }
1072            const pluginPath = `${pathConst.pluginPath}${fn}.js`;
1073            await writeFile(pluginPath, funcCode, 'utf8');
1074            plugin.path = pluginPath;
1075            plugins = plugins.concat(plugin);
1076            pluginStateMapper.notify();
1077            return plugin;
1078        }
1079        throw new Error('插件无法解析!');
1080    }
1081}
1082
1083// 安装插件
1084async function installPlugin(
1085    pluginPath: string,
1086    config?: IInstallPluginConfig,
1087) {
1088    // if (pluginPath.endsWith('.js')) {
1089    const funcCode = await readFile(pluginPath, 'utf8');
1090
1091    if (funcCode) {
1092        const plugin = new Plugin(funcCode, pluginPath);
1093        const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1094        if (_pluginIndex !== -1) {
1095            // 静默忽略
1096            return plugin;
1097        }
1098        const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1099        if (oldVersionPlugin && !config?.notCheckVersion) {
1100            if (
1101                compare(
1102                    oldVersionPlugin.instance.version ?? '',
1103                    plugin.instance.version ?? '',
1104                    '>',
1105                )
1106            ) {
1107                throw new Error('已安装更新版本的插件');
1108            }
1109        }
1110
1111        if (plugin.hash !== '') {
1112            const fn = nanoid();
1113            if (oldVersionPlugin) {
1114                plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash);
1115                try {
1116                    await unlink(oldVersionPlugin.path);
1117                } catch {}
1118            }
1119            const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1120            await copyFile(pluginPath, _pluginPath);
1121            plugin.path = _pluginPath;
1122            plugins = plugins.concat(plugin);
1123            pluginStateMapper.notify();
1124            return plugin;
1125        }
1126        throw new Error('插件无法解析!');
1127    }
1128    throw new Error('插件无法识别!');
1129}
1130
1131const reqHeaders = {
1132    'Cache-Control': 'no-cache',
1133    Pragma: 'no-cache',
1134    Expires: '0',
1135};
1136
1137async function installPluginFromUrl(
1138    url: string,
1139    config?: IInstallPluginConfig,
1140) {
1141    try {
1142        const funcCode = (
1143            await axios.get(url, {
1144                headers: reqHeaders,
1145            })
1146        ).data;
1147        if (funcCode) {
1148            const plugin = new Plugin(funcCode, '');
1149            const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
1150            if (_pluginIndex !== -1) {
1151                // 静默忽略
1152                return;
1153            }
1154            const oldVersionPlugin = plugins.find(p => p.name === plugin.name);
1155            if (oldVersionPlugin && !config?.notCheckVersion) {
1156                if (
1157                    compare(
1158                        oldVersionPlugin.instance.version ?? '',
1159                        plugin.instance.version ?? '',
1160                        '>',
1161                    )
1162                ) {
1163                    throw new Error('已安装更新版本的插件');
1164                }
1165            }
1166
1167            if (plugin.hash !== '') {
1168                const fn = nanoid();
1169                const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
1170                await writeFile(_pluginPath, funcCode, 'utf8');
1171                plugin.path = _pluginPath;
1172                plugins = plugins.concat(plugin);
1173                if (oldVersionPlugin) {
1174                    plugins = plugins.filter(
1175                        _ => _.hash !== oldVersionPlugin.hash,
1176                    );
1177                    try {
1178                        await unlink(oldVersionPlugin.path);
1179                    } catch {}
1180                }
1181                pluginStateMapper.notify();
1182                return;
1183            }
1184            throw new Error('插件无法解析!');
1185        }
1186    } catch (e: any) {
1187        devLog('error', 'URL安装插件失败', e, e?.message);
1188        errorLog('URL安装插件失败', e);
1189        throw new Error(e?.message ?? '');
1190    }
1191}
1192
1193/** 卸载插件 */
1194async function uninstallPlugin(hash: string) {
1195    const targetIndex = plugins.findIndex(_ => _.hash === hash);
1196    if (targetIndex !== -1) {
1197        try {
1198            const pluginName = plugins[targetIndex].name;
1199            await unlink(plugins[targetIndex].path);
1200            plugins = plugins.filter(_ => _.hash !== hash);
1201            pluginStateMapper.notify();
1202            // 防止其他重名
1203            if (plugins.every(_ => _.name !== pluginName)) {
1204                MediaExtra.removeAll(pluginName);
1205            }
1206        } catch {}
1207    }
1208}
1209
1210async function uninstallAllPlugins() {
1211    await Promise.all(
1212        plugins.map(async plugin => {
1213            try {
1214                const pluginName = plugin.name;
1215                await unlink(plugin.path);
1216                MediaExtra.removeAll(pluginName);
1217            } catch (e) {}
1218        }),
1219    );
1220    plugins = [];
1221    pluginStateMapper.notify();
1222
1223    /** 清除空余文件,异步做就可以了 */
1224    readDir(pathConst.pluginPath)
1225        .then(fns => {
1226            fns.forEach(fn => {
1227                unlink(fn.path).catch(emptyFunction);
1228            });
1229        })
1230        .catch(emptyFunction);
1231}
1232
1233async function updatePlugin(plugin: Plugin) {
1234    const updateUrl = plugin.instance.srcUrl;
1235    if (!updateUrl) {
1236        throw new Error('没有更新源');
1237    }
1238    try {
1239        await installPluginFromUrl(updateUrl);
1240    } catch (e: any) {
1241        if (e.message === '插件已安装') {
1242            throw new Error('当前已是最新版本');
1243        } else {
1244            throw e;
1245        }
1246    }
1247}
1248
1249function getByMedia(mediaItem: ICommon.IMediaBase) {
1250    return getByName(mediaItem?.platform);
1251}
1252
1253function getByHash(hash: string) {
1254    return hash === localPluginHash
1255        ? localFilePlugin
1256        : plugins.find(_ => _.hash === hash);
1257}
1258
1259function getByName(name: string) {
1260    return name === localPluginPlatform
1261        ? localFilePlugin
1262        : plugins.find(_ => _.name === name);
1263}
1264
1265function getValidPlugins() {
1266    return plugins.filter(_ => _.state === 'enabled');
1267}
1268
1269function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) {
1270    return plugins.filter(
1271        _ =>
1272            _.state === 'enabled' &&
1273            _.instance.search &&
1274            (supportedSearchType && _.instance.supportedSearchType
1275                ? _.instance.supportedSearchType.includes(supportedSearchType)
1276                : true),
1277    );
1278}
1279
1280function getSortedSearchablePlugins(
1281    supportedSearchType?: ICommon.SupportMediaType,
1282) {
1283    return getSearchablePlugins(supportedSearchType).sort((a, b) =>
1284        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1285            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1286        0
1287            ? -1
1288            : 1,
1289    );
1290}
1291
1292function getTopListsablePlugins() {
1293    return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists);
1294}
1295
1296function getSortedTopListsablePlugins() {
1297    return getTopListsablePlugins().sort((a, b) =>
1298        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1299            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1300        0
1301            ? -1
1302            : 1,
1303    );
1304}
1305
1306function getRecommendSheetablePlugins() {
1307    return plugins.filter(
1308        _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag,
1309    );
1310}
1311
1312function getSortedRecommendSheetablePlugins() {
1313    return getRecommendSheetablePlugins().sort((a, b) =>
1314        (PluginMeta.getPluginMeta(a).order ?? Infinity) -
1315            (PluginMeta.getPluginMeta(b).order ?? Infinity) <
1316        0
1317            ? -1
1318            : 1,
1319    );
1320}
1321
1322function useSortedPlugins() {
1323    const _plugins = pluginStateMapper.useMappedState();
1324    const _pluginMetaAll = PluginMeta.usePluginMetaAll();
1325
1326    const [sortedPlugins, setSortedPlugins] = useState(
1327        [..._plugins].sort((a, b) =>
1328            (_pluginMetaAll[a.name]?.order ?? Infinity) -
1329                (_pluginMetaAll[b.name]?.order ?? Infinity) <
1330            0
1331                ? -1
1332                : 1,
1333        ),
1334    );
1335
1336    useEffect(() => {
1337        InteractionManager.runAfterInteractions(() => {
1338            setSortedPlugins(
1339                [..._plugins].sort((a, b) =>
1340                    (_pluginMetaAll[a.name]?.order ?? Infinity) -
1341                        (_pluginMetaAll[b.name]?.order ?? Infinity) <
1342                    0
1343                        ? -1
1344                        : 1,
1345                ),
1346            );
1347        });
1348    }, [_plugins, _pluginMetaAll]);
1349
1350    return sortedPlugins;
1351}
1352
1353async function setPluginEnabled(plugin: Plugin, enabled?: boolean) {
1354    const target = plugins.find(it => it.hash === plugin.hash);
1355    if (target) {
1356        target.state = enabled ? 'enabled' : 'disabled';
1357        plugins = [...plugins];
1358        pluginStateMapper.notify();
1359        PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled);
1360    }
1361}
1362
1363const PluginManager = {
1364    setup,
1365    installPlugin,
1366    installPluginFromRawCode,
1367    installPluginFromUrl,
1368    updatePlugin,
1369    uninstallPlugin,
1370    getByMedia,
1371    getByHash,
1372    getByName,
1373    getValidPlugins,
1374    getSearchablePlugins,
1375    getSortedSearchablePlugins,
1376    getTopListsablePlugins,
1377    getSortedRecommendSheetablePlugins,
1378    getSortedTopListsablePlugins,
1379    usePlugins: pluginStateMapper.useMappedState,
1380    useSortedPlugins,
1381    uninstallAllPlugins,
1382    setPluginEnabled,
1383};
1384
1385export default PluginManager;
1386