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