1import RNFS, { 2 copyFile, 3 exists, 4 readDir, 5 readFile, 6 stat, 7 unlink, 8 writeFile, 9} from 'react-native-fs'; 10import CryptoJs from 'crypto-js'; 11import dayjs from 'dayjs'; 12import axios from 'axios'; 13import bigInt from 'big-integer'; 14import qs from 'qs'; 15import * as webdav from 'webdav'; 16import {InteractionManager, ToastAndroid} from 'react-native'; 17import pathConst from '@/constants/pathConst'; 18import {compare, satisfies} from 'compare-versions'; 19import DeviceInfo from 'react-native-device-info'; 20import StateMapper from '@/utils/stateMapper'; 21import MediaExtra from './mediaExtra'; 22import {nanoid} from 'nanoid'; 23import {devLog, errorLog, trace} from '../utils/log'; 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 Mp3Util from '@/native/mp3Util'; 44import {PluginMeta} from './pluginMeta'; 45import {useEffect, useState} from 'react'; 46import {addFileScheme, getFileName} from '@/utils/fileUtils'; 47import {URL} from 'react-native-url-polyfill'; 48import Base64 from '@/utils/base64'; 49import MediaCache from './mediaCache'; 50import {produce} from 'immer'; 51import objectPath from 'object-path'; 52 53axios.defaults.timeout = 2000; 54 55const sha256 = CryptoJs.SHA256; 56 57export enum PluginStateCode { 58 /** 版本不匹配 */ 59 VersionNotMatch = 'VERSION NOT MATCH', 60 /** 无法解析 */ 61 CannotParse = 'CANNOT PARSE', 62} 63 64const packages: Record<string, any> = { 65 cheerio, 66 'crypto-js': CryptoJs, 67 axios, 68 dayjs, 69 'big-integer': bigInt, 70 qs, 71 he, 72 '@react-native-cookies/cookies': CookieManager, 73 webdav, 74}; 75 76const _require = (packageName: string) => { 77 let pkg = packages[packageName]; 78 pkg.default = pkg; 79 return pkg; 80}; 81 82const _consoleBind = function ( 83 method: 'log' | 'error' | 'info' | 'warn', 84 ...args: any 85) { 86 const fn = console[method]; 87 if (fn) { 88 fn(...args); 89 devLog(method, ...args); 90 } 91}; 92 93const _console = { 94 log: _consoleBind.bind(null, 'log'), 95 warn: _consoleBind.bind(null, 'warn'), 96 info: _consoleBind.bind(null, 'info'), 97 error: _consoleBind.bind(null, 'error'), 98}; 99 100function formatAuthUrl(url: string) { 101 const urlObj = new URL(url); 102 103 try { 104 if (urlObj.username && urlObj.password) { 105 const auth = `Basic ${Base64.btoa( 106 `${decodeURIComponent(urlObj.username)}:${decodeURIComponent( 107 urlObj.password, 108 )}`, 109 )}`; 110 urlObj.username = ''; 111 urlObj.password = ''; 112 113 return { 114 url: urlObj.toString(), 115 auth, 116 }; 117 } 118 } catch (e) { 119 return { 120 url, 121 }; 122 } 123 return { 124 url, 125 }; 126} 127 128//#region 插件类 129export class Plugin { 130 /** 插件名 */ 131 public name: string; 132 /** 插件的hash,作为唯一id */ 133 public hash: string; 134 /** 插件状态:激活、关闭、错误 */ 135 public state: 'enabled' | 'disabled' | 'error'; 136 /** 插件状态信息 */ 137 public stateCode?: PluginStateCode; 138 /** 插件的实例 */ 139 public instance: IPlugin.IPluginInstance; 140 /** 插件路径 */ 141 public path: string; 142 /** 插件方法 */ 143 public methods: PluginMethods; 144 145 constructor( 146 funcCode: string | (() => IPlugin.IPluginInstance), 147 pluginPath: string, 148 ) { 149 this.state = 'enabled'; 150 let _instance: IPlugin.IPluginInstance; 151 const _module: any = {exports: {}}; 152 try { 153 if (typeof funcCode === 'string') { 154 // 插件的环境变量 155 const env = { 156 getUserVariables: () => { 157 return ( 158 PluginMeta.getPluginMeta(this)?.userVariables ?? {} 159 ); 160 }, 161 os: 'android', 162 }; 163 164 // eslint-disable-next-line no-new-func 165 _instance = Function(` 166 'use strict'; 167 return function(require, __musicfree_require, module, exports, console, env, URL) { 168 ${funcCode} 169 } 170 `)()( 171 _require, 172 _require, 173 _module, 174 _module.exports, 175 _console, 176 env, 177 URL, 178 ); 179 if (_module.exports.default) { 180 _instance = _module.exports 181 .default as IPlugin.IPluginInstance; 182 } else { 183 _instance = _module.exports as IPlugin.IPluginInstance; 184 } 185 } else { 186 _instance = funcCode(); 187 } 188 // 插件初始化后的一些操作 189 if (Array.isArray(_instance.userVariables)) { 190 _instance.userVariables = _instance.userVariables.filter( 191 it => it?.key, 192 ); 193 } 194 this.checkValid(_instance); 195 } catch (e: any) { 196 console.log(e); 197 this.state = 'error'; 198 this.stateCode = PluginStateCode.CannotParse; 199 if (e?.stateCode) { 200 this.stateCode = e.stateCode; 201 } 202 errorLog(`${pluginPath}插件无法解析 `, { 203 stateCode: this.stateCode, 204 message: e?.message, 205 stack: e?.stack, 206 }); 207 _instance = e?.instance ?? { 208 _path: '', 209 platform: '', 210 appVersion: '', 211 async getMediaSource() { 212 return null; 213 }, 214 async search() { 215 return {}; 216 }, 217 async getAlbumInfo() { 218 return null; 219 }, 220 }; 221 } 222 this.instance = _instance; 223 this.path = pluginPath; 224 this.name = _instance.platform; 225 if ( 226 this.instance.platform === '' || 227 this.instance.platform === undefined 228 ) { 229 this.hash = ''; 230 } else { 231 if (typeof funcCode === 'string') { 232 this.hash = sha256(funcCode).toString(); 233 } else { 234 this.hash = sha256(funcCode.toString()).toString(); 235 } 236 } 237 238 // 放在最后 239 this.methods = new PluginMethods(this); 240 } 241 242 private checkValid(_instance: IPlugin.IPluginInstance) { 243 /** 版本号校验 */ 244 if ( 245 _instance.appVersion && 246 !satisfies(DeviceInfo.getVersion(), _instance.appVersion) 247 ) { 248 throw { 249 instance: _instance, 250 stateCode: PluginStateCode.VersionNotMatch, 251 }; 252 } 253 return true; 254 } 255} 256 257//#endregion 258 259//#region 基于插件类封装的方法,供给APP侧直接调用 260/** 有缓存等信息 */ 261class PluginMethods implements IPlugin.IPluginInstanceMethods { 262 private plugin; 263 264 constructor(plugin: Plugin) { 265 this.plugin = plugin; 266 } 267 268 /** 搜索 */ 269 async search<T extends ICommon.SupportMediaType>( 270 query: string, 271 page: number, 272 type: T, 273 ): Promise<IPlugin.ISearchResult<T>> { 274 if (!this.plugin.instance.search) { 275 return { 276 isEnd: true, 277 data: [], 278 }; 279 } 280 281 const result = 282 (await this.plugin.instance.search(query, page, type)) ?? {}; 283 if (Array.isArray(result.data)) { 284 result.data.forEach(_ => { 285 resetMediaItem(_, this.plugin.name); 286 }); 287 return { 288 isEnd: result.isEnd ?? true, 289 data: result.data, 290 }; 291 } 292 return { 293 isEnd: true, 294 data: [], 295 }; 296 } 297 298 /** 获取真实源 */ 299 async getMediaSource( 300 musicItem: IMusic.IMusicItemBase, 301 quality: IMusic.IQualityKey = 'standard', 302 retryCount = 1, 303 notUpdateCache = false, 304 ): Promise<IPlugin.IMediaSourceResult | null> { 305 // 1. 本地搜索 其实直接读mediameta就好了 306 const mediaExtra = MediaExtra.get(musicItem); 307 const localPath = 308 mediaExtra?.localPath || 309 getInternalData<string>(musicItem, InternalDataType.LOCALPATH) || 310 getInternalData<string>( 311 LocalMusicSheet.isLocalMusic(musicItem), 312 InternalDataType.LOCALPATH, 313 ); 314 if (localPath && (await exists(localPath))) { 315 trace('本地播放', localPath); 316 if (mediaExtra && mediaExtra.localPath !== localPath) { 317 // 修正一下本地数据 318 MediaExtra.update(musicItem, { 319 localPath, 320 }); 321 } 322 return { 323 url: addFileScheme(localPath), 324 }; 325 } else if (mediaExtra?.localPath) { 326 MediaExtra.update(musicItem, { 327 localPath: undefined, 328 }); 329 } 330 331 if (musicItem.platform === localPluginPlatform) { 332 throw new Error('本地音乐不存在'); 333 } 334 // 2. 缓存播放 335 const mediaCache = MediaCache.getMediaCache( 336 musicItem, 337 ) as IMusic.IMusicItem | null; 338 const pluginCacheControl = 339 this.plugin.instance.cacheControl ?? 'no-cache'; 340 if ( 341 mediaCache && 342 mediaCache?.source?.[quality]?.url && 343 (pluginCacheControl === CacheControl.Cache || 344 (pluginCacheControl === CacheControl.NoCache && 345 Network.isOffline())) 346 ) { 347 trace('播放', '缓存播放'); 348 const qualityInfo = mediaCache.source[quality]; 349 return { 350 url: qualityInfo!.url, 351 headers: mediaCache.headers, 352 userAgent: 353 mediaCache.userAgent ?? mediaCache.headers?.['user-agent'], 354 }; 355 } 356 // 3. 插件解析 357 if (!this.plugin.instance.getMediaSource) { 358 const {url, auth} = formatAuthUrl( 359 musicItem?.qualities?.[quality]?.url ?? musicItem.url, 360 ); 361 return { 362 url: url, 363 headers: auth 364 ? { 365 Authorization: auth, 366 } 367 : undefined, 368 }; 369 } 370 try { 371 const {url, headers} = (await this.plugin.instance.getMediaSource( 372 musicItem, 373 quality, 374 )) ?? {url: musicItem?.qualities?.[quality]?.url}; 375 if (!url) { 376 throw new Error('NOT RETRY'); 377 } 378 trace('播放', '插件播放'); 379 const result = { 380 url, 381 headers, 382 userAgent: headers?.['user-agent'], 383 } as IPlugin.IMediaSourceResult; 384 const authFormattedResult = formatAuthUrl(result.url!); 385 if (authFormattedResult.auth) { 386 result.url = authFormattedResult.url; 387 result.headers = { 388 ...(result.headers ?? {}), 389 Authorization: authFormattedResult.auth, 390 }; 391 } 392 393 if ( 394 pluginCacheControl !== CacheControl.NoStore && 395 !notUpdateCache 396 ) { 397 // 更新缓存 398 const cacheSource = { 399 headers: result.headers, 400 userAgent: result.userAgent, 401 url, 402 }; 403 let realMusicItem = { 404 ...musicItem, 405 ...(mediaCache || {}), 406 }; 407 realMusicItem.source = { 408 ...(realMusicItem.source || {}), 409 [quality]: cacheSource, 410 }; 411 412 MediaCache.setMediaCache(realMusicItem); 413 } 414 return result; 415 } catch (e: any) { 416 if (retryCount > 0 && e?.message !== 'NOT RETRY') { 417 await delay(150); 418 return this.getMediaSource(musicItem, quality, --retryCount); 419 } 420 errorLog('获取真实源失败', e?.message); 421 devLog('error', '获取真实源失败', e, e?.message); 422 return null; 423 } 424 } 425 426 /** 获取音乐详情 */ 427 async getMusicInfo( 428 musicItem: ICommon.IMediaBase, 429 ): Promise<Partial<IMusic.IMusicItem> | null> { 430 if (!this.plugin.instance.getMusicInfo) { 431 return null; 432 } 433 try { 434 return ( 435 this.plugin.instance.getMusicInfo( 436 resetMediaItem(musicItem, undefined, true), 437 ) ?? null 438 ); 439 } catch (e: any) { 440 devLog('error', '获取音乐详情失败', e, e?.message); 441 return null; 442 } 443 } 444 445 /** 446 * 447 * getLyric(musicItem) => { 448 * lyric: string; 449 * trans: string; 450 * } 451 * 452 */ 453 /** 获取歌词 */ 454 async getLyric( 455 originalMusicItem: IMusic.IMusicItemBase, 456 ): Promise<ILyric.ILyricSource | null> { 457 // 1.额外存储的meta信息(关联歌词) 458 const meta = MediaExtra.get(originalMusicItem); 459 let musicItem: IMusic.IMusicItem; 460 if (meta && meta.associatedLrc) { 461 musicItem = meta.associatedLrc as IMusic.IMusicItem; 462 } else { 463 musicItem = originalMusicItem as IMusic.IMusicItem; 464 } 465 466 const musicItemCache = MediaCache.getMediaCache( 467 musicItem, 468 ) as IMusic.IMusicItemCache | null; 469 470 /** 原始歌词文本 */ 471 let rawLrc: string | null = musicItem.rawLrc || null; 472 let translation: string | null = null; 473 474 // 2. 本地手动设置的歌词 475 const platformHash = CryptoJs.MD5(musicItem.platform).toString( 476 CryptoJs.enc.Hex, 477 ); 478 const idHash = CryptoJs.MD5(musicItem.id).toString(CryptoJs.enc.Hex); 479 if ( 480 await RNFS.exists( 481 pathConst.localLrcPath + platformHash + '/' + idHash + '.lrc', 482 ) 483 ) { 484 rawLrc = await RNFS.readFile( 485 pathConst.localLrcPath + platformHash + '/' + idHash + '.lrc', 486 'utf8', 487 ); 488 489 if ( 490 await RNFS.exists( 491 pathConst.localLrcPath + 492 platformHash + 493 '/' + 494 idHash + 495 '.tran.lrc', 496 ) 497 ) { 498 translation = 499 (await RNFS.readFile( 500 pathConst.localLrcPath + 501 platformHash + 502 '/' + 503 idHash + 504 '.tran.lrc', 505 'utf8', 506 )) || null; 507 } 508 509 return { 510 rawLrc, 511 translation: translation || undefined, // TODO: 这里写的不好 512 }; 513 } 514 515 // 2. 缓存歌词 / 对象上本身的歌词 516 if (musicItemCache?.lyric) { 517 // 缓存的远程结果 518 let cacheLyric: ILyric.ILyricSource | null = 519 musicItemCache.lyric || null; 520 // 缓存的本地结果 521 let localLyric: ILyric.ILyricSource | null = 522 musicItemCache.$localLyric || null; 523 524 // 优先用缓存的结果 525 if (cacheLyric.rawLrc || cacheLyric.translation) { 526 return { 527 rawLrc: cacheLyric.rawLrc, 528 translation: cacheLyric.translation, 529 }; 530 } 531 532 // 本地其实是缓存的路径 533 if (localLyric) { 534 let needRefetch = false; 535 if (localLyric.rawLrc && (await exists(localLyric.rawLrc))) { 536 rawLrc = await readFile(localLyric.rawLrc, 'utf8'); 537 } else if (localLyric.rawLrc) { 538 needRefetch = true; 539 } 540 if ( 541 localLyric.translation && 542 (await exists(localLyric.translation)) 543 ) { 544 translation = await readFile( 545 localLyric.translation, 546 'utf8', 547 ); 548 } else if (localLyric.translation) { 549 needRefetch = true; 550 } 551 552 if (!needRefetch && (rawLrc || translation)) { 553 return { 554 rawLrc: rawLrc || undefined, 555 translation: translation || undefined, 556 }; 557 } 558 } 559 } 560 561 // 3. 无缓存歌词/无自带歌词/无本地歌词 562 let lrcSource: ILyric.ILyricSource | null; 563 if (isSameMediaItem(originalMusicItem, musicItem)) { 564 lrcSource = 565 (await this.plugin.instance 566 ?.getLyric?.(resetMediaItem(musicItem, undefined, true)) 567 ?.catch(() => null)) || null; 568 } else { 569 lrcSource = 570 (await PluginManager.getByMedia(musicItem) 571 ?.instance?.getLyric?.( 572 resetMediaItem(musicItem, undefined, true), 573 ) 574 ?.catch(() => null)) || null; 575 } 576 577 if (lrcSource) { 578 rawLrc = lrcSource?.rawLrc || rawLrc; 579 translation = lrcSource?.translation || null; 580 581 const deprecatedLrcUrl = lrcSource?.lrc || musicItem.lrc; 582 583 // 本地的文件名 584 let filename: string | undefined = `${ 585 pathConst.lrcCachePath 586 }${nanoid()}.lrc`; 587 let filenameTrans: string | undefined = `${ 588 pathConst.lrcCachePath 589 }${nanoid()}.lrc`; 590 591 // 旧版本兼容 592 if (!(rawLrc || translation)) { 593 if (deprecatedLrcUrl) { 594 rawLrc = ( 595 await axios 596 .get(deprecatedLrcUrl, {timeout: 3000}) 597 .catch(() => null) 598 )?.data; 599 } else if (musicItem.rawLrc) { 600 rawLrc = musicItem.rawLrc; 601 } 602 } 603 604 if (rawLrc) { 605 await writeFile(filename, rawLrc, 'utf8'); 606 } else { 607 filename = undefined; 608 } 609 if (translation) { 610 await writeFile(filenameTrans, translation, 'utf8'); 611 } else { 612 filenameTrans = undefined; 613 } 614 615 if (rawLrc || translation) { 616 MediaCache.setMediaCache( 617 produce(musicItemCache || musicItem, draft => { 618 musicItemCache?.$localLyric?.rawLrc; 619 objectPath.set(draft, '$localLyric.rawLrc', filename); 620 objectPath.set( 621 draft, 622 '$localLyric.translation', 623 filenameTrans, 624 ); 625 return draft; 626 }), 627 ); 628 return { 629 rawLrc: rawLrc || undefined, 630 translation: translation || undefined, 631 }; 632 } 633 } 634 635 // 6. 如果是本地文件 636 const isDownloaded = LocalMusicSheet.isLocalMusic(originalMusicItem); 637 if ( 638 originalMusicItem.platform !== localPluginPlatform && 639 isDownloaded 640 ) { 641 const res = await localFilePlugin.instance!.getLyric!(isDownloaded); 642 643 console.log('本地文件歌词'); 644 645 if (res) { 646 return res; 647 } 648 } 649 devLog('warn', '无歌词'); 650 651 return null; 652 } 653 654 /** 获取歌词文本 */ 655 async getLyricText( 656 musicItem: IMusic.IMusicItem, 657 ): Promise<string | undefined> { 658 return (await this.getLyric(musicItem))?.rawLrc; 659 } 660 661 /** 获取专辑信息 */ 662 async getAlbumInfo( 663 albumItem: IAlbum.IAlbumItemBase, 664 page: number = 1, 665 ): Promise<IPlugin.IAlbumInfoResult | null> { 666 if (!this.plugin.instance.getAlbumInfo) { 667 return { 668 albumItem, 669 musicList: (albumItem?.musicList ?? []).map( 670 resetMediaItem, 671 this.plugin.name, 672 true, 673 ), 674 isEnd: true, 675 }; 676 } 677 try { 678 const result = await this.plugin.instance.getAlbumInfo( 679 resetMediaItem(albumItem, undefined, true), 680 page, 681 ); 682 if (!result) { 683 throw new Error(); 684 } 685 result?.musicList?.forEach(_ => { 686 resetMediaItem(_, this.plugin.name); 687 _.album = albumItem.title; 688 }); 689 690 if (page <= 1) { 691 // 合并信息 692 return { 693 albumItem: {...albumItem, ...(result?.albumItem ?? {})}, 694 isEnd: result.isEnd === false ? false : true, 695 musicList: result.musicList, 696 }; 697 } else { 698 return { 699 isEnd: result.isEnd === false ? false : true, 700 musicList: result.musicList, 701 }; 702 } 703 } catch (e: any) { 704 trace('获取专辑信息失败', e?.message); 705 devLog('error', '获取专辑信息失败', e, e?.message); 706 707 return null; 708 } 709 } 710 711 /** 获取歌单信息 */ 712 async getMusicSheetInfo( 713 sheetItem: IMusic.IMusicSheetItem, 714 page: number = 1, 715 ): Promise<IPlugin.ISheetInfoResult | null> { 716 if (!this.plugin.instance.getMusicSheetInfo) { 717 return { 718 sheetItem, 719 musicList: sheetItem?.musicList ?? [], 720 isEnd: true, 721 }; 722 } 723 try { 724 const result = await this.plugin.instance?.getMusicSheetInfo?.( 725 resetMediaItem(sheetItem, undefined, true), 726 page, 727 ); 728 if (!result) { 729 throw new Error(); 730 } 731 result?.musicList?.forEach(_ => { 732 resetMediaItem(_, this.plugin.name); 733 }); 734 735 if (page <= 1) { 736 // 合并信息 737 return { 738 sheetItem: {...sheetItem, ...(result?.sheetItem ?? {})}, 739 isEnd: result.isEnd === false ? false : true, 740 musicList: result.musicList, 741 }; 742 } else { 743 return { 744 isEnd: result.isEnd === false ? false : true, 745 musicList: result.musicList, 746 }; 747 } 748 } catch (e: any) { 749 trace('获取歌单信息失败', e, e?.message); 750 devLog('error', '获取歌单信息失败', e, e?.message); 751 752 return null; 753 } 754 } 755 756 /** 查询作者信息 */ 757 async getArtistWorks<T extends IArtist.ArtistMediaType>( 758 artistItem: IArtist.IArtistItem, 759 page: number, 760 type: T, 761 ): Promise<IPlugin.ISearchResult<T>> { 762 if (!this.plugin.instance.getArtistWorks) { 763 return { 764 isEnd: true, 765 data: [], 766 }; 767 } 768 try { 769 const result = await this.plugin.instance.getArtistWorks( 770 artistItem, 771 page, 772 type, 773 ); 774 if (!result.data) { 775 return { 776 isEnd: true, 777 data: [], 778 }; 779 } 780 result.data?.forEach(_ => resetMediaItem(_, this.plugin.name)); 781 return { 782 isEnd: result.isEnd ?? true, 783 data: result.data, 784 }; 785 } catch (e: any) { 786 trace('查询作者信息失败', e?.message); 787 devLog('error', '查询作者信息失败', e, e?.message); 788 789 throw e; 790 } 791 } 792 793 /** 导入歌单 */ 794 async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> { 795 try { 796 const result = 797 (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? []; 798 result.forEach(_ => resetMediaItem(_, this.plugin.name)); 799 return result; 800 } catch (e: any) { 801 console.log(e); 802 devLog('error', '导入歌单失败', e, e?.message); 803 804 return []; 805 } 806 } 807 808 /** 导入单曲 */ 809 async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> { 810 try { 811 const result = await this.plugin.instance?.importMusicItem?.( 812 urlLike, 813 ); 814 if (!result) { 815 throw new Error(); 816 } 817 resetMediaItem(result, this.plugin.name); 818 return result; 819 } catch (e: any) { 820 devLog('error', '导入单曲失败', e, e?.message); 821 822 return null; 823 } 824 } 825 826 /** 获取榜单 */ 827 async getTopLists(): Promise<IMusic.IMusicSheetGroupItem[]> { 828 try { 829 const result = await this.plugin.instance?.getTopLists?.(); 830 if (!result) { 831 throw new Error(); 832 } 833 return result; 834 } catch (e: any) { 835 devLog('error', '获取榜单失败', e, e?.message); 836 return []; 837 } 838 } 839 840 /** 获取榜单详情 */ 841 async getTopListDetail( 842 topListItem: IMusic.IMusicSheetItemBase, 843 page: number, 844 ): Promise<IPlugin.ITopListInfoResult> { 845 try { 846 const result = await this.plugin.instance?.getTopListDetail?.( 847 topListItem, 848 page, 849 ); 850 if (!result) { 851 throw new Error(); 852 } 853 if (result.musicList) { 854 result.musicList.forEach(_ => 855 resetMediaItem(_, this.plugin.name), 856 ); 857 } 858 if (result.isEnd !== false) { 859 result.isEnd = true; 860 } 861 return result; 862 } catch (e: any) { 863 devLog('error', '获取榜单详情失败', e, e?.message); 864 return { 865 isEnd: true, 866 topListItem: topListItem as IMusic.IMusicSheetItem, 867 musicList: [], 868 }; 869 } 870 } 871 872 /** 获取推荐歌单的tag */ 873 async getRecommendSheetTags(): Promise<IPlugin.IGetRecommendSheetTagsResult> { 874 try { 875 const result = 876 await this.plugin.instance?.getRecommendSheetTags?.(); 877 if (!result) { 878 throw new Error(); 879 } 880 return result; 881 } catch (e: any) { 882 devLog('error', '获取推荐歌单失败', e, e?.message); 883 return { 884 data: [], 885 }; 886 } 887 } 888 889 /** 获取某个tag的推荐歌单 */ 890 async getRecommendSheetsByTag( 891 tagItem: ICommon.IUnique, 892 page?: number, 893 ): Promise<ICommon.PaginationResponse<IMusic.IMusicSheetItemBase>> { 894 try { 895 const result = 896 await this.plugin.instance?.getRecommendSheetsByTag?.( 897 tagItem, 898 page ?? 1, 899 ); 900 if (!result) { 901 throw new Error(); 902 } 903 if (result.isEnd !== false) { 904 result.isEnd = true; 905 } 906 if (!result.data) { 907 result.data = []; 908 } 909 result.data.forEach(item => resetMediaItem(item, this.plugin.name)); 910 911 return result; 912 } catch (e: any) { 913 devLog('error', '获取推荐歌单详情失败', e, e?.message); 914 return { 915 isEnd: true, 916 data: [], 917 }; 918 } 919 } 920 921 async getMusicComments( 922 musicItem: IMusic.IMusicItem, 923 ): Promise<ICommon.PaginationResponse<IMedia.IComment>> { 924 const result = await this.plugin.instance?.getMusicComments?.( 925 musicItem, 926 ); 927 if (!result) { 928 throw new Error(); 929 } 930 if (result.isEnd !== false) { 931 result.isEnd = true; 932 } 933 if (!result.data) { 934 result.data = []; 935 } 936 937 return result; 938 } 939 940 async migrateFromOtherPlugin( 941 mediaItem: ICommon.IMediaBase, 942 fromPlatform: string, 943 ): Promise<{isOk: boolean; data?: ICommon.IMediaBase}> { 944 try { 945 const result = await this.plugin.instance?.migrateFromOtherPlugin( 946 mediaItem, 947 fromPlatform, 948 ); 949 950 if ( 951 result.isOk && 952 result.data?.id && 953 result.data?.platform === this.plugin.platform 954 ) { 955 return { 956 isOk: result.isOk, 957 data: result.data, 958 }; 959 } 960 return { 961 isOk: false, 962 }; 963 } catch { 964 return { 965 isOk: false, 966 }; 967 } 968 } 969} 970//#endregion 971 972let plugins: Array<Plugin> = []; 973const pluginStateMapper = new StateMapper(() => plugins); 974 975//#region 本地音乐插件 976/** 本地插件 */ 977const localFilePlugin = new Plugin(function () { 978 return { 979 platform: localPluginPlatform, 980 _path: '', 981 async getMusicInfo(musicBase) { 982 const localPath = getInternalData<string>( 983 musicBase, 984 InternalDataType.LOCALPATH, 985 ); 986 if (localPath) { 987 const coverImg = await Mp3Util.getMediaCoverImg(localPath); 988 return { 989 artwork: coverImg, 990 }; 991 } 992 return null; 993 }, 994 async getLyric(musicBase) { 995 const localPath = getInternalData<string>( 996 musicBase, 997 InternalDataType.LOCALPATH, 998 ); 999 let rawLrc: string | null = null; 1000 if (localPath) { 1001 // 读取内嵌歌词 1002 try { 1003 rawLrc = await Mp3Util.getLyric(localPath); 1004 } catch (e) { 1005 console.log('读取内嵌歌词失败', e); 1006 } 1007 if (!rawLrc) { 1008 // 读取配置歌词 1009 const lastDot = localPath.lastIndexOf('.'); 1010 const lrcPath = localPath.slice(0, lastDot) + '.lrc'; 1011 1012 try { 1013 if (await exists(lrcPath)) { 1014 rawLrc = await readFile(lrcPath, 'utf8'); 1015 } 1016 } catch {} 1017 } 1018 } 1019 1020 return rawLrc 1021 ? { 1022 rawLrc, 1023 } 1024 : null; 1025 }, 1026 async importMusicItem(urlLike) { 1027 let meta: any = {}; 1028 let id: string; 1029 1030 try { 1031 meta = await Mp3Util.getBasicMeta(urlLike); 1032 const fileStat = await stat(urlLike); 1033 id = 1034 CryptoJs.MD5(fileStat.originalFilepath).toString( 1035 CryptoJs.enc.Hex, 1036 ) || nanoid(); 1037 } catch { 1038 id = nanoid(); 1039 } 1040 1041 return { 1042 id: id, 1043 platform: '本地', 1044 title: meta?.title ?? getFileName(urlLike), 1045 artist: meta?.artist ?? '未知歌手', 1046 duration: parseInt(meta?.duration ?? '0', 10) / 1000, 1047 album: meta?.album ?? '未知专辑', 1048 artwork: '', 1049 [internalSerializeKey]: { 1050 localPath: urlLike, 1051 }, 1052 }; 1053 }, 1054 async getMediaSource(musicItem, quality) { 1055 if (quality === 'standard') { 1056 return { 1057 url: addFileScheme(musicItem.$?.localPath || musicItem.url), 1058 }; 1059 } 1060 return null; 1061 }, 1062 }; 1063}, ''); 1064localFilePlugin.hash = localPluginHash; 1065 1066//#endregion 1067 1068async function setup() { 1069 const _plugins: Array<Plugin> = []; 1070 try { 1071 // 加载插件 1072 const pluginsPaths = await readDir(pathConst.pluginPath); 1073 for (let i = 0; i < pluginsPaths.length; ++i) { 1074 const _pluginUrl = pluginsPaths[i]; 1075 trace('初始化插件', _pluginUrl); 1076 if ( 1077 _pluginUrl.isFile() && 1078 (_pluginUrl.name?.endsWith?.('.js') || 1079 _pluginUrl.path?.endsWith?.('.js')) 1080 ) { 1081 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 1082 const plugin = new Plugin(funcCode, _pluginUrl.path); 1083 const _pluginIndex = _plugins.findIndex( 1084 p => p.hash === plugin.hash, 1085 ); 1086 if (_pluginIndex !== -1) { 1087 // 重复插件,直接忽略 1088 continue; 1089 } 1090 plugin.hash !== '' && _plugins.push(plugin); 1091 } 1092 } 1093 1094 plugins = _plugins; 1095 /** 初始化meta信息 */ 1096 await PluginMeta.setupMeta(plugins.map(_ => _.name)); 1097 /** 查看一下是否有禁用的标记 */ 1098 const allMeta = PluginMeta.getPluginMetaAll() ?? {}; 1099 for (let plugin of plugins) { 1100 if (allMeta[plugin.name]?.enabled === false) { 1101 plugin.state = 'disabled'; 1102 } 1103 } 1104 pluginStateMapper.notify(); 1105 } catch (e: any) { 1106 ToastAndroid.show( 1107 `插件初始化失败:${e?.message ?? e}`, 1108 ToastAndroid.LONG, 1109 ); 1110 errorLog('插件初始化失败', e?.message); 1111 throw e; 1112 } 1113} 1114 1115interface IInstallPluginConfig { 1116 notCheckVersion?: boolean; 1117} 1118 1119async function installPluginFromRawCode( 1120 funcCode: string, 1121 config?: IInstallPluginConfig, 1122) { 1123 if (funcCode) { 1124 const plugin = new Plugin(funcCode, ''); 1125 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 1126 if (_pluginIndex !== -1) { 1127 // 静默忽略 1128 return plugin; 1129 } 1130 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 1131 if (oldVersionPlugin && !config?.notCheckVersion) { 1132 if ( 1133 compare( 1134 oldVersionPlugin.instance.version ?? '', 1135 plugin.instance.version ?? '', 1136 '>', 1137 ) 1138 ) { 1139 throw new Error('已安装更新版本的插件'); 1140 } 1141 } 1142 1143 if (plugin.hash !== '') { 1144 const fn = nanoid(); 1145 if (oldVersionPlugin) { 1146 plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash); 1147 try { 1148 await unlink(oldVersionPlugin.path); 1149 } catch {} 1150 } 1151 const pluginPath = `${pathConst.pluginPath}${fn}.js`; 1152 await writeFile(pluginPath, funcCode, 'utf8'); 1153 plugin.path = pluginPath; 1154 plugins = plugins.concat(plugin); 1155 pluginStateMapper.notify(); 1156 return plugin; 1157 } 1158 throw new Error('插件无法解析!'); 1159 } 1160} 1161 1162// 安装插件 1163async function installPlugin( 1164 pluginPath: string, 1165 config?: IInstallPluginConfig, 1166) { 1167 // if (pluginPath.endsWith('.js')) { 1168 const funcCode = await readFile(pluginPath, 'utf8'); 1169 1170 if (funcCode) { 1171 const plugin = new Plugin(funcCode, pluginPath); 1172 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 1173 if (_pluginIndex !== -1) { 1174 // 静默忽略 1175 return plugin; 1176 } 1177 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 1178 if (oldVersionPlugin && !config?.notCheckVersion) { 1179 if ( 1180 compare( 1181 oldVersionPlugin.instance.version ?? '', 1182 plugin.instance.version ?? '', 1183 '>', 1184 ) 1185 ) { 1186 throw new Error('已安装更新版本的插件'); 1187 } 1188 } 1189 1190 if (plugin.hash !== '') { 1191 const fn = nanoid(); 1192 if (oldVersionPlugin) { 1193 plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash); 1194 try { 1195 await unlink(oldVersionPlugin.path); 1196 } catch {} 1197 } 1198 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 1199 await copyFile(pluginPath, _pluginPath); 1200 plugin.path = _pluginPath; 1201 plugins = plugins.concat(plugin); 1202 pluginStateMapper.notify(); 1203 return plugin; 1204 } 1205 throw new Error('插件无法解析!'); 1206 } 1207 throw new Error('插件无法识别!'); 1208} 1209 1210const reqHeaders = { 1211 'Cache-Control': 'no-cache', 1212 Pragma: 'no-cache', 1213 Expires: '0', 1214}; 1215 1216async function installPluginFromUrl( 1217 url: string, 1218 config?: IInstallPluginConfig, 1219) { 1220 try { 1221 const funcCode = ( 1222 await axios.get(url, { 1223 headers: reqHeaders, 1224 }) 1225 ).data; 1226 if (funcCode) { 1227 const plugin = new Plugin(funcCode, ''); 1228 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 1229 if (_pluginIndex !== -1) { 1230 // 静默忽略 1231 return; 1232 } 1233 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 1234 if (oldVersionPlugin && !config?.notCheckVersion) { 1235 if ( 1236 compare( 1237 oldVersionPlugin.instance.version ?? '', 1238 plugin.instance.version ?? '', 1239 '>', 1240 ) 1241 ) { 1242 throw new Error('已安装更新版本的插件'); 1243 } 1244 } 1245 1246 if (plugin.hash !== '') { 1247 const fn = nanoid(); 1248 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 1249 await writeFile(_pluginPath, funcCode, 'utf8'); 1250 plugin.path = _pluginPath; 1251 plugins = plugins.concat(plugin); 1252 if (oldVersionPlugin) { 1253 plugins = plugins.filter( 1254 _ => _.hash !== oldVersionPlugin.hash, 1255 ); 1256 try { 1257 await unlink(oldVersionPlugin.path); 1258 } catch {} 1259 } 1260 pluginStateMapper.notify(); 1261 return; 1262 } 1263 throw new Error('插件无法解析!'); 1264 } 1265 } catch (e: any) { 1266 devLog('error', 'URL安装插件失败', e, e?.message); 1267 errorLog('URL安装插件失败', e); 1268 throw new Error(e?.message ?? ''); 1269 } 1270} 1271 1272/** 卸载插件 */ 1273async function uninstallPlugin(hash: string) { 1274 const targetIndex = plugins.findIndex(_ => _.hash === hash); 1275 if (targetIndex !== -1) { 1276 try { 1277 const pluginName = plugins[targetIndex].name; 1278 await unlink(plugins[targetIndex].path); 1279 plugins = plugins.filter(_ => _.hash !== hash); 1280 pluginStateMapper.notify(); 1281 // 防止其他重名 1282 if (plugins.every(_ => _.name !== pluginName)) { 1283 MediaExtra.removeAll(pluginName); 1284 } 1285 } catch {} 1286 } 1287} 1288 1289async function uninstallAllPlugins() { 1290 await Promise.all( 1291 plugins.map(async plugin => { 1292 try { 1293 const pluginName = plugin.name; 1294 await unlink(plugin.path); 1295 MediaExtra.removeAll(pluginName); 1296 } catch (e) {} 1297 }), 1298 ); 1299 plugins = []; 1300 pluginStateMapper.notify(); 1301 1302 /** 清除空余文件,异步做就可以了 */ 1303 readDir(pathConst.pluginPath) 1304 .then(fns => { 1305 fns.forEach(fn => { 1306 unlink(fn.path).catch(emptyFunction); 1307 }); 1308 }) 1309 .catch(emptyFunction); 1310} 1311 1312async function updatePlugin(plugin: Plugin) { 1313 const updateUrl = plugin.instance.srcUrl; 1314 if (!updateUrl) { 1315 throw new Error('没有更新源'); 1316 } 1317 try { 1318 await installPluginFromUrl(updateUrl); 1319 } catch (e: any) { 1320 if (e.message === '插件已安装') { 1321 throw new Error('当前已是最新版本'); 1322 } else { 1323 throw e; 1324 } 1325 } 1326} 1327 1328function getByMedia(mediaItem: ICommon.IMediaBase) { 1329 return getByName(mediaItem?.platform); 1330} 1331 1332function getByHash(hash: string) { 1333 return hash === localPluginHash 1334 ? localFilePlugin 1335 : plugins.find(_ => _.hash === hash); 1336} 1337 1338function getByName(name: string) { 1339 return name === localPluginPlatform 1340 ? localFilePlugin 1341 : plugins.find(_ => _.name === name); 1342} 1343 1344function getValidPlugins() { 1345 return plugins.filter(_ => _.state === 'enabled'); 1346} 1347 1348function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) { 1349 return plugins.filter( 1350 _ => 1351 _.state === 'enabled' && 1352 _.instance.search && 1353 (supportedSearchType && _.instance.supportedSearchType 1354 ? _.instance.supportedSearchType.includes(supportedSearchType) 1355 : true), 1356 ); 1357} 1358 1359function getSortedSearchablePlugins( 1360 supportedSearchType?: ICommon.SupportMediaType, 1361) { 1362 return getSearchablePlugins(supportedSearchType).sort((a, b) => 1363 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1364 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1365 0 1366 ? -1 1367 : 1, 1368 ); 1369} 1370 1371function getTopListsablePlugins() { 1372 return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists); 1373} 1374 1375function getSortedTopListsablePlugins() { 1376 return getTopListsablePlugins().sort((a, b) => 1377 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1378 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1379 0 1380 ? -1 1381 : 1, 1382 ); 1383} 1384 1385function getRecommendSheetablePlugins() { 1386 return plugins.filter( 1387 _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag, 1388 ); 1389} 1390 1391function getSortedRecommendSheetablePlugins() { 1392 return getRecommendSheetablePlugins().sort((a, b) => 1393 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1394 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1395 0 1396 ? -1 1397 : 1, 1398 ); 1399} 1400 1401function useSortedPlugins() { 1402 const _plugins = pluginStateMapper.useMappedState(); 1403 const _pluginMetaAll = PluginMeta.usePluginMetaAll(); 1404 1405 const [sortedPlugins, setSortedPlugins] = useState( 1406 [..._plugins].sort((a, b) => 1407 (_pluginMetaAll[a.name]?.order ?? Infinity) - 1408 (_pluginMetaAll[b.name]?.order ?? Infinity) < 1409 0 1410 ? -1 1411 : 1, 1412 ), 1413 ); 1414 1415 useEffect(() => { 1416 InteractionManager.runAfterInteractions(() => { 1417 setSortedPlugins( 1418 [..._plugins].sort((a, b) => 1419 (_pluginMetaAll[a.name]?.order ?? Infinity) - 1420 (_pluginMetaAll[b.name]?.order ?? Infinity) < 1421 0 1422 ? -1 1423 : 1, 1424 ), 1425 ); 1426 }); 1427 }, [_plugins, _pluginMetaAll]); 1428 1429 return sortedPlugins; 1430} 1431 1432async function setPluginEnabled(plugin: Plugin, enabled?: boolean) { 1433 const target = plugins.find(it => it.hash === plugin.hash); 1434 if (target) { 1435 target.state = enabled ? 'enabled' : 'disabled'; 1436 plugins = [...plugins]; 1437 pluginStateMapper.notify(); 1438 PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled); 1439 } 1440} 1441 1442const PluginManager = { 1443 setup, 1444 installPlugin, 1445 installPluginFromRawCode, 1446 installPluginFromUrl, 1447 updatePlugin, 1448 uninstallPlugin, 1449 getByMedia, 1450 getByHash, 1451 getByName, 1452 getValidPlugins, 1453 getSearchablePlugins, 1454 getSortedSearchablePlugins, 1455 getTopListsablePlugins, 1456 getSortedRecommendSheetablePlugins, 1457 getSortedTopListsablePlugins, 1458 usePlugins: pluginStateMapper.useMappedState, 1459 useSortedPlugins, 1460 uninstallAllPlugins, 1461 setPluginEnabled, 1462}; 1463 1464export default PluginManager; 1465