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 async migrateFromOtherPlugin( 934 mediaItem: ICommon.IMediaBase, 935 fromPlatform: string, 936 ): Promise<{isOk: boolean; data?: ICommon.IMediaBase}> { 937 try { 938 const result = await this.plugin.instance?.migrateFromOtherPlugin( 939 mediaItem, 940 fromPlatform, 941 ); 942 943 if ( 944 result.isOk && 945 result.data?.id && 946 result.data?.platform === this.plugin.platform 947 ) { 948 return { 949 isOk: result.isOk, 950 data: result.data, 951 }; 952 } 953 return { 954 isOk: false, 955 }; 956 } catch { 957 return { 958 isOk: false, 959 }; 960 } 961 } 962} 963//#endregion 964 965let plugins: Array<Plugin> = []; 966const pluginStateMapper = new StateMapper(() => plugins); 967 968//#region 本地音乐插件 969/** 本地插件 */ 970const localFilePlugin = new Plugin(function () { 971 return { 972 platform: localPluginPlatform, 973 _path: '', 974 async getMusicInfo(musicBase) { 975 const localPath = getInternalData<string>( 976 musicBase, 977 InternalDataType.LOCALPATH, 978 ); 979 if (localPath) { 980 const coverImg = await Mp3Util.getMediaCoverImg(localPath); 981 return { 982 artwork: coverImg, 983 }; 984 } 985 return null; 986 }, 987 async getLyric(musicBase) { 988 const localPath = getInternalData<string>( 989 musicBase, 990 InternalDataType.LOCALPATH, 991 ); 992 let rawLrc: string | null = null; 993 if (localPath) { 994 // 读取内嵌歌词 995 try { 996 rawLrc = await Mp3Util.getLyric(localPath); 997 } catch (e) { 998 console.log('读取内嵌歌词失败', e); 999 } 1000 if (!rawLrc) { 1001 // 读取配置歌词 1002 const lastDot = localPath.lastIndexOf('.'); 1003 const lrcPath = localPath.slice(0, lastDot) + '.lrc'; 1004 1005 try { 1006 if (await exists(lrcPath)) { 1007 rawLrc = await readFile(lrcPath, 'utf8'); 1008 } 1009 } catch {} 1010 } 1011 } 1012 1013 return rawLrc 1014 ? { 1015 rawLrc, 1016 } 1017 : null; 1018 }, 1019 async importMusicItem(urlLike) { 1020 let meta: any = {}; 1021 try { 1022 meta = await Mp3Util.getBasicMeta(urlLike); 1023 } catch {} 1024 const stat = await getInfoAsync(urlLike, { 1025 md5: true, 1026 }); 1027 let id: string; 1028 if (stat.exists) { 1029 id = stat.md5 || nanoid(); 1030 } else { 1031 id = nanoid(); 1032 } 1033 return { 1034 id: id, 1035 platform: '本地', 1036 title: meta?.title ?? getFileName(urlLike), 1037 artist: meta?.artist ?? '未知歌手', 1038 duration: parseInt(meta?.duration ?? '0', 10) / 1000, 1039 album: meta?.album ?? '未知专辑', 1040 artwork: '', 1041 [internalSerializeKey]: { 1042 localPath: urlLike, 1043 }, 1044 }; 1045 }, 1046 async getMediaSource(musicItem, quality) { 1047 if (quality === 'standard') { 1048 return { 1049 url: addFileScheme(musicItem.$?.localPath || musicItem.url), 1050 }; 1051 } 1052 return null; 1053 }, 1054 }; 1055}, ''); 1056localFilePlugin.hash = localPluginHash; 1057 1058//#endregion 1059 1060async function setup() { 1061 const _plugins: Array<Plugin> = []; 1062 try { 1063 // 加载插件 1064 const pluginsPaths = await readDir(pathConst.pluginPath); 1065 for (let i = 0; i < pluginsPaths.length; ++i) { 1066 const _pluginUrl = pluginsPaths[i]; 1067 trace('初始化插件', _pluginUrl); 1068 if ( 1069 _pluginUrl.isFile() && 1070 (_pluginUrl.name?.endsWith?.('.js') || 1071 _pluginUrl.path?.endsWith?.('.js')) 1072 ) { 1073 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 1074 const plugin = new Plugin(funcCode, _pluginUrl.path); 1075 const _pluginIndex = _plugins.findIndex( 1076 p => p.hash === plugin.hash, 1077 ); 1078 if (_pluginIndex !== -1) { 1079 // 重复插件,直接忽略 1080 continue; 1081 } 1082 plugin.hash !== '' && _plugins.push(plugin); 1083 } 1084 } 1085 1086 plugins = _plugins; 1087 /** 初始化meta信息 */ 1088 await PluginMeta.setupMeta(plugins.map(_ => _.name)); 1089 /** 查看一下是否有禁用的标记 */ 1090 const allMeta = PluginMeta.getPluginMetaAll() ?? {}; 1091 for (let plugin of plugins) { 1092 if (allMeta[plugin.name]?.enabled === false) { 1093 plugin.state = 'disabled'; 1094 } 1095 } 1096 pluginStateMapper.notify(); 1097 } catch (e: any) { 1098 ToastAndroid.show( 1099 `插件初始化失败:${e?.message ?? e}`, 1100 ToastAndroid.LONG, 1101 ); 1102 errorLog('插件初始化失败', e?.message); 1103 throw e; 1104 } 1105} 1106 1107interface IInstallPluginConfig { 1108 notCheckVersion?: boolean; 1109} 1110 1111async function installPluginFromRawCode( 1112 funcCode: string, 1113 config?: IInstallPluginConfig, 1114) { 1115 if (funcCode) { 1116 const plugin = new Plugin(funcCode, ''); 1117 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 1118 if (_pluginIndex !== -1) { 1119 // 静默忽略 1120 return plugin; 1121 } 1122 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 1123 if (oldVersionPlugin && !config?.notCheckVersion) { 1124 if ( 1125 compare( 1126 oldVersionPlugin.instance.version ?? '', 1127 plugin.instance.version ?? '', 1128 '>', 1129 ) 1130 ) { 1131 throw new Error('已安装更新版本的插件'); 1132 } 1133 } 1134 1135 if (plugin.hash !== '') { 1136 const fn = nanoid(); 1137 if (oldVersionPlugin) { 1138 plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash); 1139 try { 1140 await unlink(oldVersionPlugin.path); 1141 } catch {} 1142 } 1143 const pluginPath = `${pathConst.pluginPath}${fn}.js`; 1144 await writeFile(pluginPath, funcCode, 'utf8'); 1145 plugin.path = pluginPath; 1146 plugins = plugins.concat(plugin); 1147 pluginStateMapper.notify(); 1148 return plugin; 1149 } 1150 throw new Error('插件无法解析!'); 1151 } 1152} 1153 1154// 安装插件 1155async function installPlugin( 1156 pluginPath: string, 1157 config?: IInstallPluginConfig, 1158) { 1159 // if (pluginPath.endsWith('.js')) { 1160 const funcCode = await readFile(pluginPath, 'utf8'); 1161 1162 if (funcCode) { 1163 const plugin = new Plugin(funcCode, pluginPath); 1164 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 1165 if (_pluginIndex !== -1) { 1166 // 静默忽略 1167 return plugin; 1168 } 1169 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 1170 if (oldVersionPlugin && !config?.notCheckVersion) { 1171 if ( 1172 compare( 1173 oldVersionPlugin.instance.version ?? '', 1174 plugin.instance.version ?? '', 1175 '>', 1176 ) 1177 ) { 1178 throw new Error('已安装更新版本的插件'); 1179 } 1180 } 1181 1182 if (plugin.hash !== '') { 1183 const fn = nanoid(); 1184 if (oldVersionPlugin) { 1185 plugins = plugins.filter(_ => _.hash !== oldVersionPlugin.hash); 1186 try { 1187 await unlink(oldVersionPlugin.path); 1188 } catch {} 1189 } 1190 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 1191 await copyFile(pluginPath, _pluginPath); 1192 plugin.path = _pluginPath; 1193 plugins = plugins.concat(plugin); 1194 pluginStateMapper.notify(); 1195 return plugin; 1196 } 1197 throw new Error('插件无法解析!'); 1198 } 1199 throw new Error('插件无法识别!'); 1200} 1201 1202const reqHeaders = { 1203 'Cache-Control': 'no-cache', 1204 Pragma: 'no-cache', 1205 Expires: '0', 1206}; 1207 1208async function installPluginFromUrl( 1209 url: string, 1210 config?: IInstallPluginConfig, 1211) { 1212 try { 1213 const funcCode = ( 1214 await axios.get(url, { 1215 headers: reqHeaders, 1216 }) 1217 ).data; 1218 if (funcCode) { 1219 const plugin = new Plugin(funcCode, ''); 1220 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 1221 if (_pluginIndex !== -1) { 1222 // 静默忽略 1223 return; 1224 } 1225 const oldVersionPlugin = plugins.find(p => p.name === plugin.name); 1226 if (oldVersionPlugin && !config?.notCheckVersion) { 1227 if ( 1228 compare( 1229 oldVersionPlugin.instance.version ?? '', 1230 plugin.instance.version ?? '', 1231 '>', 1232 ) 1233 ) { 1234 throw new Error('已安装更新版本的插件'); 1235 } 1236 } 1237 1238 if (plugin.hash !== '') { 1239 const fn = nanoid(); 1240 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 1241 await writeFile(_pluginPath, funcCode, 'utf8'); 1242 plugin.path = _pluginPath; 1243 plugins = plugins.concat(plugin); 1244 if (oldVersionPlugin) { 1245 plugins = plugins.filter( 1246 _ => _.hash !== oldVersionPlugin.hash, 1247 ); 1248 try { 1249 await unlink(oldVersionPlugin.path); 1250 } catch {} 1251 } 1252 pluginStateMapper.notify(); 1253 return; 1254 } 1255 throw new Error('插件无法解析!'); 1256 } 1257 } catch (e: any) { 1258 devLog('error', 'URL安装插件失败', e, e?.message); 1259 errorLog('URL安装插件失败', e); 1260 throw new Error(e?.message ?? ''); 1261 } 1262} 1263 1264/** 卸载插件 */ 1265async function uninstallPlugin(hash: string) { 1266 const targetIndex = plugins.findIndex(_ => _.hash === hash); 1267 if (targetIndex !== -1) { 1268 try { 1269 const pluginName = plugins[targetIndex].name; 1270 await unlink(plugins[targetIndex].path); 1271 plugins = plugins.filter(_ => _.hash !== hash); 1272 pluginStateMapper.notify(); 1273 // 防止其他重名 1274 if (plugins.every(_ => _.name !== pluginName)) { 1275 MediaExtra.removeAll(pluginName); 1276 } 1277 } catch {} 1278 } 1279} 1280 1281async function uninstallAllPlugins() { 1282 await Promise.all( 1283 plugins.map(async plugin => { 1284 try { 1285 const pluginName = plugin.name; 1286 await unlink(plugin.path); 1287 MediaExtra.removeAll(pluginName); 1288 } catch (e) {} 1289 }), 1290 ); 1291 plugins = []; 1292 pluginStateMapper.notify(); 1293 1294 /** 清除空余文件,异步做就可以了 */ 1295 readDir(pathConst.pluginPath) 1296 .then(fns => { 1297 fns.forEach(fn => { 1298 unlink(fn.path).catch(emptyFunction); 1299 }); 1300 }) 1301 .catch(emptyFunction); 1302} 1303 1304async function updatePlugin(plugin: Plugin) { 1305 const updateUrl = plugin.instance.srcUrl; 1306 if (!updateUrl) { 1307 throw new Error('没有更新源'); 1308 } 1309 try { 1310 await installPluginFromUrl(updateUrl); 1311 } catch (e: any) { 1312 if (e.message === '插件已安装') { 1313 throw new Error('当前已是最新版本'); 1314 } else { 1315 throw e; 1316 } 1317 } 1318} 1319 1320function getByMedia(mediaItem: ICommon.IMediaBase) { 1321 return getByName(mediaItem?.platform); 1322} 1323 1324function getByHash(hash: string) { 1325 return hash === localPluginHash 1326 ? localFilePlugin 1327 : plugins.find(_ => _.hash === hash); 1328} 1329 1330function getByName(name: string) { 1331 return name === localPluginPlatform 1332 ? localFilePlugin 1333 : plugins.find(_ => _.name === name); 1334} 1335 1336function getValidPlugins() { 1337 return plugins.filter(_ => _.state === 'enabled'); 1338} 1339 1340function getSearchablePlugins(supportedSearchType?: ICommon.SupportMediaType) { 1341 return plugins.filter( 1342 _ => 1343 _.state === 'enabled' && 1344 _.instance.search && 1345 (supportedSearchType && _.instance.supportedSearchType 1346 ? _.instance.supportedSearchType.includes(supportedSearchType) 1347 : true), 1348 ); 1349} 1350 1351function getSortedSearchablePlugins( 1352 supportedSearchType?: ICommon.SupportMediaType, 1353) { 1354 return getSearchablePlugins(supportedSearchType).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 getTopListsablePlugins() { 1364 return plugins.filter(_ => _.state === 'enabled' && _.instance.getTopLists); 1365} 1366 1367function getSortedTopListsablePlugins() { 1368 return getTopListsablePlugins().sort((a, b) => 1369 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1370 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1371 0 1372 ? -1 1373 : 1, 1374 ); 1375} 1376 1377function getRecommendSheetablePlugins() { 1378 return plugins.filter( 1379 _ => _.state === 'enabled' && _.instance.getRecommendSheetsByTag, 1380 ); 1381} 1382 1383function getSortedRecommendSheetablePlugins() { 1384 return getRecommendSheetablePlugins().sort((a, b) => 1385 (PluginMeta.getPluginMeta(a).order ?? Infinity) - 1386 (PluginMeta.getPluginMeta(b).order ?? Infinity) < 1387 0 1388 ? -1 1389 : 1, 1390 ); 1391} 1392 1393function useSortedPlugins() { 1394 const _plugins = pluginStateMapper.useMappedState(); 1395 const _pluginMetaAll = PluginMeta.usePluginMetaAll(); 1396 1397 const [sortedPlugins, setSortedPlugins] = useState( 1398 [..._plugins].sort((a, b) => 1399 (_pluginMetaAll[a.name]?.order ?? Infinity) - 1400 (_pluginMetaAll[b.name]?.order ?? Infinity) < 1401 0 1402 ? -1 1403 : 1, 1404 ), 1405 ); 1406 1407 useEffect(() => { 1408 InteractionManager.runAfterInteractions(() => { 1409 setSortedPlugins( 1410 [..._plugins].sort((a, b) => 1411 (_pluginMetaAll[a.name]?.order ?? Infinity) - 1412 (_pluginMetaAll[b.name]?.order ?? Infinity) < 1413 0 1414 ? -1 1415 : 1, 1416 ), 1417 ); 1418 }); 1419 }, [_plugins, _pluginMetaAll]); 1420 1421 return sortedPlugins; 1422} 1423 1424async function setPluginEnabled(plugin: Plugin, enabled?: boolean) { 1425 const target = plugins.find(it => it.hash === plugin.hash); 1426 if (target) { 1427 target.state = enabled ? 'enabled' : 'disabled'; 1428 plugins = [...plugins]; 1429 pluginStateMapper.notify(); 1430 PluginMeta.setPluginMetaProp(plugin, 'enabled', enabled); 1431 } 1432} 1433 1434const PluginManager = { 1435 setup, 1436 installPlugin, 1437 installPluginFromRawCode, 1438 installPluginFromUrl, 1439 updatePlugin, 1440 uninstallPlugin, 1441 getByMedia, 1442 getByHash, 1443 getByName, 1444 getValidPlugins, 1445 getSearchablePlugins, 1446 getSortedSearchablePlugins, 1447 getTopListsablePlugins, 1448 getSortedRecommendSheetablePlugins, 1449 getSortedTopListsablePlugins, 1450 usePlugins: pluginStateMapper.useMappedState, 1451 useSortedPlugins, 1452 uninstallAllPlugins, 1453 setPluginEnabled, 1454}; 1455 1456export default PluginManager; 1457