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