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