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