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 {InteractionManager, ToastAndroid} from 'react-native'; 15import pathConst from '@/constants/pathConst'; 16import {compare, satisfies} from 'compare-versions'; 17import DeviceInfo from 'react-native-device-info'; 18import StateMapper from '@/utils/stateMapper'; 19import MediaMeta from './mediaMeta'; 20import {nanoid} from 'nanoid'; 21import {devLog, errorLog, trace} from '../utils/log'; 22import Cache from './cache'; 23import { 24 getInternalData, 25 InternalDataType, 26 isSameMediaItem, 27 resetMediaItem, 28} from '@/utils/mediaItem'; 29import { 30 CacheControl, 31 emptyFunction, 32 internalSerializeKey, 33 localPluginHash, 34 localPluginPlatform, 35} from '@/constants/commonConst'; 36import delay from '@/utils/delay'; 37import * as cheerio from 'cheerio'; 38import CookieManager from '@react-native-cookies/cookies'; 39import he from 'he'; 40import Network from './network'; 41import LocalMusicSheet from './localMusicSheet'; 42import {FileSystem} from 'react-native-file-access'; 43import Mp3Util from '@/native/mp3Util'; 44import {PluginMeta} from './pluginMeta'; 45import {useEffect, useState} from 'react'; 46 47axios.defaults.timeout = 1500; 48 49const sha256 = CryptoJs.SHA256; 50 51export enum PluginStateCode { 52 /** 版本不匹配 */ 53 VersionNotMatch = 'VERSION NOT MATCH', 54 /** 无法解析 */ 55 CannotParse = 'CANNOT PARSE', 56} 57 58const packages: Record<string, any> = { 59 cheerio, 60 'crypto-js': CryptoJs, 61 axios, 62 dayjs, 63 'big-integer': bigInt, 64 qs, 65 he, 66 '@react-native-cookies/cookies': CookieManager, 67}; 68 69const _require = (packageName: string) => { 70 let pkg = packages[packageName]; 71 pkg.default = pkg; 72 return pkg; 73}; 74 75const _consoleBind = function ( 76 method: 'log' | 'error' | 'info' | 'warn', 77 ...args: any 78) { 79 const fn = console[method]; 80 if (fn) { 81 fn(...args); 82 devLog(method, ...args); 83 } 84}; 85 86const _console = { 87 log: _consoleBind.bind(null, 'log'), 88 warn: _consoleBind.bind(null, 'warn'), 89 info: _consoleBind.bind(null, 'info'), 90 error: _consoleBind.bind(null, 'error'), 91}; 92 93//#region 插件类 94export class Plugin { 95 /** 插件名 */ 96 public name: string; 97 /** 插件的hash,作为唯一id */ 98 public hash: string; 99 /** 插件状态:激活、关闭、错误 */ 100 public state: 'enabled' | 'disabled' | 'error'; 101 /** 插件支持的搜索类型 */ 102 public supportedSearchType?: string; 103 /** 插件状态信息 */ 104 public stateCode?: PluginStateCode; 105 /** 插件的实例 */ 106 public instance: IPlugin.IPluginInstance; 107 /** 插件路径 */ 108 public path: string; 109 /** 插件方法 */ 110 public methods: PluginMethods; 111 /** 用户输入 */ 112 public userEnv?: Record<string, string>; 113 114 constructor( 115 funcCode: string | (() => IPlugin.IPluginInstance), 116 pluginPath: string, 117 ) { 118 this.state = 'enabled'; 119 let _instance: IPlugin.IPluginInstance; 120 const _module: any = {exports: {}}; 121 try { 122 if (typeof funcCode === 'string') { 123 // eslint-disable-next-line no-new-func 124 _instance = Function(` 125 'use strict'; 126 return function(require, __musicfree_require, module, exports, console) { 127 ${funcCode} 128 } 129 `)()(_require, _require, _module, _module.exports, _console); 130 if (_module.exports.default) { 131 _instance = _module.exports 132 .default as IPlugin.IPluginInstance; 133 } else { 134 _instance = _module.exports as IPlugin.IPluginInstance; 135 } 136 } else { 137 _instance = funcCode(); 138 } 139 this.checkValid(_instance); 140 } catch (e: any) { 141 console.log(e); 142 this.state = 'error'; 143 this.stateCode = PluginStateCode.CannotParse; 144 if (e?.stateCode) { 145 this.stateCode = e.stateCode; 146 } 147 errorLog(`${pluginPath}插件无法解析 `, { 148 stateCode: this.stateCode, 149 message: e?.message, 150 stack: e?.stack, 151 }); 152 _instance = e?.instance ?? { 153 _path: '', 154 platform: '', 155 appVersion: '', 156 async getMediaSource() { 157 return null; 158 }, 159 async search() { 160 return {}; 161 }, 162 async getAlbumInfo() { 163 return null; 164 }, 165 }; 166 } 167 this.instance = _instance; 168 this.path = pluginPath; 169 this.name = _instance.platform; 170 if ( 171 this.instance.platform === '' || 172 this.instance.platform === undefined 173 ) { 174 this.hash = ''; 175 } else { 176 if (typeof funcCode === 'string') { 177 this.hash = sha256(funcCode).toString(); 178 } else { 179 this.hash = sha256(funcCode.toString()).toString(); 180 } 181 } 182 183 // 放在最后 184 this.methods = new PluginMethods(this); 185 } 186 187 private checkValid(_instance: IPlugin.IPluginInstance) { 188 /** 版本号校验 */ 189 if ( 190 _instance.appVersion && 191 !satisfies(DeviceInfo.getVersion(), _instance.appVersion) 192 ) { 193 throw { 194 instance: _instance, 195 stateCode: PluginStateCode.VersionNotMatch, 196 }; 197 } 198 return true; 199 } 200} 201//#endregion 202 203//#region 基于插件类封装的方法,供给APP侧直接调用 204/** 有缓存等信息 */ 205class PluginMethods implements IPlugin.IPluginInstanceMethods { 206 private plugin; 207 constructor(plugin: Plugin) { 208 this.plugin = plugin; 209 } 210 /** 搜索 */ 211 async search<T extends ICommon.SupportMediaType>( 212 query: string, 213 page: number, 214 type: T, 215 ): Promise<IPlugin.ISearchResult<T>> { 216 if (!this.plugin.instance.search) { 217 return { 218 isEnd: true, 219 data: [], 220 }; 221 } 222 223 const result = 224 (await this.plugin.instance.search(query, page, type)) ?? {}; 225 if (Array.isArray(result.data)) { 226 result.data.forEach(_ => { 227 resetMediaItem(_, this.plugin.name); 228 }); 229 return { 230 isEnd: result.isEnd ?? true, 231 data: result.data, 232 }; 233 } 234 return { 235 isEnd: true, 236 data: [], 237 }; 238 } 239 240 /** 获取真实源 */ 241 async getMediaSource( 242 musicItem: IMusic.IMusicItemBase, 243 quality: IMusic.IQualityKey = 'standard', 244 retryCount = 1, 245 notUpdateCache = false, 246 ): Promise<IPlugin.IMediaSourceResult | null> { 247 // 1. 本地搜索 其实直接读mediameta就好了 248 const localPath = 249 getInternalData<string>(musicItem, InternalDataType.LOCALPATH) ?? 250 getInternalData<string>( 251 LocalMusicSheet.isLocalMusic(musicItem), 252 InternalDataType.LOCALPATH, 253 ); 254 if (localPath && (await FileSystem.exists(localPath))) { 255 trace('本地播放', localPath); 256 return { 257 url: localPath, 258 }; 259 } 260 if (musicItem.platform === localPluginPlatform) { 261 throw new Error('本地音乐不存在'); 262 } 263 // 2. 缓存播放 264 const mediaCache = Cache.get(musicItem); 265 const pluginCacheControl = 266 this.plugin.instance.cacheControl ?? 'no-cache'; 267 if ( 268 mediaCache && 269 mediaCache?.qualities?.[quality]?.url && 270 (pluginCacheControl === CacheControl.Cache || 271 (pluginCacheControl === CacheControl.NoCache && 272 Network.isOffline())) 273 ) { 274 trace('播放', '缓存播放'); 275 const qualityInfo = mediaCache.qualities[quality]; 276 return { 277 url: qualityInfo.url, 278 headers: mediaCache.headers, 279 userAgent: 280 mediaCache.userAgent ?? mediaCache.headers?.['user-agent'], 281 }; 282 } 283 // 3. 插件解析 284 if (!this.plugin.instance.getMediaSource) { 285 return {url: musicItem?.qualities?.[quality]?.url ?? musicItem.url}; 286 } 287 try { 288 const {url, headers} = (await this.plugin.instance.getMediaSource( 289 musicItem, 290 quality, 291 )) ?? {url: musicItem?.qualities?.[quality]?.url}; 292 if (!url) { 293 throw new Error('NOT RETRY'); 294 } 295 trace('播放', '插件播放'); 296 const result = { 297 url, 298 headers, 299 userAgent: headers?.['user-agent'], 300 } as IPlugin.IMediaSourceResult; 301 302 if ( 303 pluginCacheControl !== CacheControl.NoStore && 304 !notUpdateCache 305 ) { 306 Cache.update(musicItem, [ 307 ['headers', result.headers], 308 ['userAgent', result.userAgent], 309 [`qualities.${quality}.url`, url], 310 ]); 311 } 312 313 return result; 314 } catch (e: any) { 315 if (retryCount > 0 && e?.message !== 'NOT RETRY') { 316 await delay(150); 317 return this.getMediaSource(musicItem, quality, --retryCount); 318 } 319 errorLog('获取真实源失败', e?.message); 320 devLog('error', '获取真实源失败', e, e?.message); 321 return null; 322 } 323 } 324 325 /** 获取音乐详情 */ 326 async getMusicInfo( 327 musicItem: ICommon.IMediaBase, 328 ): Promise<Partial<IMusic.IMusicItem> | null> { 329 if (!this.plugin.instance.getMusicInfo) { 330 return null; 331 } 332 try { 333 return ( 334 this.plugin.instance.getMusicInfo( 335 resetMediaItem(musicItem, undefined, true), 336 ) ?? null 337 ); 338 } catch (e: any) { 339 devLog('error', '获取音乐详情失败', e, e?.message); 340 return null; 341 } 342 } 343 344 /** 获取歌词 */ 345 async getLyric( 346 musicItem: IMusic.IMusicItemBase, 347 from?: IMusic.IMusicItemBase, 348 ): Promise<ILyric.ILyricSource | null> { 349 // 1.额外存储的meta信息 350 const meta = MediaMeta.get(musicItem); 351 if (meta && meta.associatedLrc) { 352 // 有关联歌词 353 if ( 354 isSameMediaItem(musicItem, from) || 355 isSameMediaItem(meta.associatedLrc, musicItem) 356 ) { 357 // 形成环路,断开当前的环 358 await MediaMeta.update(musicItem, { 359 associatedLrc: undefined, 360 }); 361 // 无歌词 362 return null; 363 } 364 // 获取关联歌词 365 const associatedMeta = MediaMeta.get(meta.associatedLrc) ?? {}; 366 const result = await this.getLyric( 367 {...meta.associatedLrc, ...associatedMeta}, 368 from ?? musicItem, 369 ); 370 if (result) { 371 // 如果有关联歌词,就返回关联歌词,深度优先 372 return result; 373 } 374 } 375 const cache = Cache.get(musicItem); 376 let rawLrc = meta?.rawLrc || musicItem.rawLrc || cache?.rawLrc; 377 let lrcUrl = meta?.lrc || musicItem.lrc || cache?.lrc; 378 // 如果存在文本 379 if (rawLrc) { 380 return { 381 rawLrc, 382 lrc: lrcUrl, 383 }; 384 } 385 // 2.本地缓存 386 const localLrc = 387 meta?.[internalSerializeKey]?.local?.localLrc || 388 cache?.[internalSerializeKey]?.local?.localLrc; 389 if (localLrc && (await exists(localLrc))) { 390 rawLrc = await readFile(localLrc, 'utf8'); 391 return { 392 rawLrc, 393 lrc: lrcUrl, 394 }; 395 } 396 // 3.优先使用url 397 if (lrcUrl) { 398 try { 399 // 需要超时时间 axios timeout 但是没生效 400 rawLrc = (await axios.get(lrcUrl, {timeout: 1500})).data; 401 return { 402 rawLrc, 403 lrc: lrcUrl, 404 }; 405 } catch { 406 lrcUrl = undefined; 407 } 408 } 409 // 4. 如果地址失效 410 if (!lrcUrl) { 411 // 插件获得url 412 try { 413 let lrcSource; 414 if (from) { 415 lrcSource = await PluginManager.getByMedia( 416 musicItem, 417 )?.instance?.getLyric?.( 418 resetMediaItem(musicItem, undefined, true), 419 ); 420 } else { 421 lrcSource = await this.plugin.instance?.getLyric?.( 422 resetMediaItem(musicItem, undefined, true), 423 ); 424 } 425 426 rawLrc = lrcSource?.rawLrc; 427 lrcUrl = lrcSource?.lrc; 428 } catch (e: any) { 429 trace('插件获取歌词失败', e?.message, 'error'); 430 devLog('error', '插件获取歌词失败', e, e?.message); 431 } 432 } 433 // 5. 最后一次请求 434 if (rawLrc || lrcUrl) { 435 const filename = `${pathConst.lrcCachePath}${nanoid()}.lrc`; 436 if (lrcUrl) { 437 try { 438 rawLrc = (await axios.get(lrcUrl, {timeout: 1500})).data; 439 } catch {} 440 } 441 if (rawLrc) { 442 await writeFile(filename, rawLrc, 'utf8'); 443 // 写入缓存 444 Cache.update(musicItem, [ 445 [`${internalSerializeKey}.local.localLrc`, filename], 446 ]); 447 // 如果有meta 448 if (meta) { 449 MediaMeta.update(musicItem, [ 450 [`${internalSerializeKey}.local.localLrc`, filename], 451 ]); 452 } 453 return { 454 rawLrc, 455 lrc: lrcUrl, 456 }; 457 } 458 } 459 // 6. 如果是本地文件 460 const isDownloaded = LocalMusicSheet.isLocalMusic(musicItem); 461 if (musicItem.platform !== localPluginPlatform && isDownloaded) { 462 const res = await localFilePlugin.instance!.getLyric!(isDownloaded); 463 if (res) { 464 return res; 465 } 466 } 467 devLog('warn', '无歌词'); 468 469 return null; 470 } 471 472 /** 获取歌词文本 */ 473 async getLyricText( 474 musicItem: IMusic.IMusicItem, 475 ): Promise<string | undefined> { 476 return (await this.getLyric(musicItem))?.rawLrc; 477 } 478 479 /** 获取专辑信息 */ 480 async getAlbumInfo( 481 albumItem: IAlbum.IAlbumItemBase, 482 ): Promise<IAlbum.IAlbumItem | null> { 483 if (!this.plugin.instance.getAlbumInfo) { 484 return {...albumItem, musicList: []}; 485 } 486 try { 487 const result = await this.plugin.instance.getAlbumInfo( 488 resetMediaItem(albumItem, undefined, true), 489 ); 490 if (!result) { 491 throw new Error(); 492 } 493 result?.musicList?.forEach(_ => { 494 resetMediaItem(_, this.plugin.name); 495 }); 496 497 return {...albumItem, ...result}; 498 } catch (e: any) { 499 trace('获取专辑信息失败', e?.message); 500 devLog('error', '获取专辑信息失败', e, e?.message); 501 502 return {...albumItem, musicList: []}; 503 } 504 } 505 506 /** 查询作者信息 */ 507 async getArtistWorks<T extends IArtist.ArtistMediaType>( 508 artistItem: IArtist.IArtistItem, 509 page: number, 510 type: T, 511 ): Promise<IPlugin.ISearchResult<T>> { 512 if (!this.plugin.instance.getArtistWorks) { 513 return { 514 isEnd: true, 515 data: [], 516 }; 517 } 518 try { 519 const result = await this.plugin.instance.getArtistWorks( 520 artistItem, 521 page, 522 type, 523 ); 524 if (!result.data) { 525 return { 526 isEnd: true, 527 data: [], 528 }; 529 } 530 result.data?.forEach(_ => resetMediaItem(_, this.plugin.name)); 531 return { 532 isEnd: result.isEnd ?? true, 533 data: result.data, 534 }; 535 } catch (e: any) { 536 trace('查询作者信息失败', e?.message); 537 devLog('error', '查询作者信息失败', e, e?.message); 538 539 throw e; 540 } 541 } 542 543 /** 导入歌单 */ 544 async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> { 545 try { 546 const result = 547 (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? []; 548 result.forEach(_ => resetMediaItem(_, this.plugin.name)); 549 return result; 550 } catch (e: any) { 551 console.log(e); 552 devLog('error', '导入歌单失败', e, e?.message); 553 554 return []; 555 } 556 } 557 /** 导入单曲 */ 558 async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> { 559 try { 560 const result = await this.plugin.instance?.importMusicItem?.( 561 urlLike, 562 ); 563 if (!result) { 564 throw new Error(); 565 } 566 resetMediaItem(result, this.plugin.name); 567 return result; 568 } catch (e: any) { 569 devLog('error', '导入单曲失败', e, e?.message); 570 571 return null; 572 } 573 } 574 /** 获取榜单 */ 575 async getTopLists(): Promise<IMusic.IMusicTopListGroupItem[]> { 576 try { 577 const result = await this.plugin.instance?.getTopLists?.(); 578 if (!result) { 579 throw new Error(); 580 } 581 return result; 582 } catch (e: any) { 583 devLog('error', '获取榜单失败', e, e?.message); 584 return []; 585 } 586 } 587 /** 获取榜单详情 */ 588 async getTopListDetail( 589 topListItem: IMusic.IMusicTopListItem, 590 ): Promise<ICommon.WithMusicList<IMusic.IMusicTopListItem>> { 591 try { 592 const result = await this.plugin.instance?.getTopListDetail?.( 593 topListItem, 594 ); 595 if (!result) { 596 throw new Error(); 597 } 598 if (result.musicList) { 599 result.musicList.forEach(_ => 600 resetMediaItem(_, this.plugin.name), 601 ); 602 } 603 return result; 604 } catch (e: any) { 605 devLog('error', '获取榜单详情失败', e, e?.message); 606 return { 607 ...topListItem, 608 musicList: [], 609 }; 610 } 611 } 612} 613//#endregion 614 615let plugins: Array<Plugin> = []; 616const pluginStateMapper = new StateMapper(() => plugins); 617 618//#region 本地音乐插件 619/** 本地插件 */ 620const localFilePlugin = new Plugin(function () { 621 return { 622 platform: localPluginPlatform, 623 _path: '', 624 async getMusicInfo(musicBase) { 625 const localPath = getInternalData<string>( 626 musicBase, 627 InternalDataType.LOCALPATH, 628 ); 629 if (localPath) { 630 const coverImg = await Mp3Util.getMediaCoverImg(localPath); 631 return { 632 artwork: coverImg, 633 }; 634 } 635 return null; 636 }, 637 async getLyric(musicBase) { 638 const localPath = getInternalData<string>( 639 musicBase, 640 InternalDataType.LOCALPATH, 641 ); 642 let rawLrc: string | null = null; 643 if (localPath) { 644 // 读取内嵌歌词 645 try { 646 rawLrc = await Mp3Util.getLyric(localPath); 647 } catch (e) { 648 console.log('e', e); 649 } 650 if (!rawLrc) { 651 // 读取配置歌词 652 const lastDot = localPath.lastIndexOf('.'); 653 const lrcPath = localPath.slice(0, lastDot) + '.lrc'; 654 655 try { 656 if (await exists(lrcPath)) { 657 rawLrc = await readFile(lrcPath, 'utf8'); 658 } 659 } catch {} 660 } 661 } 662 663 return rawLrc 664 ? { 665 rawLrc, 666 } 667 : null; 668 }, 669 }; 670}, ''); 671localFilePlugin.hash = localPluginHash; 672 673//#endregion 674 675async function setup() { 676 const _plugins: Array<Plugin> = []; 677 try { 678 // 加载插件 679 const pluginsPaths = await readDir(pathConst.pluginPath); 680 for (let i = 0; i < pluginsPaths.length; ++i) { 681 const _pluginUrl = pluginsPaths[i]; 682 trace('初始化插件', _pluginUrl); 683 if ( 684 _pluginUrl.isFile() && 685 (_pluginUrl.name?.endsWith?.('.js') || 686 _pluginUrl.path?.endsWith?.('.js')) 687 ) { 688 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 689 const plugin = new Plugin(funcCode, _pluginUrl.path); 690 const _pluginIndex = _plugins.findIndex( 691 p => p.hash === plugin.hash, 692 ); 693 if (_pluginIndex !== -1) { 694 // 重复插件,直接忽略 695 return; 696 } 697 plugin.hash !== '' && _plugins.push(plugin); 698 } 699 } 700 701 plugins = _plugins; 702 pluginStateMapper.notify(); 703 /** 初始化meta信息 */ 704 PluginMeta.setupMeta(plugins.map(_ => _.name)); 705 } catch (e: any) { 706 ToastAndroid.show( 707 `插件初始化失败:${e?.message ?? e}`, 708 ToastAndroid.LONG, 709 ); 710 errorLog('插件初始化失败', e?.message); 711 throw e; 712 } 713} 714 715// 安装插件 716async function installPlugin(pluginPath: string) { 717 // if (pluginPath.endsWith('.js')) { 718 const funcCode = await readFile(pluginPath, 'utf8'); 719 const plugin = new Plugin(funcCode, pluginPath); 720 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 721 if (_pluginIndex !== -1) { 722 throw new Error('插件已安装'); 723 } 724 if (plugin.hash !== '') { 725 const fn = nanoid(); 726 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 727 await copyFile(pluginPath, _pluginPath); 728 plugin.path = _pluginPath; 729 plugins = plugins.concat(plugin); 730 pluginStateMapper.notify(); 731 return; 732 } 733 throw new Error('插件无法解析'); 734 // } 735 // throw new Error('插件不存在'); 736} 737 738async function installPluginFromUrl(url: string) { 739 try { 740 const funcCode = (await axios.get(url)).data; 741 if (funcCode) { 742 const plugin = new Plugin(funcCode, ''); 743 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 744 if (_pluginIndex !== -1) { 745 // 静默忽略 746 return; 747 } 748 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 749 if (oldVersionPlugin) { 750 if ( 751 compare( 752 oldVersionPlugin.instance.version ?? '', 753 plugin.instance.version ?? '', 754 '>', 755 ) 756 ) { 757 throw new Error('已安装更新版本的插件'); 758 } 759 } 760 761 if (plugin.hash !== '') { 762 const fn = nanoid(); 763 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 764 await writeFile(_pluginPath, funcCode, 'utf8'); 765 plugin.path = _pluginPath; 766 plugins = plugins.concat(plugin); 767 if (oldVersionPlugin) { 768 plugins = plugins.filter( 769 _ => _.hash !== oldVersionPlugin.hash, 770 ); 771 try { 772 await unlink(oldVersionPlugin.path); 773 } catch {} 774 } 775 pluginStateMapper.notify(); 776 return; 777 } 778 throw new Error('插件无法解析!'); 779 } 780 } catch (e: any) { 781 devLog('error', 'URL安装插件失败', e, e?.message); 782 errorLog('URL安装插件失败', e); 783 throw new Error(e?.message ?? ''); 784 } 785} 786 787/** 卸载插件 */ 788async function uninstallPlugin(hash: string) { 789 const targetIndex = plugins.findIndex(_ => _.hash === hash); 790 if (targetIndex !== -1) { 791 try { 792 const pluginName = plugins[targetIndex].name; 793 await unlink(plugins[targetIndex].path); 794 plugins = plugins.filter(_ => _.hash !== hash); 795 pluginStateMapper.notify(); 796 if (plugins.every(_ => _.name !== pluginName)) { 797 await MediaMeta.removePlugin(pluginName); 798 } 799 } catch {} 800 } 801} 802 803async function uninstallAllPlugins() { 804 await Promise.all( 805 plugins.map(async plugin => { 806 try { 807 const pluginName = plugin.name; 808 await unlink(plugin.path); 809 await MediaMeta.removePlugin(pluginName); 810 } catch (e) {} 811 }), 812 ); 813 plugins = []; 814 pluginStateMapper.notify(); 815 816 /** 清除空余文件,异步做就可以了 */ 817 readDir(pathConst.pluginPath) 818 .then(fns => { 819 fns.forEach(fn => { 820 unlink(fn.path).catch(emptyFunction); 821 }); 822 }) 823 .catch(emptyFunction); 824} 825 826async function updatePlugin(plugin: Plugin) { 827 const updateUrl = plugin.instance.srcUrl; 828 if (!updateUrl) { 829 throw new Error('没有更新源'); 830 } 831 try { 832 await installPluginFromUrl(updateUrl); 833 } catch (e: any) { 834 if (e.message === '插件已安装') { 835 throw new Error('当前已是最新版本'); 836 } else { 837 throw e; 838 } 839 } 840} 841 842function getByMedia(mediaItem: ICommon.IMediaBase) { 843 return getByName(mediaItem?.platform); 844} 845 846function getByHash(hash: string) { 847 return hash === localPluginHash 848 ? localFilePlugin 849 : plugins.find(_ => _.hash === hash); 850} 851 852function getByName(name: string) { 853 return name === localPluginPlatform 854 ? localFilePlugin 855 : plugins.find(_ => _.name === name); 856} 857 858function getValidPlugins() { 859 return plugins.filter(_ => _.state === 'enabled'); 860} 861 862function getSearchablePlugins() { 863 return plugins.filter(_ => _.state === 'enabled' && _.instance.search); 864} 865 866function getSortedSearchablePlugins() { 867 return getSearchablePlugins().sort((a, b) => 868 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 869 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 870 0 871 ? -1 872 : 1, 873 ); 874} 875 876function getTopListsablePlugins() { 877 return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists); 878} 879 880function getSortedTopListsablePlugins() { 881 return getTopListsablePlugins().sort((a, b) => 882 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 883 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 884 0 885 ? -1 886 : 1, 887 ); 888} 889 890function useSortedPlugins() { 891 const _plugins = pluginStateMapper.useMappedState(); 892 const _pluginMetaAll = PluginMeta.usePluginMetaAll(); 893 894 const [sortedPlugins, setSortedPlugins] = useState( 895 [..._plugins].sort((a, b) => 896 (_pluginMetaAll[a.name]?.order ?? Infinity) - 897 (_pluginMetaAll[b.name]?.order ?? Infinity) < 898 0 899 ? -1 900 : 1, 901 ), 902 ); 903 904 useEffect(() => { 905 InteractionManager.runAfterInteractions(() => { 906 setSortedPlugins( 907 [..._plugins].sort((a, b) => 908 (_pluginMetaAll[a.name]?.order ?? Infinity) - 909 (_pluginMetaAll[b.name]?.order ?? Infinity) < 910 0 911 ? -1 912 : 1, 913 ), 914 ); 915 }); 916 }, [_plugins, _pluginMetaAll]); 917 918 return sortedPlugins; 919} 920 921const PluginManager = { 922 setup, 923 installPlugin, 924 installPluginFromUrl, 925 updatePlugin, 926 uninstallPlugin, 927 getByMedia, 928 getByHash, 929 getByName, 930 getValidPlugins, 931 getSearchablePlugins, 932 getSortedSearchablePlugins, 933 getTopListsablePlugins, 934 getSortedTopListsablePlugins, 935 usePlugins: pluginStateMapper.useMappedState, 936 useSortedPlugins, 937 uninstallAllPlugins, 938}; 939 940export default PluginManager; 941