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 = 2000; 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 /** TODO 用户输入 */ 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: 2000})).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: 2000})).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 page: number = 1, 483 ): Promise<IPlugin.IAlbumInfoResult | null> { 484 if (!this.plugin.instance.getAlbumInfo) { 485 return { 486 albumItem, 487 musicList: albumItem?.musicList ?? [], 488 isEnd: true, 489 }; 490 } 491 try { 492 const result = await this.plugin.instance.getAlbumInfo( 493 resetMediaItem(albumItem, undefined, true), 494 page, 495 ); 496 if (!result) { 497 throw new Error(); 498 } 499 result?.musicList?.forEach(_ => { 500 resetMediaItem(_, this.plugin.name); 501 _.album = albumItem.title; 502 }); 503 504 if (page <= 1) { 505 // 合并信息 506 return { 507 albumItem: {...albumItem, ...(result?.albumItem ?? {})}, 508 isEnd: result.isEnd === false ? false : true, 509 musicList: result.musicList, 510 }; 511 } else { 512 return { 513 isEnd: result.isEnd === false ? false : true, 514 musicList: result.musicList, 515 }; 516 } 517 } catch (e: any) { 518 trace('获取专辑信息失败', e?.message); 519 devLog('error', '获取专辑信息失败', e, e?.message); 520 521 return null; 522 } 523 } 524 525 /** 查询作者信息 */ 526 async getArtistWorks<T extends IArtist.ArtistMediaType>( 527 artistItem: IArtist.IArtistItem, 528 page: number, 529 type: T, 530 ): Promise<IPlugin.ISearchResult<T>> { 531 if (!this.plugin.instance.getArtistWorks) { 532 return { 533 isEnd: true, 534 data: [], 535 }; 536 } 537 try { 538 const result = await this.plugin.instance.getArtistWorks( 539 artistItem, 540 page, 541 type, 542 ); 543 if (!result.data) { 544 return { 545 isEnd: true, 546 data: [], 547 }; 548 } 549 result.data?.forEach(_ => resetMediaItem(_, this.plugin.name)); 550 return { 551 isEnd: result.isEnd ?? true, 552 data: result.data, 553 }; 554 } catch (e: any) { 555 trace('查询作者信息失败', e?.message); 556 devLog('error', '查询作者信息失败', e, e?.message); 557 558 throw e; 559 } 560 } 561 562 /** 导入歌单 */ 563 async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> { 564 try { 565 const result = 566 (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? []; 567 result.forEach(_ => resetMediaItem(_, this.plugin.name)); 568 return result; 569 } catch (e: any) { 570 console.log(e); 571 devLog('error', '导入歌单失败', e, e?.message); 572 573 return []; 574 } 575 } 576 /** 导入单曲 */ 577 async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> { 578 try { 579 const result = await this.plugin.instance?.importMusicItem?.( 580 urlLike, 581 ); 582 if (!result) { 583 throw new Error(); 584 } 585 resetMediaItem(result, this.plugin.name); 586 return result; 587 } catch (e: any) { 588 devLog('error', '导入单曲失败', e, e?.message); 589 590 return null; 591 } 592 } 593 /** 获取榜单 */ 594 async getTopLists(): Promise<IMusic.IMusicTopListGroupItem[]> { 595 try { 596 const result = await this.plugin.instance?.getTopLists?.(); 597 if (!result) { 598 throw new Error(); 599 } 600 return result; 601 } catch (e: any) { 602 devLog('error', '获取榜单失败', e, e?.message); 603 return []; 604 } 605 } 606 /** 获取榜单详情 */ 607 async getTopListDetail( 608 topListItem: IMusic.IMusicTopListItem, 609 ): Promise<ICommon.WithMusicList<IMusic.IMusicTopListItem>> { 610 try { 611 const result = await this.plugin.instance?.getTopListDetail?.( 612 topListItem, 613 ); 614 if (!result) { 615 throw new Error(); 616 } 617 if (result.musicList) { 618 result.musicList.forEach(_ => 619 resetMediaItem(_, this.plugin.name), 620 ); 621 } 622 return result; 623 } catch (e: any) { 624 devLog('error', '获取榜单详情失败', e, e?.message); 625 return { 626 ...topListItem, 627 musicList: [], 628 }; 629 } 630 } 631} 632//#endregion 633 634let plugins: Array<Plugin> = []; 635const pluginStateMapper = new StateMapper(() => plugins); 636 637//#region 本地音乐插件 638/** 本地插件 */ 639const localFilePlugin = new Plugin(function () { 640 return { 641 platform: localPluginPlatform, 642 _path: '', 643 async getMusicInfo(musicBase) { 644 const localPath = getInternalData<string>( 645 musicBase, 646 InternalDataType.LOCALPATH, 647 ); 648 if (localPath) { 649 const coverImg = await Mp3Util.getMediaCoverImg(localPath); 650 return { 651 artwork: coverImg, 652 }; 653 } 654 return null; 655 }, 656 async getLyric(musicBase) { 657 const localPath = getInternalData<string>( 658 musicBase, 659 InternalDataType.LOCALPATH, 660 ); 661 let rawLrc: string | null = null; 662 if (localPath) { 663 // 读取内嵌歌词 664 try { 665 rawLrc = await Mp3Util.getLyric(localPath); 666 } catch (e) { 667 console.log('e', e); 668 } 669 if (!rawLrc) { 670 // 读取配置歌词 671 const lastDot = localPath.lastIndexOf('.'); 672 const lrcPath = localPath.slice(0, lastDot) + '.lrc'; 673 674 try { 675 if (await exists(lrcPath)) { 676 rawLrc = await readFile(lrcPath, 'utf8'); 677 } 678 } catch {} 679 } 680 } 681 682 return rawLrc 683 ? { 684 rawLrc, 685 } 686 : null; 687 }, 688 }; 689}, ''); 690localFilePlugin.hash = localPluginHash; 691 692//#endregion 693 694async function setup() { 695 const _plugins: Array<Plugin> = []; 696 try { 697 // 加载插件 698 const pluginsPaths = await readDir(pathConst.pluginPath); 699 for (let i = 0; i < pluginsPaths.length; ++i) { 700 const _pluginUrl = pluginsPaths[i]; 701 trace('初始化插件', _pluginUrl); 702 if ( 703 _pluginUrl.isFile() && 704 (_pluginUrl.name?.endsWith?.('.js') || 705 _pluginUrl.path?.endsWith?.('.js')) 706 ) { 707 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 708 const plugin = new Plugin(funcCode, _pluginUrl.path); 709 const _pluginIndex = _plugins.findIndex( 710 p => p.hash === plugin.hash, 711 ); 712 if (_pluginIndex !== -1) { 713 // 重复插件,直接忽略 714 return; 715 } 716 plugin.hash !== '' && _plugins.push(plugin); 717 } 718 } 719 720 plugins = _plugins; 721 pluginStateMapper.notify(); 722 /** 初始化meta信息 */ 723 PluginMeta.setupMeta(plugins.map(_ => _.name)); 724 } catch (e: any) { 725 ToastAndroid.show( 726 `插件初始化失败:${e?.message ?? e}`, 727 ToastAndroid.LONG, 728 ); 729 errorLog('插件初始化失败', e?.message); 730 throw e; 731 } 732} 733 734// 安装插件 735async function installPlugin(pluginPath: string) { 736 // if (pluginPath.endsWith('.js')) { 737 const funcCode = await readFile(pluginPath, 'utf8'); 738 const plugin = new Plugin(funcCode, pluginPath); 739 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 740 if (_pluginIndex !== -1) { 741 throw new Error('插件已安装'); 742 } 743 if (plugin.hash !== '') { 744 const fn = nanoid(); 745 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 746 await copyFile(pluginPath, _pluginPath); 747 plugin.path = _pluginPath; 748 plugins = plugins.concat(plugin); 749 pluginStateMapper.notify(); 750 return; 751 } 752 throw new Error('插件无法解析'); 753 // } 754 // throw new Error('插件不存在'); 755} 756 757async function installPluginFromUrl(url: string) { 758 try { 759 const funcCode = (await axios.get(url)).data; 760 if (funcCode) { 761 const plugin = new Plugin(funcCode, ''); 762 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 763 if (_pluginIndex !== -1) { 764 // 静默忽略 765 return; 766 } 767 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 768 if (oldVersionPlugin) { 769 if ( 770 compare( 771 oldVersionPlugin.instance.version ?? '', 772 plugin.instance.version ?? '', 773 '>', 774 ) 775 ) { 776 throw new Error('已安装更新版本的插件'); 777 } 778 } 779 780 if (plugin.hash !== '') { 781 const fn = nanoid(); 782 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 783 await writeFile(_pluginPath, funcCode, 'utf8'); 784 plugin.path = _pluginPath; 785 plugins = plugins.concat(plugin); 786 if (oldVersionPlugin) { 787 plugins = plugins.filter( 788 _ => _.hash !== oldVersionPlugin.hash, 789 ); 790 try { 791 await unlink(oldVersionPlugin.path); 792 } catch {} 793 } 794 pluginStateMapper.notify(); 795 return; 796 } 797 throw new Error('插件无法解析!'); 798 } 799 } catch (e: any) { 800 devLog('error', 'URL安装插件失败', e, e?.message); 801 errorLog('URL安装插件失败', e); 802 throw new Error(e?.message ?? ''); 803 } 804} 805 806/** 卸载插件 */ 807async function uninstallPlugin(hash: string) { 808 const targetIndex = plugins.findIndex(_ => _.hash === hash); 809 if (targetIndex !== -1) { 810 try { 811 const pluginName = plugins[targetIndex].name; 812 await unlink(plugins[targetIndex].path); 813 plugins = plugins.filter(_ => _.hash !== hash); 814 pluginStateMapper.notify(); 815 if (plugins.every(_ => _.name !== pluginName)) { 816 await MediaMeta.removePlugin(pluginName); 817 } 818 } catch {} 819 } 820} 821 822async function uninstallAllPlugins() { 823 await Promise.all( 824 plugins.map(async plugin => { 825 try { 826 const pluginName = plugin.name; 827 await unlink(plugin.path); 828 await MediaMeta.removePlugin(pluginName); 829 } catch (e) {} 830 }), 831 ); 832 plugins = []; 833 pluginStateMapper.notify(); 834 835 /** 清除空余文件,异步做就可以了 */ 836 readDir(pathConst.pluginPath) 837 .then(fns => { 838 fns.forEach(fn => { 839 unlink(fn.path).catch(emptyFunction); 840 }); 841 }) 842 .catch(emptyFunction); 843} 844 845async function updatePlugin(plugin: Plugin) { 846 const updateUrl = plugin.instance.srcUrl; 847 if (!updateUrl) { 848 throw new Error('没有更新源'); 849 } 850 try { 851 await installPluginFromUrl(updateUrl); 852 } catch (e: any) { 853 if (e.message === '插件已安装') { 854 throw new Error('当前已是最新版本'); 855 } else { 856 throw e; 857 } 858 } 859} 860 861function getByMedia(mediaItem: ICommon.IMediaBase) { 862 return getByName(mediaItem?.platform); 863} 864 865function getByHash(hash: string) { 866 return hash === localPluginHash 867 ? localFilePlugin 868 : plugins.find(_ => _.hash === hash); 869} 870 871function getByName(name: string) { 872 return name === localPluginPlatform 873 ? localFilePlugin 874 : plugins.find(_ => _.name === name); 875} 876 877function getValidPlugins() { 878 return plugins.filter(_ => _.state === 'enabled'); 879} 880 881function getSearchablePlugins() { 882 return plugins.filter(_ => _.state === 'enabled' && _.instance.search); 883} 884 885function getSortedSearchablePlugins() { 886 return getSearchablePlugins().sort((a, b) => 887 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 888 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 889 0 890 ? -1 891 : 1, 892 ); 893} 894 895function getTopListsablePlugins() { 896 return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists); 897} 898 899function getSortedTopListsablePlugins() { 900 return getTopListsablePlugins().sort((a, b) => 901 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 902 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 903 0 904 ? -1 905 : 1, 906 ); 907} 908 909function useSortedPlugins() { 910 const _plugins = pluginStateMapper.useMappedState(); 911 const _pluginMetaAll = PluginMeta.usePluginMetaAll(); 912 913 const [sortedPlugins, setSortedPlugins] = useState( 914 [..._plugins].sort((a, b) => 915 (_pluginMetaAll[a.name]?.order ?? Infinity) - 916 (_pluginMetaAll[b.name]?.order ?? Infinity) < 917 0 918 ? -1 919 : 1, 920 ), 921 ); 922 923 useEffect(() => { 924 InteractionManager.runAfterInteractions(() => { 925 setSortedPlugins( 926 [..._plugins].sort((a, b) => 927 (_pluginMetaAll[a.name]?.order ?? Infinity) - 928 (_pluginMetaAll[b.name]?.order ?? Infinity) < 929 0 930 ? -1 931 : 1, 932 ), 933 ); 934 }); 935 }, [_plugins, _pluginMetaAll]); 936 937 return sortedPlugins; 938} 939 940const PluginManager = { 941 setup, 942 installPlugin, 943 installPluginFromUrl, 944 updatePlugin, 945 uninstallPlugin, 946 getByMedia, 947 getByHash, 948 getByName, 949 getValidPlugins, 950 getSearchablePlugins, 951 getSortedSearchablePlugins, 952 getTopListsablePlugins, 953 getSortedTopListsablePlugins, 954 usePlugins: pluginStateMapper.useMappedState, 955 useSortedPlugins, 956 uninstallAllPlugins, 957}; 958 959export default PluginManager; 960