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