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