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