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