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