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