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