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