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