xref: /MusicFree/src/core/pluginManager.ts (revision 927dbe935e5ed3130d85ec27a6ac3efd1a163ad1)
1*927dbe93S猫头猫import RNFS, {
2*927dbe93S猫头猫  copyFile,
3*927dbe93S猫头猫  exists,
4*927dbe93S猫头猫  moveFile,
5*927dbe93S猫头猫  readDir,
6*927dbe93S猫头猫  readFile,
7*927dbe93S猫头猫  unlink,
8*927dbe93S猫头猫  writeFile,
9*927dbe93S猫头猫} from 'react-native-fs';
10*927dbe93S猫头猫import CryptoJs from 'crypto-js';
11*927dbe93S猫头猫import dayjs from 'dayjs';
12*927dbe93S猫头猫import axios from 'axios';
13*927dbe93S猫头猫import {useEffect, useState} from 'react';
14*927dbe93S猫头猫import {ToastAndroid} from 'react-native';
15*927dbe93S猫头猫import pathConst from '@/constants/pathConst';
16*927dbe93S猫头猫import {satisfies} from 'compare-versions';
17*927dbe93S猫头猫import DeviceInfo from 'react-native-device-info';
18*927dbe93S猫头猫import StateMapper from '@/utils/stateMapper';
19*927dbe93S猫头猫import MediaMeta from './mediaMeta';
20*927dbe93S猫头猫import {nanoid} from 'nanoid';
21*927dbe93S猫头猫import {errorLog, trace} from '../utils/log';
22*927dbe93S猫头猫import Cache from './cache';
23*927dbe93S猫头猫import {isSameMediaItem, resetMediaItem} from '@/utils/mediaItem';
24*927dbe93S猫头猫import {internalSerialzeKey, internalSymbolKey} from '@/constants/commonConst';
25*927dbe93S猫头猫import Download from './download';
26*927dbe93S猫头猫import delay from '@/utils/delay';
27*927dbe93S猫头猫
28*927dbe93S猫头猫axios.defaults.timeout = 1500;
29*927dbe93S猫头猫
30*927dbe93S猫头猫const sha256 = CryptoJs.SHA256;
31*927dbe93S猫头猫
32*927dbe93S猫头猫enum PluginStateCode {
33*927dbe93S猫头猫  /** 版本不匹配 */
34*927dbe93S猫头猫  VersionNotMatch = 'VERSION NOT MATCH',
35*927dbe93S猫头猫  /** 插件不完整 */
36*927dbe93S猫头猫  NotComplete = 'NOT COMPLETE',
37*927dbe93S猫头猫  /** 无法解析 */
38*927dbe93S猫头猫  CannotParse = 'CANNOT PARSE',
39*927dbe93S猫头猫}
40*927dbe93S猫头猫
41*927dbe93S猫头猫export class Plugin {
42*927dbe93S猫头猫  /** 插件名 */
43*927dbe93S猫头猫  public name: string;
44*927dbe93S猫头猫  /** 插件的hash,作为唯一id */
45*927dbe93S猫头猫  public hash: string;
46*927dbe93S猫头猫  /** 插件状态:激活、关闭、错误 */
47*927dbe93S猫头猫  public state: 'enabled' | 'disabled' | 'error';
48*927dbe93S猫头猫  /** 插件支持的搜索类型 */
49*927dbe93S猫头猫  public supportedSearchType?: string;
50*927dbe93S猫头猫  /** 插件状态信息 */
51*927dbe93S猫头猫  public stateCode?: PluginStateCode;
52*927dbe93S猫头猫  /** 插件的实例 */
53*927dbe93S猫头猫  public instance: IPlugin.IPluginInstance;
54*927dbe93S猫头猫  /** 插件路径 */
55*927dbe93S猫头猫  public path: string;
56*927dbe93S猫头猫  /** 插件方法 */
57*927dbe93S猫头猫  public methods: PluginMethods;
58*927dbe93S猫头猫
59*927dbe93S猫头猫  constructor(funcCode: string, pluginPath: string) {
60*927dbe93S猫头猫    this.state = 'enabled';
61*927dbe93S猫头猫    let _instance: IPlugin.IPluginInstance;
62*927dbe93S猫头猫    try {
63*927dbe93S猫头猫      _instance = Function(`
64*927dbe93S猫头猫      'use strict';
65*927dbe93S猫头猫      try {
66*927dbe93S猫头猫        return ${funcCode};
67*927dbe93S猫头猫      } catch(e) {
68*927dbe93S猫头猫        return null;
69*927dbe93S猫头猫      }
70*927dbe93S猫头猫    `)()({CryptoJs, axios, dayjs});
71*927dbe93S猫头猫
72*927dbe93S猫头猫      this.checkValid(_instance);
73*927dbe93S猫头猫    } catch (e: any) {
74*927dbe93S猫头猫      this.state = 'error';
75*927dbe93S猫头猫      this.stateCode = PluginStateCode.CannotParse;
76*927dbe93S猫头猫      if (e?.stateCode) {
77*927dbe93S猫头猫        this.stateCode = e.stateCode;
78*927dbe93S猫头猫      }
79*927dbe93S猫头猫      errorLog(`${pluginPath}插件无法解析 `, {
80*927dbe93S猫头猫        stateCode: this.stateCode,
81*927dbe93S猫头猫        message: e?.message,
82*927dbe93S猫头猫        stack: e?.stack,
83*927dbe93S猫头猫      });
84*927dbe93S猫头猫      _instance = e?.instance ?? {
85*927dbe93S猫头猫        _path: '',
86*927dbe93S猫头猫        platform: '',
87*927dbe93S猫头猫        appVersion: '',
88*927dbe93S猫头猫        async getMusicTrack() {
89*927dbe93S猫头猫          return null;
90*927dbe93S猫头猫        },
91*927dbe93S猫头猫        async search() {
92*927dbe93S猫头猫          return {};
93*927dbe93S猫头猫        },
94*927dbe93S猫头猫        async getAlbumInfo() {
95*927dbe93S猫头猫          return null;
96*927dbe93S猫头猫        },
97*927dbe93S猫头猫      };
98*927dbe93S猫头猫    }
99*927dbe93S猫头猫    this.instance = _instance;
100*927dbe93S猫头猫    this.path = pluginPath;
101*927dbe93S猫头猫    this.name = _instance.platform;
102*927dbe93S猫头猫    if (this.instance.platform === '') {
103*927dbe93S猫头猫      this.hash = '';
104*927dbe93S猫头猫    } else {
105*927dbe93S猫头猫      this.hash = sha256(funcCode).toString();
106*927dbe93S猫头猫    }
107*927dbe93S猫头猫
108*927dbe93S猫头猫    // 放在最后
109*927dbe93S猫头猫    this.methods = new PluginMethods(this);
110*927dbe93S猫头猫  }
111*927dbe93S猫头猫
112*927dbe93S猫头猫  private checkValid(_instance: IPlugin.IPluginInstance) {
113*927dbe93S猫头猫    // 总不会一个都没有吧
114*927dbe93S猫头猫    const keys: Array<keyof IPlugin.IPluginInstance> = [
115*927dbe93S猫头猫      'getAlbumInfo',
116*927dbe93S猫头猫      'search',
117*927dbe93S猫头猫      'getMusicTrack',
118*927dbe93S猫头猫    ];
119*927dbe93S猫头猫    if (keys.every(k => !_instance[k])) {
120*927dbe93S猫头猫      throw {
121*927dbe93S猫头猫        instance: _instance,
122*927dbe93S猫头猫        stateCode: PluginStateCode.NotComplete,
123*927dbe93S猫头猫      };
124*927dbe93S猫头猫    }
125*927dbe93S猫头猫    /** 版本号校验 */
126*927dbe93S猫头猫    if (
127*927dbe93S猫头猫      _instance.appVersion &&
128*927dbe93S猫头猫      !satisfies(DeviceInfo.getVersion(), _instance.appVersion)
129*927dbe93S猫头猫    ) {
130*927dbe93S猫头猫      throw {
131*927dbe93S猫头猫        instance: _instance,
132*927dbe93S猫头猫        stateCode: PluginStateCode.VersionNotMatch,
133*927dbe93S猫头猫      };
134*927dbe93S猫头猫    }
135*927dbe93S猫头猫    return true;
136*927dbe93S猫头猫  }
137*927dbe93S猫头猫}
138*927dbe93S猫头猫
139*927dbe93S猫头猫/** 有缓存等信息 */
140*927dbe93S猫头猫class PluginMethods implements IPlugin.IPluginInstanceMethods {
141*927dbe93S猫头猫  private plugin;
142*927dbe93S猫头猫  constructor(plugin: Plugin) {
143*927dbe93S猫头猫    this.plugin = plugin;
144*927dbe93S猫头猫  }
145*927dbe93S猫头猫  /** 搜索 */
146*927dbe93S猫头猫  async search<T extends ICommon.SupportMediaType>(
147*927dbe93S猫头猫    query: string,
148*927dbe93S猫头猫    page: number,
149*927dbe93S猫头猫    type: T,
150*927dbe93S猫头猫  ): Promise<IPlugin.ISearchResult<T>> {
151*927dbe93S猫头猫    if (!this.plugin.instance.search) {
152*927dbe93S猫头猫      return {
153*927dbe93S猫头猫        isEnd: true,
154*927dbe93S猫头猫        data: [],
155*927dbe93S猫头猫      };
156*927dbe93S猫头猫    }
157*927dbe93S猫头猫
158*927dbe93S猫头猫    const result = (await this.plugin.instance.search(query, page, type)) ?? {};
159*927dbe93S猫头猫    if (Array.isArray(result.data)) {
160*927dbe93S猫头猫      result.data.forEach(_ => {
161*927dbe93S猫头猫        resetMediaItem(_, this.plugin.name);
162*927dbe93S猫头猫      });
163*927dbe93S猫头猫      return {
164*927dbe93S猫头猫        isEnd: result.isEnd ?? true,
165*927dbe93S猫头猫        data: result.data,
166*927dbe93S猫头猫      };
167*927dbe93S猫头猫    }
168*927dbe93S猫头猫    return {
169*927dbe93S猫头猫      isEnd: true,
170*927dbe93S猫头猫      data: [],
171*927dbe93S猫头猫    };
172*927dbe93S猫头猫  }
173*927dbe93S猫头猫
174*927dbe93S猫头猫  /** 获取真实源 */
175*927dbe93S猫头猫  async getMusicTrack(
176*927dbe93S猫头猫    musicItem: IMusic.IMusicItemBase,
177*927dbe93S猫头猫    retryCount = 1,
178*927dbe93S猫头猫  ): Promise<IPlugin.IMusicTrackResult> {
179*927dbe93S猫头猫    // 1. 本地搜索 其实直接读mediameta就好了
180*927dbe93S猫头猫    const localPath =
181*927dbe93S猫头猫      musicItem?.[internalSymbolKey]?.localPath ??
182*927dbe93S猫头猫      Download.getDownloaded(musicItem)?.[internalSymbolKey]?.localPath;
183*927dbe93S猫头猫    if (localPath && (await exists(localPath))) {
184*927dbe93S猫头猫      return {
185*927dbe93S猫头猫        url: localPath,
186*927dbe93S猫头猫      };
187*927dbe93S猫头猫    }
188*927dbe93S猫头猫    // 2. 缓存播放
189*927dbe93S猫头猫    const mediaCache = Cache.get(musicItem);
190*927dbe93S猫头猫    if (mediaCache && mediaCache?.url) {
191*927dbe93S猫头猫      return {
192*927dbe93S猫头猫        url: mediaCache.url,
193*927dbe93S猫头猫        headers: mediaCache.headers,
194*927dbe93S猫头猫        userAgent: mediaCache.userAgent ?? mediaCache.headers?.['user-agent'],
195*927dbe93S猫头猫      };
196*927dbe93S猫头猫    }
197*927dbe93S猫头猫    // 3. 插件解析
198*927dbe93S猫头猫    if (!this.plugin.instance.getMusicTrack) {
199*927dbe93S猫头猫      return {url: musicItem.url};
200*927dbe93S猫头猫    }
201*927dbe93S猫头猫    try {
202*927dbe93S猫头猫      const {url, headers} =
203*927dbe93S猫头猫        (await this.plugin.instance.getMusicTrack(musicItem)) ?? {};
204*927dbe93S猫头猫      if (!url) {
205*927dbe93S猫头猫        throw new Error();
206*927dbe93S猫头猫      }
207*927dbe93S猫头猫      const result = {
208*927dbe93S猫头猫        url,
209*927dbe93S猫头猫        headers,
210*927dbe93S猫头猫        userAgent: headers?.['user-agent'],
211*927dbe93S猫头猫      };
212*927dbe93S猫头猫
213*927dbe93S猫头猫      Cache.update(musicItem, result);
214*927dbe93S猫头猫      return result;
215*927dbe93S猫头猫    } catch (e: any) {
216*927dbe93S猫头猫      if (retryCount > 0) {
217*927dbe93S猫头猫        await delay(150);
218*927dbe93S猫头猫        return this.getMusicTrack(musicItem, --retryCount);
219*927dbe93S猫头猫      }
220*927dbe93S猫头猫      errorLog('获取真实源失败', e?.message);
221*927dbe93S猫头猫      throw e;
222*927dbe93S猫头猫    }
223*927dbe93S猫头猫  }
224*927dbe93S猫头猫
225*927dbe93S猫头猫  /** 获取音乐详情 */
226*927dbe93S猫头猫  async getMusicInfo(
227*927dbe93S猫头猫    musicItem: ICommon.IMediaBase,
228*927dbe93S猫头猫  ): Promise<IMusic.IMusicItem | null> {
229*927dbe93S猫头猫    if (!this.plugin.instance.getMusicInfo) {
230*927dbe93S猫头猫      return musicItem as IMusic.IMusicItem;
231*927dbe93S猫头猫    }
232*927dbe93S猫头猫    return (
233*927dbe93S猫头猫      this.plugin.instance.getMusicInfo(
234*927dbe93S猫头猫        resetMediaItem(musicItem, undefined, true),
235*927dbe93S猫头猫      ) ?? musicItem
236*927dbe93S猫头猫    );
237*927dbe93S猫头猫  }
238*927dbe93S猫头猫
239*927dbe93S猫头猫  /** 获取歌词 */
240*927dbe93S猫头猫  async getLyric(
241*927dbe93S猫头猫    musicItem: IMusic.IMusicItemBase,
242*927dbe93S猫头猫    from?: IMusic.IMusicItemBase,
243*927dbe93S猫头猫  ): Promise<ILyric.ILyricSource | null> {
244*927dbe93S猫头猫    // 1.额外存储的meta信息
245*927dbe93S猫头猫    const meta = MediaMeta.get(musicItem);
246*927dbe93S猫头猫    if (meta && meta.associatedLrc) {
247*927dbe93S猫头猫      // 有关联歌词
248*927dbe93S猫头猫      if (
249*927dbe93S猫头猫        isSameMediaItem(musicItem, from) ||
250*927dbe93S猫头猫        isSameMediaItem(meta.associatedLrc, musicItem)
251*927dbe93S猫头猫      ) {
252*927dbe93S猫头猫        // 形成环路,断开当前的环
253*927dbe93S猫头猫        await MediaMeta.update(musicItem, {
254*927dbe93S猫头猫          associatedLrc: undefined,
255*927dbe93S猫头猫        });
256*927dbe93S猫头猫        // 无歌词
257*927dbe93S猫头猫        return null;
258*927dbe93S猫头猫      }
259*927dbe93S猫头猫      // 获取关联歌词
260*927dbe93S猫头猫      const result = await this.getLyric(meta.associatedLrc, from ?? musicItem);
261*927dbe93S猫头猫      if (result) {
262*927dbe93S猫头猫        // 如果有关联歌词,就返回关联歌词,深度优先
263*927dbe93S猫头猫        return result;
264*927dbe93S猫头猫      }
265*927dbe93S猫头猫    }
266*927dbe93S猫头猫    const cache = Cache.get(musicItem);
267*927dbe93S猫头猫    let rawLrc = meta?.rawLrc || musicItem.rawLrc || cache?.rawLrc;
268*927dbe93S猫头猫    let lrcUrl = meta?.lrc || musicItem.lrc || cache?.lrc;
269*927dbe93S猫头猫    // 如果存在文本
270*927dbe93S猫头猫    if (rawLrc) {
271*927dbe93S猫头猫      return {
272*927dbe93S猫头猫        rawLrc,
273*927dbe93S猫头猫        lrc: lrcUrl,
274*927dbe93S猫头猫      };
275*927dbe93S猫头猫    }
276*927dbe93S猫头猫    // 2.本地缓存
277*927dbe93S猫头猫    const localLrc =
278*927dbe93S猫头猫      meta?.[internalSerialzeKey]?.local?.localLrc ||
279*927dbe93S猫头猫      cache?.[internalSerialzeKey]?.local?.localLrc;
280*927dbe93S猫头猫    if (localLrc && (await exists(localLrc))) {
281*927dbe93S猫头猫      rawLrc = await readFile(localLrc, 'utf8');
282*927dbe93S猫头猫      return {
283*927dbe93S猫头猫        rawLrc,
284*927dbe93S猫头猫        lrc: lrcUrl,
285*927dbe93S猫头猫      };
286*927dbe93S猫头猫    }
287*927dbe93S猫头猫    // 3.优先使用url
288*927dbe93S猫头猫    if (lrcUrl) {
289*927dbe93S猫头猫      try {
290*927dbe93S猫头猫        // 需要超时时间 axios timeout 但是没生效
291*927dbe93S猫头猫        rawLrc = (await axios.get(lrcUrl)).data;
292*927dbe93S猫头猫        return {
293*927dbe93S猫头猫          rawLrc,
294*927dbe93S猫头猫          lrc: lrcUrl,
295*927dbe93S猫头猫        };
296*927dbe93S猫头猫      } catch {
297*927dbe93S猫头猫        lrcUrl = undefined;
298*927dbe93S猫头猫      }
299*927dbe93S猫头猫    }
300*927dbe93S猫头猫    // 4. 如果地址失效
301*927dbe93S猫头猫    if (!lrcUrl) {
302*927dbe93S猫头猫      // 插件获得url
303*927dbe93S猫头猫      try {
304*927dbe93S猫头猫        const lrcSource = await this.plugin.instance?.getLyric?.(
305*927dbe93S猫头猫          resetMediaItem(musicItem, undefined, true),
306*927dbe93S猫头猫        );
307*927dbe93S猫头猫        rawLrc = lrcSource?.rawLrc;
308*927dbe93S猫头猫        lrcUrl = lrcSource?.lrc;
309*927dbe93S猫头猫      } catch (e: any) {
310*927dbe93S猫头猫        trace('插件获取歌词失败', e?.message, 'error');
311*927dbe93S猫头猫      }
312*927dbe93S猫头猫    }
313*927dbe93S猫头猫    // 5. 最后一次请求
314*927dbe93S猫头猫    if (rawLrc || lrcUrl) {
315*927dbe93S猫头猫      const filename = `${pathConst.lrcCachePath}${nanoid()}.lrc`;
316*927dbe93S猫头猫      if (lrcUrl) {
317*927dbe93S猫头猫        try {
318*927dbe93S猫头猫          rawLrc = (await axios.get(lrcUrl)).data;
319*927dbe93S猫头猫        } catch {}
320*927dbe93S猫头猫      }
321*927dbe93S猫头猫      if (rawLrc) {
322*927dbe93S猫头猫        await writeFile(filename, rawLrc, 'utf8');
323*927dbe93S猫头猫        // 写入缓存
324*927dbe93S猫头猫        Cache.update(musicItem, [
325*927dbe93S猫头猫          [`${internalSerialzeKey}.local.localLrc`, filename],
326*927dbe93S猫头猫        ]);
327*927dbe93S猫头猫        // 如果有meta
328*927dbe93S猫头猫        if (meta) {
329*927dbe93S猫头猫          MediaMeta.update(musicItem, [
330*927dbe93S猫头猫            [`${internalSerialzeKey}.local.localLrc`, filename],
331*927dbe93S猫头猫          ]);
332*927dbe93S猫头猫        }
333*927dbe93S猫头猫        return {
334*927dbe93S猫头猫          rawLrc,
335*927dbe93S猫头猫          lrc: lrcUrl,
336*927dbe93S猫头猫        };
337*927dbe93S猫头猫      }
338*927dbe93S猫头猫    }
339*927dbe93S猫头猫
340*927dbe93S猫头猫    return null;
341*927dbe93S猫头猫  }
342*927dbe93S猫头猫
343*927dbe93S猫头猫  /** 获取歌词文本 */
344*927dbe93S猫头猫  async getLyricText(
345*927dbe93S猫头猫    musicItem: IMusic.IMusicItem,
346*927dbe93S猫头猫  ): Promise<string | undefined> {
347*927dbe93S猫头猫    return (await this.getLyric(musicItem))?.rawLrc;
348*927dbe93S猫头猫  }
349*927dbe93S猫头猫
350*927dbe93S猫头猫  /** 获取专辑信息 */
351*927dbe93S猫头猫  async getAlbumInfo(
352*927dbe93S猫头猫    albumItem: IAlbum.IAlbumItemBase,
353*927dbe93S猫头猫  ): Promise<IAlbum.IAlbumItem | null> {
354*927dbe93S猫头猫    if (!this.plugin.instance.getAlbumInfo) {
355*927dbe93S猫头猫      return {...albumItem, musicList: []};
356*927dbe93S猫头猫    }
357*927dbe93S猫头猫    try {
358*927dbe93S猫头猫      const result = await this.plugin.instance.getAlbumInfo(
359*927dbe93S猫头猫        resetMediaItem(albumItem, undefined, true),
360*927dbe93S猫头猫      );
361*927dbe93S猫头猫      result?.musicList?.forEach(_ => {
362*927dbe93S猫头猫        resetMediaItem(_, this.plugin.name);
363*927dbe93S猫头猫      });
364*927dbe93S猫头猫      return result;
365*927dbe93S猫头猫    } catch {
366*927dbe93S猫头猫      return {...albumItem, musicList: []};
367*927dbe93S猫头猫    }
368*927dbe93S猫头猫  }
369*927dbe93S猫头猫
370*927dbe93S猫头猫  /** 查询作者信息 */
371*927dbe93S猫头猫  async queryArtistWorks<T extends IArtist.ArtistMediaType>(
372*927dbe93S猫头猫    artistItem: IArtist.IArtistItem,
373*927dbe93S猫头猫    page: number,
374*927dbe93S猫头猫    type: T,
375*927dbe93S猫头猫  ): Promise<IPlugin.ISearchResult<T>> {
376*927dbe93S猫头猫    if (!this.plugin.instance.queryArtistWorks) {
377*927dbe93S猫头猫      return {
378*927dbe93S猫头猫        isEnd: true,
379*927dbe93S猫头猫        data: [],
380*927dbe93S猫头猫      };
381*927dbe93S猫头猫    }
382*927dbe93S猫头猫    try {
383*927dbe93S猫头猫      const result = await this.plugin.instance.queryArtistWorks(
384*927dbe93S猫头猫        artistItem,
385*927dbe93S猫头猫        page,
386*927dbe93S猫头猫        type,
387*927dbe93S猫头猫      );
388*927dbe93S猫头猫      if (!result.data) {
389*927dbe93S猫头猫        return {
390*927dbe93S猫头猫          isEnd: true,
391*927dbe93S猫头猫          data: [],
392*927dbe93S猫头猫        };
393*927dbe93S猫头猫      }
394*927dbe93S猫头猫      result.data?.forEach(_ => resetMediaItem(_, this.plugin.name));
395*927dbe93S猫头猫      return {
396*927dbe93S猫头猫        isEnd: result.isEnd ?? true,
397*927dbe93S猫头猫        data: result.data,
398*927dbe93S猫头猫      };
399*927dbe93S猫头猫    } catch (e) {
400*927dbe93S猫头猫      throw e;
401*927dbe93S猫头猫    }
402*927dbe93S猫头猫  }
403*927dbe93S猫头猫}
404*927dbe93S猫头猫let plugins: Array<Plugin> = [];
405*927dbe93S猫头猫const pluginStateMapper = new StateMapper(() => plugins);
406*927dbe93S猫头猫
407*927dbe93S猫头猫async function setup() {
408*927dbe93S猫头猫  const _plugins: Array<Plugin> = [];
409*927dbe93S猫头猫  try {
410*927dbe93S猫头猫    // 加载插件
411*927dbe93S猫头猫    const pluginsPaths = await readDir(pathConst.pluginPath);
412*927dbe93S猫头猫    for (let i = 0; i < pluginsPaths.length; ++i) {
413*927dbe93S猫头猫      const _pluginUrl = pluginsPaths[i];
414*927dbe93S猫头猫
415*927dbe93S猫头猫      if (_pluginUrl.isFile() && _pluginUrl.name.endsWith('.js')) {
416*927dbe93S猫头猫        const funcCode = await readFile(_pluginUrl.path, 'utf8');
417*927dbe93S猫头猫        const plugin = new Plugin(funcCode, _pluginUrl.path);
418*927dbe93S猫头猫        const _pluginIndex = _plugins.findIndex(p => p.hash === plugin.hash);
419*927dbe93S猫头猫        if (_pluginIndex !== -1) {
420*927dbe93S猫头猫          // 重复插件,直接忽略
421*927dbe93S猫头猫          return;
422*927dbe93S猫头猫        }
423*927dbe93S猫头猫        plugin.hash !== '' && _plugins.push(plugin);
424*927dbe93S猫头猫      }
425*927dbe93S猫头猫    }
426*927dbe93S猫头猫
427*927dbe93S猫头猫    plugins = _plugins;
428*927dbe93S猫头猫    pluginStateMapper.notify();
429*927dbe93S猫头猫  } catch (e: any) {
430*927dbe93S猫头猫    ToastAndroid.show(`插件初始化失败:${e?.message ?? e}`, ToastAndroid.LONG);
431*927dbe93S猫头猫    throw e;
432*927dbe93S猫头猫  }
433*927dbe93S猫头猫}
434*927dbe93S猫头猫
435*927dbe93S猫头猫// 安装插件
436*927dbe93S猫头猫async function installPlugin(pluginPath: string) {
437*927dbe93S猫头猫  if (pluginPath.endsWith('.js') && (await exists(pluginPath))) {
438*927dbe93S猫头猫    const funcCode = await readFile(pluginPath, 'utf8');
439*927dbe93S猫头猫    const plugin = new Plugin(funcCode, pluginPath);
440*927dbe93S猫头猫    const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash);
441*927dbe93S猫头猫    if (_pluginIndex !== -1) {
442*927dbe93S猫头猫      return;
443*927dbe93S猫头猫    }
444*927dbe93S猫头猫    if (plugin.hash !== '') {
445*927dbe93S猫头猫      const fn = nanoid();
446*927dbe93S猫头猫      const _pluginPath = `${pathConst.pluginPath}${fn}.js`;
447*927dbe93S猫头猫      await copyFile(pluginPath, _pluginPath);
448*927dbe93S猫头猫      plugin.path = _pluginPath;
449*927dbe93S猫头猫      plugins = plugins.concat(plugin);
450*927dbe93S猫头猫      pluginStateMapper.notify();
451*927dbe93S猫头猫    }
452*927dbe93S猫头猫  }
453*927dbe93S猫头猫}
454*927dbe93S猫头猫
455*927dbe93S猫头猫/** 卸载插件 */
456*927dbe93S猫头猫async function uninstallPlugin(hash: string) {
457*927dbe93S猫头猫  const targetIndex = plugins.findIndex(_ => _.hash === hash);
458*927dbe93S猫头猫  if (targetIndex !== -1) {
459*927dbe93S猫头猫    try {
460*927dbe93S猫头猫      await unlink(plugins[targetIndex].path);
461*927dbe93S猫头猫      plugins = plugins.filter(_ => _.hash !== hash);
462*927dbe93S猫头猫      pluginStateMapper.notify();
463*927dbe93S猫头猫    } catch {}
464*927dbe93S猫头猫  }
465*927dbe93S猫头猫}
466*927dbe93S猫头猫
467*927dbe93S猫头猫function getByMedia(mediaItem: ICommon.IMediaBase) {
468*927dbe93S猫头猫    return getByName(mediaItem.platform);
469*927dbe93S猫头猫}
470*927dbe93S猫头猫
471*927dbe93S猫头猫function getByHash(hash: string) {
472*927dbe93S猫头猫  return plugins.find(_ => _.hash === hash);
473*927dbe93S猫头猫}
474*927dbe93S猫头猫
475*927dbe93S猫头猫function getByName(name: string) {
476*927dbe93S猫头猫  return plugins.find(_ => _.name === name);
477*927dbe93S猫头猫}
478*927dbe93S猫头猫
479*927dbe93S猫头猫function getValidPlugins() {
480*927dbe93S猫头猫  return plugins.filter(_ => _.state === 'enabled');
481*927dbe93S猫头猫}
482*927dbe93S猫头猫
483*927dbe93S猫头猫
484*927dbe93S猫头猫const PluginManager = {
485*927dbe93S猫头猫    setup,
486*927dbe93S猫头猫    installPlugin,
487*927dbe93S猫头猫    uninstallPlugin,
488*927dbe93S猫头猫    getByMedia,
489*927dbe93S猫头猫    getByHash,
490*927dbe93S猫头猫    getByName,
491*927dbe93S猫头猫    getValidPlugins,
492*927dbe93S猫头猫    usePlugins: pluginStateMapper.useMappedState
493*927dbe93S猫头猫}
494*927dbe93S猫头猫
495*927dbe93S猫头猫export default PluginManager;
496