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