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 {ToastAndroid} from 'react-native'; 13import pathConst from '@/constants/pathConst'; 14import {satisfies} from 'compare-versions'; 15import DeviceInfo from 'react-native-device-info'; 16import StateMapper from '@/utils/stateMapper'; 17import MediaMeta from './mediaMeta'; 18import {nanoid} from 'nanoid'; 19import {errorLog, trace} from '../utils/log'; 20import Cache from './cache'; 21import {isSameMediaItem, resetMediaItem} from '@/utils/mediaItem'; 22import { 23 CacheControl, 24 internalSerialzeKey, 25 internalSymbolKey, 26} from '@/constants/commonConst'; 27import Download from './download'; 28import delay from '@/utils/delay'; 29import * as cheerio from 'cheerio'; 30import Network from './network'; 31 32axios.defaults.timeout = 1500; 33 34const sha256 = CryptoJs.SHA256; 35 36export enum PluginStateCode { 37 /** 版本不匹配 */ 38 VersionNotMatch = 'VERSION NOT MATCH', 39 /** 无法解析 */ 40 CannotParse = 'CANNOT PARSE', 41} 42 43export class Plugin { 44 /** 插件名 */ 45 public name: string; 46 /** 插件的hash,作为唯一id */ 47 public hash: string; 48 /** 插件状态:激活、关闭、错误 */ 49 public state: 'enabled' | 'disabled' | 'error'; 50 /** 插件支持的搜索类型 */ 51 public supportedSearchType?: string; 52 /** 插件状态信息 */ 53 public stateCode?: PluginStateCode; 54 /** 插件的实例 */ 55 public instance: IPlugin.IPluginInstance; 56 /** 插件路径 */ 57 public path: string; 58 /** 插件方法 */ 59 public methods: PluginMethods; 60 61 constructor(funcCode: string, pluginPath: string) { 62 this.state = 'enabled'; 63 let _instance: IPlugin.IPluginInstance; 64 try { 65 // eslint-disable-next-line no-new-func 66 _instance = Function(` 67 'use strict'; 68 try { 69 return ${funcCode}; 70 } catch(e) { 71 return null; 72 } 73 `)()({CryptoJs, axios, dayjs, cheerio}); 74 this.checkValid(_instance); 75 } catch (e: any) { 76 this.state = 'error'; 77 this.stateCode = PluginStateCode.CannotParse; 78 if (e?.stateCode) { 79 this.stateCode = e.stateCode; 80 } 81 errorLog(`${pluginPath}插件无法解析 `, { 82 stateCode: this.stateCode, 83 message: e?.message, 84 stack: e?.stack, 85 }); 86 _instance = e?.instance ?? { 87 _path: '', 88 platform: '', 89 appVersion: '', 90 async getMediaSource() { 91 return null; 92 }, 93 async search() { 94 return {}; 95 }, 96 async getAlbumInfo() { 97 return null; 98 }, 99 }; 100 } 101 this.instance = _instance; 102 this.path = pluginPath; 103 this.name = _instance.platform; 104 if (this.instance.platform === '') { 105 this.hash = ''; 106 } else { 107 this.hash = sha256(funcCode).toString(); 108 } 109 110 // 放在最后 111 this.methods = new PluginMethods(this); 112 } 113 114 private checkValid(_instance: IPlugin.IPluginInstance) { 115 /** 版本号校验 */ 116 if ( 117 _instance.appVersion && 118 !satisfies(DeviceInfo.getVersion(), _instance.appVersion) 119 ) { 120 throw { 121 instance: _instance, 122 stateCode: PluginStateCode.VersionNotMatch, 123 }; 124 } 125 return true; 126 } 127} 128 129/** 有缓存等信息 */ 130class PluginMethods implements IPlugin.IPluginInstanceMethods { 131 private plugin; 132 constructor(plugin: Plugin) { 133 this.plugin = plugin; 134 } 135 /** 搜索 */ 136 async search<T extends ICommon.SupportMediaType>( 137 query: string, 138 page: number, 139 type: T, 140 ): Promise<IPlugin.ISearchResult<T>> { 141 if (!this.plugin.instance.search) { 142 return { 143 isEnd: true, 144 data: [], 145 }; 146 } 147 148 const result = 149 (await this.plugin.instance.search(query, page, type)) ?? {}; 150 if (Array.isArray(result.data)) { 151 result.data.forEach(_ => { 152 resetMediaItem(_, this.plugin.name); 153 }); 154 return { 155 isEnd: result.isEnd ?? true, 156 data: result.data, 157 }; 158 } 159 return { 160 isEnd: true, 161 data: [], 162 }; 163 } 164 165 /** 获取真实源 */ 166 async getMediaSource( 167 musicItem: IMusic.IMusicItemBase, 168 retryCount = 1, 169 ): Promise<IPlugin.IMediaSourceResult> { 170 // 1. 本地搜索 其实直接读mediameta就好了 171 const localPath = 172 musicItem?.[internalSymbolKey]?.localPath ?? 173 Download.getDownloaded(musicItem)?.[internalSymbolKey]?.localPath; 174 if (localPath && (await exists(localPath))) { 175 trace('播放', '本地播放'); 176 return { 177 url: localPath, 178 }; 179 } 180 // 2. 缓存播放 181 const mediaCache = Cache.get(musicItem); 182 if ( 183 mediaCache && 184 mediaCache?.url && 185 (mediaCache.cacheControl === CacheControl.Cache || 186 (mediaCache.cacheControl === CacheControl.NoCache && 187 Network.isOffline())) 188 ) { 189 trace('播放', '缓存播放'); 190 return { 191 url: mediaCache.url, 192 headers: mediaCache.headers, 193 userAgent: 194 mediaCache.userAgent ?? mediaCache.headers?.['user-agent'], 195 }; 196 } 197 // 3. 插件解析 198 if (!this.plugin.instance.getMediaSource) { 199 return {url: musicItem.url}; 200 } 201 try { 202 const {url, headers, cacheControl} = 203 (await this.plugin.instance.getMediaSource(musicItem)) ?? {}; 204 if (!url) { 205 throw new Error(); 206 } 207 trace('播放', '插件播放'); 208 const result = { 209 url, 210 headers, 211 userAgent: headers?.['user-agent'], 212 cacheControl: cacheControl ?? CacheControl.Cache, 213 } as IPlugin.IMediaSourceResult; 214 215 if (cacheControl !== CacheControl.NoStore) { 216 Cache.update(musicItem, result); 217 } 218 219 return result; 220 } catch (e: any) { 221 if (retryCount > 0) { 222 await delay(150); 223 return this.getMediaSource(musicItem, --retryCount); 224 } 225 errorLog('获取真实源失败', e?.message); 226 throw e; 227 } 228 } 229 230 /** 获取音乐详情 */ 231 async getMusicInfo( 232 musicItem: ICommon.IMediaBase, 233 ): Promise<IMusic.IMusicItem | null> { 234 if (!this.plugin.instance.getMusicInfo) { 235 return musicItem as IMusic.IMusicItem; 236 } 237 return ( 238 this.plugin.instance.getMusicInfo( 239 resetMediaItem(musicItem, undefined, true), 240 ) ?? musicItem 241 ); 242 } 243 244 /** 获取歌词 */ 245 async getLyric( 246 musicItem: IMusic.IMusicItemBase, 247 from?: IMusic.IMusicItemBase, 248 ): Promise<ILyric.ILyricSource | null> { 249 // 1.额外存储的meta信息 250 const meta = MediaMeta.get(musicItem); 251 if (meta && meta.associatedLrc) { 252 // 有关联歌词 253 if ( 254 isSameMediaItem(musicItem, from) || 255 isSameMediaItem(meta.associatedLrc, musicItem) 256 ) { 257 // 形成环路,断开当前的环 258 await MediaMeta.update(musicItem, { 259 associatedLrc: undefined, 260 }); 261 // 无歌词 262 return null; 263 } 264 // 获取关联歌词 265 const result = await this.getLyric( 266 meta.associatedLrc, 267 from ?? musicItem, 268 ); 269 if (result) { 270 // 如果有关联歌词,就返回关联歌词,深度优先 271 return result; 272 } 273 } 274 const cache = Cache.get(musicItem); 275 let rawLrc = meta?.rawLrc || musicItem.rawLrc || cache?.rawLrc; 276 let lrcUrl = meta?.lrc || musicItem.lrc || cache?.lrc; 277 // 如果存在文本 278 if (rawLrc) { 279 return { 280 rawLrc, 281 lrc: lrcUrl, 282 }; 283 } 284 // 2.本地缓存 285 const localLrc = 286 meta?.[internalSerialzeKey]?.local?.localLrc || 287 cache?.[internalSerialzeKey]?.local?.localLrc; 288 if (localLrc && (await exists(localLrc))) { 289 rawLrc = await readFile(localLrc, 'utf8'); 290 return { 291 rawLrc, 292 lrc: lrcUrl, 293 }; 294 } 295 // 3.优先使用url 296 if (lrcUrl) { 297 try { 298 // 需要超时时间 axios timeout 但是没生效 299 rawLrc = (await axios.get(lrcUrl, {timeout: 1500})).data; 300 return { 301 rawLrc, 302 lrc: lrcUrl, 303 }; 304 } catch { 305 lrcUrl = undefined; 306 } 307 } 308 // 4. 如果地址失效 309 if (!lrcUrl) { 310 // 插件获得url 311 try { 312 const lrcSource = await this.plugin.instance?.getLyric?.( 313 resetMediaItem(musicItem, undefined, true), 314 ); 315 rawLrc = lrcSource?.rawLrc; 316 lrcUrl = lrcSource?.lrc; 317 } catch (e: any) { 318 trace('插件获取歌词失败', e?.message, 'error'); 319 } 320 } 321 // 5. 最后一次请求 322 if (rawLrc || lrcUrl) { 323 const filename = `${pathConst.lrcCachePath}${nanoid()}.lrc`; 324 if (lrcUrl) { 325 try { 326 rawLrc = (await axios.get(lrcUrl, {timeout: 1500})).data; 327 } catch {} 328 } 329 if (rawLrc) { 330 await writeFile(filename, rawLrc, 'utf8'); 331 // 写入缓存 332 Cache.update(musicItem, [ 333 [`${internalSerialzeKey}.local.localLrc`, filename], 334 ]); 335 // 如果有meta 336 if (meta) { 337 MediaMeta.update(musicItem, [ 338 [`${internalSerialzeKey}.local.localLrc`, filename], 339 ]); 340 } 341 return { 342 rawLrc, 343 lrc: lrcUrl, 344 }; 345 } 346 } 347 348 return null; 349 } 350 351 /** 获取歌词文本 */ 352 async getLyricText( 353 musicItem: IMusic.IMusicItem, 354 ): Promise<string | undefined> { 355 return (await this.getLyric(musicItem))?.rawLrc; 356 } 357 358 /** 获取专辑信息 */ 359 async getAlbumInfo( 360 albumItem: IAlbum.IAlbumItemBase, 361 ): Promise<IAlbum.IAlbumItem | null> { 362 if (!this.plugin.instance.getAlbumInfo) { 363 return {...albumItem, musicList: []}; 364 } 365 try { 366 const result = await this.plugin.instance.getAlbumInfo( 367 resetMediaItem(albumItem, undefined, true), 368 ); 369 if (!result) { 370 throw new Error(); 371 } 372 result?.musicList?.forEach(_ => { 373 resetMediaItem(_, this.plugin.name); 374 }); 375 376 return {...albumItem, ...result}; 377 } catch { 378 return {...albumItem, musicList: []}; 379 } 380 } 381 382 /** 查询作者信息 */ 383 async queryArtistWorks<T extends IArtist.ArtistMediaType>( 384 artistItem: IArtist.IArtistItem, 385 page: number, 386 type: T, 387 ): Promise<IPlugin.ISearchResult<T>> { 388 if (!this.plugin.instance.queryArtistWorks) { 389 return { 390 isEnd: true, 391 data: [], 392 }; 393 } 394 try { 395 const result = await this.plugin.instance.queryArtistWorks( 396 artistItem, 397 page, 398 type, 399 ); 400 if (!result.data) { 401 return { 402 isEnd: true, 403 data: [], 404 }; 405 } 406 result.data?.forEach(_ => resetMediaItem(_, this.plugin.name)); 407 return { 408 isEnd: result.isEnd ?? true, 409 data: result.data, 410 }; 411 } catch (e) { 412 throw e; 413 } 414 } 415 416 /** 导入歌单 */ 417 async importMusicSheet(urlLike: string): Promise<IMusic.IMusicItem[]> { 418 try { 419 const result = 420 (await this.plugin.instance?.importMusicSheet?.(urlLike)) ?? []; 421 result.forEach(_ => resetMediaItem(_, this.plugin.name)); 422 return result; 423 } catch { 424 return []; 425 } 426 } 427 /** 导入单曲 */ 428 async importMusicItem(urlLike: string): Promise<IMusic.IMusicItem | null> { 429 try { 430 const result = await this.plugin.instance?.importMusicItem?.( 431 urlLike, 432 ); 433 if (!result) { 434 throw new Error(); 435 } 436 resetMediaItem(result, this.plugin.name); 437 return result; 438 } catch { 439 return null; 440 } 441 } 442} 443 444let plugins: Array<Plugin> = []; 445const pluginStateMapper = new StateMapper(() => plugins); 446 447async function setup() { 448 const _plugins: Array<Plugin> = []; 449 try { 450 // 加载插件 451 const pluginsPaths = await readDir(pathConst.pluginPath); 452 for (let i = 0; i < pluginsPaths.length; ++i) { 453 const _pluginUrl = pluginsPaths[i]; 454 455 if (_pluginUrl.isFile() && _pluginUrl.name.endsWith('.js')) { 456 const funcCode = await readFile(_pluginUrl.path, 'utf8'); 457 const plugin = new Plugin(funcCode, _pluginUrl.path); 458 const _pluginIndex = _plugins.findIndex( 459 p => p.hash === plugin.hash, 460 ); 461 if (_pluginIndex !== -1) { 462 // 重复插件,直接忽略 463 return; 464 } 465 plugin.hash !== '' && _plugins.push(plugin); 466 } 467 } 468 469 plugins = _plugins; 470 pluginStateMapper.notify(); 471 } catch (e: any) { 472 ToastAndroid.show( 473 `插件初始化失败:${e?.message ?? e}`, 474 ToastAndroid.LONG, 475 ); 476 errorLog('插件初始化失败', e?.message); 477 throw e; 478 } 479} 480 481// 安装插件 482async function installPlugin(pluginPath: string) { 483 // let checkPath = decodeURIComponent(pluginPath); 484 // trace(checkPath, await exists(checkPath)); 485 // trace(pluginPath, await exists(pluginPath)); 486 // trace(pluginPath.substring(7), await exists(pluginPath.substring(7))); 487 // trace(checkPath.substring(7), await exists(checkPath.substring(7))); 488 if (pluginPath.endsWith('.js')) { 489 const funcCode = await readFile(pluginPath, 'utf8'); 490 const plugin = new Plugin(funcCode, pluginPath); 491 const _pluginIndex = plugins.findIndex(p => p.hash === plugin.hash); 492 if (_pluginIndex !== -1) { 493 throw new Error('插件已安装'); 494 } 495 if (plugin.hash !== '') { 496 const fn = nanoid(); 497 const _pluginPath = `${pathConst.pluginPath}${fn}.js`; 498 await copyFile(pluginPath, _pluginPath); 499 plugin.path = _pluginPath; 500 plugins = plugins.concat(plugin); 501 pluginStateMapper.notify(); 502 return; 503 } 504 throw new Error('插件无法解析'); 505 } 506 throw new Error('插件不存在'); 507} 508 509/** 卸载插件 */ 510async function uninstallPlugin(hash: string) { 511 const targetIndex = plugins.findIndex(_ => _.hash === hash); 512 if (targetIndex !== -1) { 513 try { 514 await unlink(plugins[targetIndex].path); 515 plugins = plugins.filter(_ => _.hash !== hash); 516 pluginStateMapper.notify(); 517 } catch {} 518 } 519} 520 521function getByMedia(mediaItem: ICommon.IMediaBase) { 522 return getByName(mediaItem.platform); 523} 524 525function getByHash(hash: string) { 526 return plugins.find(_ => _.hash === hash); 527} 528 529function getByName(name: string) { 530 return plugins.find(_ => _.name === name); 531} 532 533function getValidPlugins() { 534 return plugins.filter(_ => _.state === 'enabled'); 535} 536 537const PluginManager = { 538 setup, 539 installPlugin, 540 uninstallPlugin, 541 getByMedia, 542 getByHash, 543 getByName, 544 getValidPlugins, 545 usePlugins: pluginStateMapper.useMappedState, 546}; 547 548export default PluginManager; 549