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