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