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