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