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