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