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