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