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 ?? [], 514 isEnd: true, 515 }; 516 } 517 try { 518 const result = await this.plugin.instance.getAlbumInfo( 519 resetMediaItem(albumItem, undefined, true), 520 page, 521 ); 522 if (!result) { 523 throw new Error(); 524 } 525 result?.musicList?.forEach(_ => { 526 resetMediaItem(_, this.plugin.name); 527 _.album = albumItem.title; 528 }); 529 530 if (page <= 1) { 531 // 合并信息 532 return { 533 albumItem: {...albumItem, ...(result?.albumItem ?? {})}, 534 isEnd: result.isEnd === false ? false : true, 535 musicList: result.musicList, 536 }; 537 } else { 538 return { 539 isEnd: result.isEnd === false ? false : true, 540 musicList: result.musicList, 541 }; 542 } 543 } catch (e: any) { 544 trace('获取专辑信息失败', e?.message); 545 devLog('error', '获取专辑信息失败', e, e?.message); 546 547 return null; 548 } 549 } 550 551 /** 获取歌单信息 */ 552 async getMusicSheetInfo( 553 sheetItem: IMusic.IMusicSheetItem, 554 page: number = 1, 555 ): Promise<IPlugin.ISheetInfoResult | null> { 556 if (!this.plugin.instance.getMusicSheetInfo) { 557 return { 558 sheetItem, 559 musicList: sheetItem?.musicList ?? [], 560 isEnd: true, 561 }; 562 } 563 try { 564 const result = await this.plugin.instance?.getMusicSheetInfo?.( 565 resetMediaItem(sheetItem, undefined, true), 566 page, 567 ); 568 if (!result) { 569 throw new Error(); 570 } 571 result?.musicList?.forEach(_ => { 572 resetMediaItem(_, this.plugin.name); 573 }); 574 575 if (page <= 1) { 576 // 合并信息 577 return { 578 sheetItem: {...sheetItem, ...(result?.sheetItem ?? {})}, 579 isEnd: result.isEnd === false ? false : true, 580 musicList: result.musicList, 581 }; 582 } else { 583 return { 584 isEnd: result.isEnd === false ? false : true, 585 musicList: result.musicList, 586 }; 587 } 588 } catch (e: any) { 589 trace('获取歌单信息失败', e, e?.message); 590 devLog('error', '获取歌单信息失败', e, e?.message); 591 592 return null; 593 } 594 } 595 596 /** 查询作者信息 */ 597 async getArtistWorks<T extends IArtist.ArtistMediaType>( 598 artistItem: IArtist.IArtistItem, 599 page: number, 600 type: T, 601 ): Promise<IPlugin.ISearchResult<T>> { 602 if (!this.plugin.instance.getArtistWorks) { 603 return { 604 isEnd: true, 605 data: [], 606 }; 607 } 608 try { 609 const result = await this.plugin.instance.getArtistWorks( 610 artistItem, 611 page, 612 type, 613 ); 614 if (!result.data) { 615 return { 616 isEnd: true, 617 data: [], 618 }; 619 } 620 result.data?.forEach(_ => resetMediaItem(_, this.plugin.name)); 621 return { 622 isEnd: result.isEnd ?? true, 623 data: result.data, 624 }; 625 } catch (e: any) { 626 trace('查询作者信息失败', e?.message); 627 devLog('error', '查询作者信息失败', e, e?.message); 628 629 throw e; 630 } 631 } 632 633 /** 导入歌单 */ 634 async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> { 635 try { 636 const result = 637 (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? []; 638 result.forEach(_ => resetMediaItem(_, this.plugin.name)); 639 return result; 640 } catch (e: any) { 641 console.log(e); 642 devLog('error', '导入歌单失败', e, e?.message); 643 644 return []; 645 } 646 } 647 /** 导入单曲 */ 648 async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> { 649 try { 650 const result = await this.plugin.instance?.importMusicItem?.( 651 urlLike, 652 ); 653 if (!result) { 654 throw new Error(); 655 } 656 resetMediaItem(result, this.plugin.name); 657 return result; 658 } catch (e: any) { 659 devLog('error', '导入单曲失败', e, e?.message); 660 661 return null; 662 } 663 } 664 /** 获取榜单 */ 665 async getTopLists(): Promise<IMusic.IMusicSheetGroupItem[]> { 666 try { 667 const result = await this.plugin.instance?.getTopLists?.(); 668 if (!result) { 669 throw new Error(); 670 } 671 return result; 672 } catch (e: any) { 673 devLog('error', '获取榜单失败', e, e?.message); 674 return []; 675 } 676 } 677 /** 获取榜单详情 */ 678 async getTopListDetail( 679 topListItem: IMusic.IMusicSheetItemBase, 680 ): Promise<ICommon.WithMusicList<IMusic.IMusicSheetItemBase>> { 681 try { 682 const result = await this.plugin.instance?.getTopListDetail?.( 683 topListItem, 684 ); 685 if (!result) { 686 throw new Error(); 687 } 688 if (result.musicList) { 689 result.musicList.forEach(_ => 690 resetMediaItem(_, this.plugin.name), 691 ); 692 } 693 return result; 694 } catch (e: any) { 695 devLog('error', '获取榜单详情失败', e, e?.message); 696 return { 697 ...topListItem, 698 musicList: [], 699 }; 700 } 701 } 702 703 /** 获取推荐歌单的tag */ 704 async getRecommendSheetTags(): Promise<IPlugin.IGetRecommendSheetTagsResult> { 705 try { 706 const result = 707 await this.plugin.instance?.getRecommendSheetTags?.(); 708 if (!result) { 709 throw new Error(); 710 } 711 return result; 712 } catch (e: any) { 713 devLog('error', '获取推荐歌单失败', e, e?.message); 714 return { 715 data: [], 716 }; 717 } 718 } 719 /** 获取某个tag的推荐歌单 */ 720 async getRecommendSheetsByTag( 721 tagItem: ICommon.IUnique, 722 page?: number, 723 ): Promise<ICommon.PaginationResponse<IMusic.IMusicSheetItemBase>> { 724 try { 725 const result = 726 await this.plugin.instance?.getRecommendSheetsByTag?.( 727 tagItem, 728 page ?? 1, 729 ); 730 if (!result) { 731 throw new Error(); 732 } 733 if (result.isEnd !== false) { 734 result.isEnd = true; 735 } 736 if (!result.data) { 737 result.data = []; 738 } 739 result.data.forEach(item => resetMediaItem(item, this.plugin.name)); 740 741 return result; 742 } catch (e: any) { 743 devLog('error', '获取推荐歌单详情失败', e, e?.message); 744 return { 745 isEnd: true, 746 data: [], 747 }; 748 } 749 } 750} 751//#endregion 752 753let plugins: Array<Plugin> = []; 754const pluginStateMapper = new StateMapper(() => plugins); 755 756//#region 本地音乐插件 757/** 本地插件 */ 758const localFilePlugin = new Plugin(function () { 759 return { 760 platform: localPluginPlatform, 761 _path: '', 762 async getMusicInfo(musicBase) { 763 const localPath = getInternalData<string>( 764 musicBase, 765 InternalDataType.LOCALPATH, 766 ); 767 if (localPath) { 768 const coverImg = await Mp3Util.getMediaCoverImg(localPath); 769 return { 770 artwork: coverImg, 771 }; 772 } 773 return null; 774 }, 775 async getLyric(musicBase) { 776 const localPath = getInternalData<string>( 777 musicBase, 778 InternalDataType.LOCALPATH, 779 ); 780 let rawLrc: string | null = null; 781 if (localPath) { 782 // 读取内嵌歌词 783 try { 784 rawLrc = await Mp3Util.getLyric(localPath); 785 } catch (e) { 786 console.log('读取内嵌歌词失败', e); 787 } 788 if (!rawLrc) { 789 // 读取配置歌词 790 const lastDot = localPath.lastIndexOf('.'); 791 const lrcPath = localPath.slice(0, lastDot) + '.lrc'; 792 793 try { 794 if (await exists(lrcPath)) { 795 rawLrc = await readFile(lrcPath, 'utf8'); 796 } 797 } catch {} 798 } 799 } 800 801 return rawLrc 802 ? { 803 rawLrc, 804 } 805 : null; 806 }, 807 async importMusicItem(urlLike) { 808 let meta: any = {}; 809 try { 810 meta = await Mp3Util.getBasicMeta(urlLike); 811 } catch {} 812 const id = await FileSystem.hash(urlLike, 'MD5'); 813 return { 814 id: id, 815 platform: '本地', 816 title: meta?.title ?? getFileName(urlLike), 817 artist: meta?.artist ?? '未知歌手', 818 duration: parseInt(meta?.duration ?? '0') / 1000, 819 album: meta?.album ?? '未知专辑', 820 artwork: '', 821 [internalSerializeKey]: { 822 localPath: urlLike, 823 }, 824 }; 825 }, 826 }; 827}, ''); 828localFilePlugin.hash = localPluginHash; 829 830//#endregion 831 832async function setup() { 833 const _plugins: Array<Plugin> = []; 834 try { 835 // 加载插件 836 const pluginsPaths = await readDir(pathConst.pluginPath); 837 for (let i = 0; i < pluginsPaths.length; ++i) { 838 const _pluginUrl = pluginsPaths[i]; 839 trace('初始化插件', _pluginUrl); 840 if ( 841 _pluginUrl.isFile() && 842 (_pluginUrl.name?.endsWith?.('.js') || 843 _pluginUrl.path?.endsWith?.('.js')) 844 ) { 845 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 846 const plugin = new Plugin(funcCode, _pluginUrl.path); 847 const _pluginIndex = _plugins.findIndex( 848 p => p.hash === plugin.hash, 849 ); 850 if (_pluginIndex !== -1) { 851 // 重复插件,直接忽略 852 continue; 853 } 854 plugin.hash !== '' && _plugins.push(plugin); 855 } 856 } 857 858 plugins = _plugins; 859 /** 初始化meta信息 */ 860 await PluginMeta.setupMeta(plugins.map(_ => _.name)); 861 /** 查看一下是否有禁用的标记 */ 862 const allMeta = PluginMeta.getPluginMetaAll() ?? {}; 863 for (let plugin of plugins) { 864 if (allMeta[plugin.name]?.enabled === false) { 865 plugin.state = 'disabled'; 866 } 867 } 868 pluginStateMapper.notify(); 869 } catch (e: any) { 870 ToastAndroid.show( 871 `插件初始化失败:${e?.message ?? e}`, 872 ToastAndroid.LONG, 873 ); 874 errorLog('插件初始化失败', e?.message); 875 throw e; 876 } 877} 878 879interface IInstallPluginConfig { 880 notCheckVersion?: boolean; 881} 882 883// 安装插件 884async function installPlugin( 885 pluginPath: string, 886 config?: IInstallPluginConfig, 887) { 888 // if (pluginPath.endsWith('.js')) { 889 const funcCode = await readFile(pluginPath, 'utf8'); 890 891 if (funcCode) { 892 const plugin = new Plugin(funcCode, pluginPath); 893 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 894 if (_pluginIndex !== -1) { 895 // 静默忽略 896 return plugin; 897 } 898 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 899 if (oldVersionPlugin && !config?.notCheckVersion) { 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 if (oldVersionPlugin) { 914 plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash); 915 try { 916 await unlink(oldVersionPlugin.path); 917 } catch {} 918 } 919 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 920 await copyFile(pluginPath, _pluginPath); 921 plugin.path = _pluginPath; 922 plugins = plugins.concat(plugin); 923 pluginStateMapper.notify(); 924 return plugin; 925 } 926 throw new Error('插件无法解析!'); 927 } 928 throw new Error('插件无法识别!'); 929} 930 931async function installPluginFromUrl( 932 url: string, 933 config?: IInstallPluginConfig, 934) { 935 try { 936 const funcCode = (await axios.get(url)).data; 937 if (funcCode) { 938 const plugin = new Plugin(funcCode, ''); 939 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 940 if (_pluginIndex !== -1) { 941 // 静默忽略 942 return; 943 } 944 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 945 if (oldVersionPlugin && !config?.notCheckVersion) { 946 if ( 947 compare( 948 oldVersionPlugin.instance.version ?? '', 949 plugin.instance.version ?? '', 950 '>', 951 ) 952 ) { 953 throw new Error('已安装更新版本的插件'); 954 } 955 } 956 957 if (plugin.hash !== '') { 958 const fn = nanoid(); 959 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 960 await writeFile(_pluginPath, funcCode, 'utf8'); 961 plugin.path = _pluginPath; 962 plugins = plugins.concat(plugin); 963 if (oldVersionPlugin) { 964 plugins = plugins.filter( 965 _ => _.hash !== oldVersionPlugin.hash, 966 ); 967 try { 968 await unlink(oldVersionPlugin.path); 969 } catch {} 970 } 971 pluginStateMapper.notify(); 972 return; 973 } 974 throw new Error('插件无法解析!'); 975 } 976 } catch (e: any) { 977 devLog('error', 'URL安装插件失败', e, e?.message); 978 errorLog('URL安装插件失败', e); 979 throw new Error(e?.message ?? ''); 980 } 981} 982 983/** 卸载插件 */ 984async function uninstallPlugin(hash: string) { 985 const targetIndex = plugins.findIndex(_ => _.hash === hash); 986 if (targetIndex !== -1) { 987 try { 988 const pluginName = plugins[targetIndex].name; 989 await unlink(plugins[targetIndex].path); 990 plugins = plugins.filter(_ => _.hash !== hash); 991 pluginStateMapper.notify(); 992 if (plugins.every(_ => _.name !== pluginName)) { 993 await MediaMeta.removePlugin(pluginName); 994 } 995 } catch {} 996 } 997} 998 999async function uninstallAllPlugins() { 1000 await Promise.all( 1001 plugins.map(async plugin => { 1002 try { 1003 const pluginName = plugin.name; 1004 await unlink(plugin.path); 1005 await MediaMeta.removePlugin(pluginName); 1006 } catch (e) {} 1007 }), 1008 ); 1009 plugins = []; 1010 pluginStateMapper.notify(); 1011 1012 /** 清除空余文件,异步做就可以了 */ 1013 readDir(pathConst.pluginPath) 1014 .then(fns => { 1015 fns.forEach(fn => { 1016 unlink(fn.path).catch(emptyFunction); 1017 }); 1018 }) 1019 .catch(emptyFunction); 1020} 1021 1022async function updatePlugin(plugin: Plugin) { 1023 const updateUrl = plugin.instance.srcUrl; 1024 if (!updateUrl) { 1025 throw new Error('没有更新源'); 1026 } 1027 try { 1028 await installPluginFromUrl(updateUrl); 1029 } catch (e: any) { 1030 if (e.message === '插件已安装') { 1031 throw new Error('当前已是最新版本'); 1032 } else { 1033 throw e; 1034 } 1035 } 1036} 1037 1038function getByMedia(mediaItem: ICommon.IMediaBase) { 1039 return getByName(mediaItem?.platform); 1040} 1041 1042function getByHash(hash: string) { 1043 return hash === localPluginHash 1044 ? localFilePlugin 1045 : plugins.find(_ => _.hash === hash); 1046} 1047 1048function getByName(name: string) { 1049 return name === localPluginPlatform 1050 ? localFilePlugin 1051 : plugins.find(_ => _.name === name); 1052} 1053 1054function getValidPlugins() { 1055 return plugins.filter(_ => _.state === 'enabled'); 1056} 1057 1058function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) { 1059 return plugins.filter( 1060 _ => 1061 _.state === 'enabled' && 1062 _.instance.search && 1063 (supportedSearchType && _.instance.supportedSearchType 1064 ? _.instance.supportedSearchType.includes(supportedSearchType) 1065 : true), 1066 ); 1067} 1068 1069function getSortedSearchablePlugins( 1070 supportedSearchType?: ICommon.SupportMediaType, 1071) { 1072 return getSearchablePlugins(supportedSearchType).sort((a, b) => 1073 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1074 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1075 0 1076 ? -1 1077 : 1, 1078 ); 1079} 1080 1081function getTopListsablePlugins() { 1082 return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists); 1083} 1084 1085function getSortedTopListsablePlugins() { 1086 return getTopListsablePlugins().sort((a, b) => 1087 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1088 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1089 0 1090 ? -1 1091 : 1, 1092 ); 1093} 1094 1095function getRecommendSheetablePlugins() { 1096 return plugins.filter( 1097 _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag, 1098 ); 1099} 1100 1101function getSortedRecommendSheetablePlugins() { 1102 return getRecommendSheetablePlugins().sort((a, b) => 1103 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1104 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1105 0 1106 ? -1 1107 : 1, 1108 ); 1109} 1110 1111function useSortedPlugins() { 1112 const _plugins = pluginStateMapper.useMappedState(); 1113 const _pluginMetaAll = PluginMeta.usePluginMetaAll(); 1114 1115 const [sortedPlugins, setSortedPlugins] = useState( 1116 [..._plugins].sort((a, b) => 1117 (_pluginMetaAll[a.name]?.order ?? Infinity) - 1118 (_pluginMetaAll[b.name]?.order ?? Infinity) < 1119 0 1120 ? -1 1121 : 1, 1122 ), 1123 ); 1124 1125 useEffect(() => { 1126 InteractionManager.runAfterInteractions(() => { 1127 setSortedPlugins( 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 }, [_plugins, _pluginMetaAll]); 1138 1139 return sortedPlugins; 1140} 1141 1142async function setPluginEnabled(plugin: Plugin, enabled?: boolean) { 1143 const target = plugins.find(it => it.hash === plugin.hash); 1144 if (target) { 1145 target.state = enabled ? 'enabled' : 'disabled'; 1146 plugins = [...plugins]; 1147 pluginStateMapper.notify(); 1148 PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled); 1149 } 1150} 1151 1152const PluginManager = { 1153 setup, 1154 installPlugin, 1155 installPluginFromUrl, 1156 updatePlugin, 1157 uninstallPlugin, 1158 getByMedia, 1159 getByHash, 1160 getByName, 1161 getValidPlugins, 1162 getSearchablePlugins, 1163 getSortedSearchablePlugins, 1164 getTopListsablePlugins, 1165 getSortedRecommendSheetablePlugins, 1166 getSortedTopListsablePlugins, 1167 usePlugins: pluginStateMapper.useMappedState, 1168 useSortedPlugins, 1169 uninstallAllPlugins, 1170 setPluginEnabled, 1171}; 1172 1173export default PluginManager; 1174