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