1import RNFS, {exists, readFile, writeFile} from 'react-native-fs'; 2import CryptoJs from 'crypto-js'; 3import dayjs from 'dayjs'; 4import axios from 'axios'; 5import {useEffect, useState} from 'react'; 6import {ToastAndroid} from 'react-native'; 7import pathConst from '@/constants/pathConst'; 8import {satisfies} from 'compare-versions'; 9import DeviceInfo from 'react-native-device-info'; 10import StateMapper from '@/utils/stateMapper'; 11import MediaMetaManager from './mediaMeta'; 12import {nanoid} from 'nanoid'; 13import {errorLog, trace} from '../utils/log'; 14import Cache from './cache'; 15import { isSameMediaItem } from '@/utils/mediaItem'; 16 17axios.defaults.timeout = 1500; 18 19const sha256 = CryptoJs.SHA256; 20 21enum PluginStateCode { 22 /** 版本不匹配 */ 23 VersionNotMatch = 'VERSION NOT MATCH', 24 /** 插件不完整 */ 25 NotComplete = 'NOT COMPLETE', 26 /** 无法解析 */ 27 CannotParse = 'CANNOT PARSE', 28} 29export class Plugin { 30 /** 插件名 */ 31 public name: string; 32 /** 插件的hash,作为唯一id */ 33 public hash: string; 34 /** 插件状态:激活、关闭、错误 */ 35 public state: 'enabled' | 'disabled' | 'error'; 36 /** 插件支持的搜索类型 */ 37 public supportedSearchType?: string; 38 /** 插件状态信息 */ 39 public stateCode?: PluginStateCode; 40 /** 插件的实例 */ 41 public instance: IPlugin.IPluginInstance; 42 43 constructor(funcCode: string, pluginPath: string) { 44 this.state = 'enabled'; 45 let _instance: IPlugin.IPluginInstance; 46 try { 47 _instance = Function(` 48 'use strict'; 49 try { 50 return ${funcCode}; 51 } catch(e) { 52 return null; 53 } 54 `)()({CryptoJs, axios, dayjs}); 55 56 this.checkValid(_instance); 57 } catch (e: any) { 58 this.state = 'error'; 59 this.stateCode = PluginStateCode.CannotParse; 60 if (e?.stateCode) { 61 this.stateCode = e.stateCode; 62 } 63 errorLog(`${pluginPath}插件无法解析 `, { 64 stateCode: this.stateCode, 65 message: e?.message, 66 stack: e?.stack, 67 }); 68 _instance = e?.instance ?? { 69 _path: '', 70 platform: '', 71 appVersion: '', 72 async getMusicTrack() { 73 return null; 74 }, 75 async search() { 76 return {}; 77 }, 78 async getAlbumInfo() { 79 return null; 80 }, 81 }; 82 } 83 this.instance = _instance; 84 this.instance._path = pluginPath; 85 this.name = _instance.platform; 86 if (this.instance.platform === '') { 87 this.hash = ''; 88 } else { 89 this.hash = sha256(funcCode).toString(); 90 } 91 } 92 93 private checkValid(_instance: IPlugin.IPluginInstance) { 94 // 总不会一个都没有吧 95 const keys: Array<keyof IPlugin.IPluginInstance> = [ 96 'getAlbumInfo', 97 'search', 98 'getMusicTrack', 99 ]; 100 if (keys.every(k => !_instance[k])) { 101 throw { 102 instance: _instance, 103 stateCode: PluginStateCode.NotComplete, 104 }; 105 } 106 /** 版本号校验 */ 107 if ( 108 _instance.appVersion && 109 !satisfies(DeviceInfo.getVersion(), _instance.appVersion) 110 ) { 111 throw { 112 instance: _instance, 113 stateCode: PluginStateCode.VersionNotMatch, 114 }; 115 } 116 return true; 117 } 118} 119 120class PluginManager { 121 private plugins: Array<Plugin> = []; 122 loading: boolean = true; 123 /** 插件安装位置 */ 124 pluginPath: string = pathConst.pluginPath; 125 constructor(private onSetup: () => void) {} 126 127 private loadPlugin(funcCode: string, pluginPath: string) { 128 const plugin = new Plugin(funcCode, pluginPath); 129 const _pluginIndex = this.plugins.findIndex(p => p.hash === plugin.hash); 130 if (_pluginIndex !== -1) { 131 // 有重复的了,直接忽略 132 return; 133 } 134 plugin.hash !== '' && this.plugins.push(plugin); 135 } 136 137 getPlugins() { 138 return this.plugins; 139 } 140 141 getValidPlugins() { 142 return this.plugins.filter(_ => _.state === 'enabled'); 143 } 144 145 getPlugin(platform: string) { 146 return this.plugins.find( 147 _ => _.instance.platform === platform && _.state === 'enabled', 148 ); 149 } 150 151 getPluginByPlatform(platform: string) { 152 return this.plugins.filter(_ => _.name === platform); 153 } 154 155 getPluginByHash(hash: string) { 156 return this.plugins.find(_ => _.hash === hash); 157 } 158 159 async setupPlugins() { 160 this.loading = true; 161 this.plugins = []; 162 try { 163 this.loading = false; 164 // 加载插件 165 const pluginsPaths = await RNFS.readDir(pathConst.pluginPath); 166 for (let i = 0; i < pluginsPaths.length; ++i) { 167 const _plugin = pluginsPaths[i]; 168 169 if (_plugin.isFile() && _plugin.name.endsWith('.js')) { 170 const funcCode = await RNFS.readFile(_plugin.path, 'utf8'); 171 this.loadPlugin(funcCode, _plugin.path); 172 } 173 } 174 this.onSetup(); 175 this.loading = false; 176 } catch (e: any) { 177 ToastAndroid.show(`插件初始化失败:${e?.message ?? e}`, ToastAndroid.LONG); 178 this.loading = false; 179 throw e; 180 } 181 } 182} 183 184const pluginManager = new PluginManager(() => { 185 pluginStateMapper?.notify?.(); 186}); 187const pluginStateMapper = new StateMapper(() => pluginManager?.getPlugins?.()); 188 189// function usePlugins() { 190// const [plugins, setPlugins] = useState(pluginManager.getValidPlugins()); 191 192// useEffect(() => { 193// if (pluginManager.loading === false) { 194// setPlugins(pluginManager.getValidPlugins()); 195// } 196// }, [pluginManager.loading]); 197 198// return plugins; 199// } 200 201/** 封装的插件方法 */ 202const pluginMethod = { 203 async getLyric( 204 musicItem: IMusic.IMusicItem, 205 from?: IMusic.IMusicItem, 206 ): Promise<string | undefined> { 207 // 1. 额外存储的meta信息 208 const meta = MediaMetaManager.getMediaMeta(musicItem) ?? {}; 209 // 有关联歌词 210 if (meta.associatedLrc) { 211 if ( 212 isSameMediaItem(musicItem, from) || 213 isSameMediaItem(meta.associatedLrc, musicItem) 214 ) { 215 // 形成了环 只把自己断开 216 await MediaMetaManager.updateMediaMeta(musicItem, { 217 associatedLrc: undefined, 218 }); 219 return; 220 } 221 const result = await pluginMethod.getLyric( 222 meta.associatedLrc as IMusic.IMusicItem, 223 from ?? musicItem, 224 ); 225 if (result) { 226 return result; 227 } 228 } 229 const cache = Cache.get(musicItem); 230 // 优先级:meta中、当前歌曲最新的rawlrc、缓存中的rawlrc 231 if (meta?.rawLrc || musicItem.rawLrc || cache?.rawLrc) { 232 return meta.rawLrc ?? musicItem.rawLrc ?? cache?.rawLrc; 233 } 234 // 本地文件中的lrc 235 const localLrc = meta.localLrc ?? cache?.localLrc; 236 if (localLrc && (await exists(localLrc))) { 237 return await readFile(localLrc, 'utf8'); 238 } 239 240 let lrcUrl: string | undefined = meta?.lrc ?? musicItem.lrc ?? cache?.lrc; 241 let rawLrc: string | undefined; 242 // mediaItem中自带的lrcurl 243 if (lrcUrl) { 244 try { 245 // 需要超时时间 axios timeout 但是没生效 246 rawLrc = (await axios.get(lrcUrl)).data; 247 } catch { 248 lrcUrl = undefined; 249 } 250 } 251 if (!lrcUrl) { 252 // 从插件中获得 253 const plugin = pluginManager.getPlugin(musicItem.platform); 254 try { 255 const lrcSource = await plugin?.instance?.getLyric?.(musicItem); 256 rawLrc = lrcSource?.rawLrc; 257 lrcUrl = lrcSource?.lrc; 258 } catch (e: any) { 259 trace('插件获取失败', e?.message, 'error'); 260 } 261 } 262 if (rawLrc || lrcUrl) { 263 const filename = `${pathConst.lrcCachePath}${nanoid()}.lrc`; 264 if (rawLrc) { 265 await writeFile(filename, rawLrc, 'utf8'); 266 // todo 写入缓存 应该是internaldata 267 Cache.update(musicItem, { 268 localLrc: filename, 269 }); 270 // 如果有用户定制化的信息,就写入持久存储中 271 if (meta) { 272 MediaMetaManager.updateMediaMeta(musicItem, { 273 localLrc: filename, 274 }); 275 } 276 277 return rawLrc; 278 } 279 if (lrcUrl) { 280 try { 281 const content = (await axios.get(lrcUrl)).data; 282 await writeFile(filename, content, 'utf8'); 283 Cache.update(musicItem, { 284 localLrc: filename, 285 }); 286 if (meta) { 287 MediaMetaManager.updateMediaMeta(musicItem, { 288 localLrc: filename, 289 lrc: lrcUrl, 290 }); 291 } 292 293 return content; 294 } catch {} 295 } 296 } 297 }, 298}; 299 300const usePlugins = pluginStateMapper.useMappedState; 301 302export {pluginManager, usePlugins, pluginMethod}; 303