mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/lyswhut/lx-music-desktop.git
synced 2026-09-20 08:04:00 +08:00
refactor: 将QRC解密替换为纯JS实现并简化kw歌词获取 (#2845)
* 简化kw歌词获取 && kw歌词解密逻辑移动 将酷我歌词解密从主进程 native/IPC 方案迁移到渲染进程纯 JS 实现, 移除 kw_decodeLyric 主进程入口与 handle_kw_decode_lyric IPC 通道。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * tx歌词解密改用js实现 将 QRC 歌词的 3DES 解密由主进程 native 插件(qrc_decode.node)按原 C++ 算法 1:1 移植为渲染进程纯 JS 实现,移除 tx_decodeLyric 主进程入口、 handle_tx_decode_lyric IPC 通道,以及 build 配置中对 qrc_decode.node 的引用与各平台预构建二进制。 注意:QRC 使用的是非标准 DES 变体(S2[23]=15、S4[53]=10,标准为 14、1),新实现已逐字节对照原 native 二进制保持一致。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * update --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: lyswhut <lyswhut@qq.com>
This commit is contained in:
@@ -10,23 +10,6 @@ const better_sqlite3_fileNameMap = {
|
||||
[Arch.armv7l]: 'linux-arm',
|
||||
}
|
||||
|
||||
const qrc_decode_fileNameMap = {
|
||||
win32: {
|
||||
[Arch.x64]: 'win32-x64',
|
||||
[Arch.ia32]: 'win32-ia32',
|
||||
[Arch.arm64]: 'win32-arm64',
|
||||
},
|
||||
linux: {
|
||||
[Arch.x64]: 'linux-x64',
|
||||
[Arch.arm64]: 'linux-arm64',
|
||||
[Arch.armv7l]: 'linux-arm',
|
||||
},
|
||||
darwin: {
|
||||
[Arch.x64]: 'darwin-x64',
|
||||
[Arch.arm64]: 'darwin-arm64',
|
||||
},
|
||||
}
|
||||
|
||||
const replaceSqliteLib = async(electronNodeAbi, arch) => {
|
||||
// console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))
|
||||
// if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return
|
||||
@@ -40,22 +23,11 @@ const replaceSqliteLib = async(electronNodeAbi, arch) => {
|
||||
await fsPromises.copyFile(filePath, targetPath)
|
||||
}
|
||||
|
||||
const replaceQrcDecodeLib = async(electronNodeAbi, platform, arch) => {
|
||||
console.log('replace qrc_decode lib...', platform, electronNodeAbi, qrc_decode_fileNameMap[platform][arch])
|
||||
const filePath = path.join(__dirname, `./lib/qrc_decode_electron-v${electronNodeAbi}-${qrc_decode_fileNameMap[platform][arch]}.node`)
|
||||
const targetPath = path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
const targetDir = path.dirname(targetPath)
|
||||
if (fs.existsSync(targetDir)) await fsPromises.unlink(targetPath).catch(_ => _)
|
||||
else await fsPromises.mkdir(targetDir, { recursive: true })
|
||||
await fsPromises.copyFile(filePath, targetPath)
|
||||
}
|
||||
|
||||
|
||||
module.exports = async(context) => {
|
||||
const { electronPlatformName, arch } = context
|
||||
const electronVersion = context.packager?.info?._framework?.version ?? require('../package.json').devDependencies.electron.replace(/^[^\d]*?(\d+)/, '$1')
|
||||
const electronNodeAbi = nodeAbi.getAbi(electronVersion, 'electron')
|
||||
await replaceQrcDecodeLib(electronNodeAbi, electronPlatformName, arch)
|
||||
if (electronPlatformName !== 'linux' || process.env.FORCE) return
|
||||
const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')
|
||||
const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')
|
||||
|
||||
@@ -35,7 +35,6 @@ const options = {
|
||||
'node_modules/node-gyp-build',
|
||||
'node_modules/bufferutil',
|
||||
'node_modules/utf-8-validate',
|
||||
'build/Release/qrc_decode.node',
|
||||
'dist/**/*',
|
||||
],
|
||||
asar: {
|
||||
|
||||
@@ -25,7 +25,6 @@ const unzip = async(filePath) => {
|
||||
}
|
||||
|
||||
const files = [
|
||||
'qrc_decode',
|
||||
'better_sqlite3',
|
||||
]
|
||||
const moveFile = async(filePath) => {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,6 @@
|
||||
const path = require('path')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
target: 'electron-main',
|
||||
output: {
|
||||
@@ -18,7 +16,6 @@ module.exports = {
|
||||
'electron-font-manager': 'electron-font-manager',
|
||||
bufferutil: 'bufferutil',
|
||||
'utf-8-validate': 'utf-8-validate',
|
||||
'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -78,10 +78,6 @@ const modules = {
|
||||
|
||||
restart_window: 'restart_window',
|
||||
|
||||
// lang_s2t: 'lang_s2t',
|
||||
|
||||
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
|
||||
handle_tx_decode_lyric: 'handle_tx_decode_lyric',
|
||||
get_lyric_info: 'get_lyric_info',
|
||||
set_lyric_info: 'set_lyric_info',
|
||||
set_config: 'set_config',
|
||||
|
||||
@@ -3,8 +3,6 @@ import { registerRendererEvents as list } from '@main/modules/commonRenderers/li
|
||||
import { registerRendererEvents as dislike } from '@main/modules/commonRenderers/dislike'
|
||||
import app, { sendConfigChange } from './app'
|
||||
import hotKey from './hotKey'
|
||||
import kw_decodeLyric from './kw_decodeLyric'
|
||||
import tx_decodeLyric from './tx_decodeLyric'
|
||||
import userApi from './userApi'
|
||||
import sync from './sync'
|
||||
import data from './data'
|
||||
@@ -30,8 +28,6 @@ export default () => {
|
||||
dislike(sendEvent)
|
||||
app()
|
||||
hotKey()
|
||||
kw_decodeLyric()
|
||||
tx_decodeLyric()
|
||||
userApi()
|
||||
sync()
|
||||
data()
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { inflate } from 'zlib'
|
||||
import iconv from 'iconv-lite'
|
||||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
|
||||
const handleInflate = async(data: Buffer) => {
|
||||
return new Promise((resolve: (result: Buffer) => void, reject) => {
|
||||
inflate(data, (err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const buf_key = Buffer.from('yeelion')
|
||||
const buf_key_len = buf_key.length
|
||||
|
||||
const decodeLyric = async(buf: Buffer, isGetLyricx: boolean) => {
|
||||
// const info = buf.slice(0, index).toString()
|
||||
// if (!info.startsWith('tp=content')) return null
|
||||
// const isLyric = info.includes('\r\nlrcx=0\r\n')
|
||||
if (buf.toString('utf8', 0, 10) != 'tp=content') return ''
|
||||
// const index = buf.indexOf('\r\n\r\n') + 4
|
||||
const lrcData = await handleInflate(buf.subarray(buf.indexOf('\r\n\r\n') + 4))
|
||||
|
||||
if (!isGetLyricx) return iconv.decode(lrcData, 'gb18030')
|
||||
|
||||
const buf_str = Buffer.from(lrcData.toString(), 'base64')
|
||||
const buf_str_len = buf_str.length
|
||||
const output = new Uint8Array(buf_str_len)
|
||||
let i = 0
|
||||
while (i < buf_str_len) {
|
||||
let j = 0
|
||||
while (j < buf_key_len && i < buf_str_len) {
|
||||
output[i] = buf_str[i] ^ buf_key[j]
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
return iconv.decode(Buffer.from(output), 'gb18030')
|
||||
}
|
||||
|
||||
|
||||
export default () => {
|
||||
mainHandle<{ lrcBase64: string, isGetLyricx: boolean }, string>(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, async({ params: { lrcBase64, isGetLyricx } }) => {
|
||||
const lrc = await decodeLyric(Buffer.from(lrcBase64, 'base64'), isGetLyricx)
|
||||
return Buffer.from(lrc).toString('base64')
|
||||
})
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { createInflate, constants as zlibConstants } from 'node:zlib'
|
||||
// import path from 'path'
|
||||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/quotes
|
||||
// const require = module[`require`].bind(module)
|
||||
|
||||
let qrc_decode: (buf: Buffer, len: number) => Buffer
|
||||
|
||||
const inflate = async(lrcBuf: Buffer) => new Promise<string>((resolve, reject) => {
|
||||
const buffer_builder: Buffer[] = []
|
||||
const decompress_stream = createInflate()
|
||||
.on('data', (chunk) => {
|
||||
buffer_builder.push(chunk)
|
||||
})
|
||||
.on('close', () => {
|
||||
resolve(Buffer.concat(buffer_builder).toString())
|
||||
})
|
||||
.on('error', (err: any) => {
|
||||
// console.log(err)
|
||||
if (err.errno !== zlibConstants.Z_BUF_ERROR) { // EOF: expected
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// decompress_stream.write(lrcBuf)
|
||||
decompress_stream.end(lrcBuf)
|
||||
})
|
||||
|
||||
const decode = async(str: string): Promise<string> => {
|
||||
if (!str) return ''
|
||||
const buf = Buffer.from(str, 'hex')
|
||||
return inflate(qrc_decode(buf, buf.length))
|
||||
}
|
||||
|
||||
|
||||
// 感谢某位不愿透露姓名的大佬提供的C++算法源码,但由于作者不希望公开,所以将会以预构建二进制文件的形式加入代码仓库中
|
||||
const handleDecode = async(lrc: string, tlrc: string, rlrc: string) => {
|
||||
if (!qrc_decode) {
|
||||
// const nativeBindingPath = path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
// const nativeBindingPath = process.env.NODE_ENV !== 'production' ? path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const addon = require('qrc_decode.node')
|
||||
// console.log(addon)
|
||||
qrc_decode = addon.qrc_decode
|
||||
}
|
||||
|
||||
const [lyric, tlyric, rlyric] = await Promise.all([decode(lrc), decode(tlrc), decode(rlrc)])
|
||||
return {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default () => {
|
||||
mainHandle<{ lrc: string, tlrc: string, rlrc: string }, { lyric: string, tlyric: string, rlyric: string }>(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, async({ params: { lrc, tlrc, rlrc } }) => {
|
||||
return handleDecode(lrc, tlrc, rlrc)
|
||||
})
|
||||
}
|
||||
@@ -67,31 +67,31 @@ export default {
|
||||
}
|
||||
*/
|
||||
|
||||
const buf_key = Buffer.from('yeelion')
|
||||
const buf_key_len = buf_key.length
|
||||
const buildParams = (id, isGetLyricx) => {
|
||||
let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`
|
||||
if (isGetLyricx) params += '&lrcx=1'
|
||||
const buf_str = Buffer.from(params)
|
||||
const buf_str_len = buf_str.length
|
||||
const output = new Uint16Array(buf_str_len)
|
||||
let i = 0
|
||||
while (i < buf_str_len) {
|
||||
let j = 0
|
||||
while (j < buf_key_len && i < buf_str_len) {
|
||||
output[i] = buf_key[j] ^ buf_str[i]
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
return Buffer.from(output).toString('base64')
|
||||
}
|
||||
// const buf_key = Buffer.from('yeelion')
|
||||
// const buf_key_len = buf_key.length
|
||||
// const buildParams = (id, isGetLyricx) => {
|
||||
// let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`
|
||||
// if (isGetLyricx) params += '&lrcx=1'
|
||||
// const buf_str = Buffer.from(params)
|
||||
// const buf_str_len = buf_str.length
|
||||
// const output = new Uint16Array(buf_str_len)
|
||||
// let i = 0
|
||||
// while (i < buf_str_len) {
|
||||
// let j = 0
|
||||
// while (j < buf_key_len && i < buf_str_len) {
|
||||
// output[i] = buf_key[j] ^ buf_str[i]
|
||||
// i++
|
||||
// j++
|
||||
// }
|
||||
// }
|
||||
// return Buffer.from(output).toString('base64')
|
||||
// }
|
||||
|
||||
// console.log(buildParams('207527604', false))
|
||||
// console.log(buildParams('207527604', true))
|
||||
|
||||
const timeExp = /^\[([\d:.]*)\]{1}/g
|
||||
const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/
|
||||
const existTimeExp = /\[\d{1,2}:.*\d{1,4}]/
|
||||
const lyricxTag = /^<-?\d+,-?\d+>/
|
||||
export default {
|
||||
/* sortLrcArr(arr) {
|
||||
@@ -168,14 +168,6 @@ export default {
|
||||
|
||||
if (!isLyricx && lrcT.length > lrc.length * 0.3 && lrc.length - lrcT.length > 6) {
|
||||
throw new Error('failed')
|
||||
// if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍,否则认为没有翻译
|
||||
// const tItem = lrc.pop()
|
||||
// tItem.time = lrc[lrc.length - 1].time
|
||||
// lrcT.push(tItem)
|
||||
// } else {
|
||||
// lrc = arr
|
||||
// lrcT = []
|
||||
// }
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -235,35 +227,35 @@ export default {
|
||||
// },
|
||||
getLyric(musicInfo, isGetLyricx = true) {
|
||||
// this.getLyric2(musicInfo)
|
||||
const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`)
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {
|
||||
const requestObj = httpFetch(`http://mlyric.kuwo.cn/mobi.s?f=web&type=lyric&lrcx=${isGetLyricx ? 1 : 0}&rid=${musicInfo.songmid}&encode=utf8`)
|
||||
requestObj.promise = requestObj.promise.then(async({ statusCode, body, raw }) => {
|
||||
if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))
|
||||
return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {
|
||||
// let lrcInfo
|
||||
// try {
|
||||
// lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
|
||||
// } catch {
|
||||
// return Promise.reject(new Error('Get lyric failed'))
|
||||
// }
|
||||
let lrcInfo
|
||||
// console.log(Buffer.from(base64Data, 'base64').toString())
|
||||
try {
|
||||
lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error('Get lyric failed'))
|
||||
}
|
||||
// console.log(lrcInfo)
|
||||
if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
try {
|
||||
lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
|
||||
} catch {
|
||||
lrcInfo.lxlyric = ''
|
||||
}
|
||||
lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed'))
|
||||
// console.log(lrcInfo)
|
||||
return lrcInfo
|
||||
})
|
||||
const lrcText = await decodeLyric(raw, isGetLyricx)
|
||||
// console.log(lrcText)
|
||||
// let lrcInfo
|
||||
// try {
|
||||
// lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())
|
||||
// } catch {
|
||||
// return Promise.reject(new Error('Get lyric failed'))
|
||||
// }
|
||||
let lrcInfo
|
||||
// console.log(Buffer.from(base64Data, 'base64').toString())
|
||||
try {
|
||||
lrcInfo = this.parseLrc(lrcText)
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error('Get lyric failed'))
|
||||
}
|
||||
// console.log(lrcInfo)
|
||||
if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
try {
|
||||
lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
|
||||
} catch {
|
||||
lrcInfo.lxlyric = ''
|
||||
}
|
||||
lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed'))
|
||||
// console.log(lrcInfo)
|
||||
return lrcInfo
|
||||
})
|
||||
return requestObj
|
||||
},
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
// import { httpGet, httpFetch } from '../../request'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { rendererInvoke } from '@common/rendererIpc'
|
||||
import { createCipheriv, createDecipheriv } from 'crypto'
|
||||
import { toMD5 } from '../utils'
|
||||
import { inflate } from 'zlib'
|
||||
|
||||
// const kw_token = {
|
||||
// token: null,
|
||||
// isGetingToken: false,
|
||||
// }
|
||||
|
||||
// const translationMap = {
|
||||
// "{'": '{"',
|
||||
// "'}\n": '"}',
|
||||
// "'}": '"}',
|
||||
// "':'": '":"',
|
||||
// "','": '","',
|
||||
// "':{'": '":{"',
|
||||
// "':['": '":["',
|
||||
// "'}],'": '"}],"',
|
||||
// "':[{'": '":[{"',
|
||||
// "'},'": '"},"',
|
||||
// "'},{'": '"},{"',
|
||||
// "':[],'": '":[],"',
|
||||
// "':{},'": '":{},"',
|
||||
// "'}]}": '"}]}',
|
||||
// }
|
||||
const handleInflate = async(data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
inflate(data, (err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// export const objStr2JSON = str => {
|
||||
// return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},'|'}]})/g, s => translationMap[s]))
|
||||
// }
|
||||
const buf_key = Buffer.from('yeelion')
|
||||
const buf_key_len = buf_key.length
|
||||
|
||||
export const decodeLyric = async(rawData, isGetLyricx) => {
|
||||
const buf = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData)
|
||||
if (buf.toString('utf8', 0, 10).toLowerCase() !== 'tp=content') return ''
|
||||
const lrcData = await handleInflate(buf.subarray(buf.indexOf('\r\n\r\n') + 4))
|
||||
if (!isGetLyricx) return lrcData.toString('utf8')
|
||||
const buf_str = Buffer.from(lrcData.toString(), 'base64')
|
||||
const buf_str_len = buf_str.length
|
||||
const output = new Uint8Array(buf_str_len)
|
||||
let i = 0
|
||||
while (i < buf_str_len) {
|
||||
let j = 0
|
||||
while (j < buf_key_len && i < buf_str_len) {
|
||||
output[i] = buf_str[i] ^ buf_key[j]
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.from(output).toString('utf8')
|
||||
}
|
||||
|
||||
export const objStr2JSON = str => {
|
||||
return JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"'))
|
||||
@@ -37,13 +46,13 @@ export const objStr2JSON = str => {
|
||||
|
||||
export const formatSinger = rawData => rawData.replace(/&/g, '、')
|
||||
|
||||
export const matchToken = headers => {
|
||||
try {
|
||||
return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1]
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// export const matchToken = headers => {
|
||||
// try {
|
||||
// return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1]
|
||||
// } catch (err) {
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
|
||||
// const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))
|
||||
|
||||
@@ -63,8 +72,6 @@ export const matchToken = headers => {
|
||||
// })
|
||||
// })
|
||||
|
||||
export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data)
|
||||
|
||||
// export const tokenRequest = async(url, options = {}) => {
|
||||
// let token = kw_token.token
|
||||
// if (!token) token = await getToken()
|
||||
@@ -88,8 +95,8 @@ export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_
|
||||
|
||||
export const lrcTools = {
|
||||
rxps: {
|
||||
wordLine: /^(\[\d{1,2}:.*\d{1,4}\])\s*(\S+(?:\s+\S+)*)?\s*/,
|
||||
tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*\]/,
|
||||
wordLine: /^(\[\d{1,2}:.*\d{1,4}])\s*(\S+(?:\s+\S+)*)?\s*/,
|
||||
tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*]/,
|
||||
wordTimeAll: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/g,
|
||||
wordTime: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import getMusicInfo from './musicInfo'
|
||||
import { rendererInvoke } from '@common/rendererIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { decodeQrc } from './qrcDecode'
|
||||
|
||||
const songIdMap = new Map()
|
||||
const promises = new Map()
|
||||
export const decodeLyric = (lrc, tlrc, rlrc) => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc })
|
||||
export const decodeLyric = async(lrc, tlrc, rlrc) => ({
|
||||
lyric: await decodeQrc(lrc),
|
||||
tlyric: await decodeQrc(tlrc),
|
||||
rlyric: await decodeQrc(rlrc),
|
||||
})
|
||||
|
||||
|
||||
const parseTools = {
|
||||
|
||||
252
src/renderer/utils/musicSdk/tx/qrcDecode.js
Normal file
252
src/renderer/utils/musicSdk/tx/qrcDecode.js
Normal file
@@ -0,0 +1,252 @@
|
||||
import { inflate, constants } from 'zlib'
|
||||
|
||||
/**
|
||||
* QRC 歌词解密(3DES-ECB + zlib inflate)
|
||||
*
|
||||
* 由原 C++ native 插件(qrc_decode.node)1:1 移植为纯 JS 实现。
|
||||
* 加密算法为标准 3DES(DES-EDE3,ECB 模式、无填充),密钥固定。
|
||||
* 数据流程:hex 字符串 -> 字节 -> 3DES 解密 -> zlib inflate -> UTF-8 文本。
|
||||
*/
|
||||
|
||||
const DES_ENCRYPT = 1
|
||||
const DES_DECRYPT = 0
|
||||
|
||||
// DES S-box。注意:QRC 使用的是非标准 DES 变体,
|
||||
// 其中 S2[23]=15、S4[53]=10 两处与标准 DES 不同(标准为 14、1)。
|
||||
// 这两个值与原 native 插件(qrc_decode.node)二进制完全一致,不可改为标准值,
|
||||
// 否则解密结果错误。下表逐字节对应原 C++ 实现。
|
||||
const sbox = [
|
||||
[
|
||||
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
|
||||
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
|
||||
],
|
||||
[
|
||||
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5,
|
||||
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
|
||||
],
|
||||
[
|
||||
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
|
||||
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
|
||||
],
|
||||
[
|
||||
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
|
||||
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
|
||||
],
|
||||
[
|
||||
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
|
||||
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
|
||||
],
|
||||
[
|
||||
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
|
||||
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
|
||||
],
|
||||
[
|
||||
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
|
||||
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
|
||||
],
|
||||
[
|
||||
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
|
||||
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11,
|
||||
],
|
||||
]
|
||||
|
||||
const key_rnd_shift = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
||||
const key_perm_c = [
|
||||
56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35,
|
||||
]
|
||||
const key_perm_d = [
|
||||
62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3,
|
||||
]
|
||||
const key_compression = [
|
||||
13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46,
|
||||
54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31,
|
||||
]
|
||||
|
||||
// 全部以无符号 32 位处理,>>> 0 保证溢出行为与 C++ uint32_t 一致
|
||||
const bitnum = (a, b, c) => (((a[((b / 32) | 0) * 4 + 3 - (((b % 32) / 8) | 0)] >>> (7 - (b % 8))) & 1) << c) >>> 0
|
||||
|
||||
const bitnum_intr = (a, b, c) => (((a >>> (31 - b)) & 1) << c) >>> 0
|
||||
|
||||
const bitnum_intl = (a, b, c) => (((((a << b) >>> 0) & 0x80000000) >>> 0) >>> c) >>> 0
|
||||
|
||||
const sbox_bit = a => (a & 32) | ((a & 31) >>> 1) | ((a & 1) << 4)
|
||||
|
||||
// 初始置换:返回 [s0, s1]
|
||||
const initial_permutation = (input) => {
|
||||
const s0 = (
|
||||
bitnum(input, 57, 31) | bitnum(input, 49, 30) | bitnum(input, 41, 29) | bitnum(input, 33, 28) |
|
||||
bitnum(input, 25, 27) | bitnum(input, 17, 26) | bitnum(input, 9, 25) | bitnum(input, 1, 24) |
|
||||
bitnum(input, 59, 23) | bitnum(input, 51, 22) | bitnum(input, 43, 21) | bitnum(input, 35, 20) |
|
||||
bitnum(input, 27, 19) | bitnum(input, 19, 18) | bitnum(input, 11, 17) | bitnum(input, 3, 16) |
|
||||
bitnum(input, 61, 15) | bitnum(input, 53, 14) | bitnum(input, 45, 13) | bitnum(input, 37, 12) |
|
||||
bitnum(input, 29, 11) | bitnum(input, 21, 10) | bitnum(input, 13, 9) | bitnum(input, 5, 8) |
|
||||
bitnum(input, 63, 7) | bitnum(input, 55, 6) | bitnum(input, 47, 5) | bitnum(input, 39, 4) |
|
||||
bitnum(input, 31, 3) | bitnum(input, 23, 2) | bitnum(input, 15, 1) | bitnum(input, 7, 0)
|
||||
) >>> 0
|
||||
const s1 = (
|
||||
bitnum(input, 56, 31) | bitnum(input, 48, 30) | bitnum(input, 40, 29) | bitnum(input, 32, 28) |
|
||||
bitnum(input, 24, 27) | bitnum(input, 16, 26) | bitnum(input, 8, 25) | bitnum(input, 0, 24) |
|
||||
bitnum(input, 58, 23) | bitnum(input, 50, 22) | bitnum(input, 42, 21) | bitnum(input, 34, 20) |
|
||||
bitnum(input, 26, 19) | bitnum(input, 18, 18) | bitnum(input, 10, 17) | bitnum(input, 2, 16) |
|
||||
bitnum(input, 60, 15) | bitnum(input, 52, 14) | bitnum(input, 44, 13) | bitnum(input, 36, 12) |
|
||||
bitnum(input, 28, 11) | bitnum(input, 20, 10) | bitnum(input, 12, 9) | bitnum(input, 4, 8) |
|
||||
bitnum(input, 62, 7) | bitnum(input, 54, 6) | bitnum(input, 46, 5) | bitnum(input, 38, 4) |
|
||||
bitnum(input, 30, 3) | bitnum(input, 22, 2) | bitnum(input, 14, 1) | bitnum(input, 6, 0)
|
||||
) >>> 0
|
||||
return [s0, s1]
|
||||
}
|
||||
|
||||
// 逆置换:将 s0/s1 写回 8 字节 out
|
||||
const inverse_permutation = (s0, s1, out) => {
|
||||
out[3] = (bitnum_intr(s1, 7, 7) | bitnum_intr(s0, 7, 6) | bitnum_intr(s1, 15, 5) | bitnum_intr(s0, 15, 4) | bitnum_intr(s1, 23, 3) | bitnum_intr(s0, 23, 2) | bitnum_intr(s1, 31, 1) | bitnum_intr(s0, 31, 0)) & 0xff
|
||||
out[2] = (bitnum_intr(s1, 6, 7) | bitnum_intr(s0, 6, 6) | bitnum_intr(s1, 14, 5) | bitnum_intr(s0, 14, 4) | bitnum_intr(s1, 22, 3) | bitnum_intr(s0, 22, 2) | bitnum_intr(s1, 30, 1) | bitnum_intr(s0, 30, 0)) & 0xff
|
||||
out[1] = (bitnum_intr(s1, 5, 7) | bitnum_intr(s0, 5, 6) | bitnum_intr(s1, 13, 5) | bitnum_intr(s0, 13, 4) | bitnum_intr(s1, 21, 3) | bitnum_intr(s0, 21, 2) | bitnum_intr(s1, 29, 1) | bitnum_intr(s0, 29, 0)) & 0xff
|
||||
out[0] = (bitnum_intr(s1, 4, 7) | bitnum_intr(s0, 4, 6) | bitnum_intr(s1, 12, 5) | bitnum_intr(s0, 12, 4) | bitnum_intr(s1, 20, 3) | bitnum_intr(s0, 20, 2) | bitnum_intr(s1, 28, 1) | bitnum_intr(s0, 28, 0)) & 0xff
|
||||
out[7] = (bitnum_intr(s1, 3, 7) | bitnum_intr(s0, 3, 6) | bitnum_intr(s1, 11, 5) | bitnum_intr(s0, 11, 4) | bitnum_intr(s1, 19, 3) | bitnum_intr(s0, 19, 2) | bitnum_intr(s1, 27, 1) | bitnum_intr(s0, 27, 0)) & 0xff
|
||||
out[6] = (bitnum_intr(s1, 2, 7) | bitnum_intr(s0, 2, 6) | bitnum_intr(s1, 10, 5) | bitnum_intr(s0, 10, 4) | bitnum_intr(s1, 18, 3) | bitnum_intr(s0, 18, 2) | bitnum_intr(s1, 26, 1) | bitnum_intr(s0, 26, 0)) & 0xff
|
||||
out[5] = (bitnum_intr(s1, 1, 7) | bitnum_intr(s0, 1, 6) | bitnum_intr(s1, 9, 5) | bitnum_intr(s0, 9, 4) | bitnum_intr(s1, 17, 3) | bitnum_intr(s0, 17, 2) | bitnum_intr(s1, 25, 1) | bitnum_intr(s0, 25, 0)) & 0xff
|
||||
out[4] = (bitnum_intr(s1, 0, 7) | bitnum_intr(s0, 0, 6) | bitnum_intr(s1, 8, 5) | bitnum_intr(s0, 8, 4) | bitnum_intr(s1, 16, 3) | bitnum_intr(s0, 16, 2) | bitnum_intr(s1, 24, 1) | bitnum_intr(s0, 24, 0)) & 0xff
|
||||
}
|
||||
|
||||
// Feistel 轮函数 f(state, key)
|
||||
const des_f = (state, key) => {
|
||||
const t1 = (
|
||||
bitnum_intl(state, 31, 0) | (((state & 0xF0000000) >>> 0) >>> 1) | bitnum_intl(state, 4, 5) |
|
||||
bitnum_intl(state, 3, 6) | (((state & 0x0F000000) >>> 0) >>> 3) | bitnum_intl(state, 8, 11) |
|
||||
bitnum_intl(state, 7, 12) | (((state & 0x00F00000) >>> 0) >>> 5) | bitnum_intl(state, 12, 17) |
|
||||
bitnum_intl(state, 11, 18) | (((state & 0x000F0000) >>> 0) >>> 7) | bitnum_intl(state, 16, 23)
|
||||
) >>> 0
|
||||
const t2 = (
|
||||
bitnum_intl(state, 15, 0) | (((state & 0x0000F000) << 15) >>> 0) | bitnum_intl(state, 20, 5) |
|
||||
bitnum_intl(state, 19, 6) | (((state & 0x00000F00) << 13) >>> 0) | bitnum_intl(state, 24, 11) |
|
||||
bitnum_intl(state, 23, 12) | (((state & 0x000000F0) << 11) >>> 0) | bitnum_intl(state, 28, 17) |
|
||||
bitnum_intl(state, 27, 18) | (((state & 0x0000000F) << 9) >>> 0) | bitnum_intl(state, 0, 23)
|
||||
) >>> 0
|
||||
|
||||
const lrgstate = [
|
||||
(t1 >>> 24) & 0xFF, (t1 >>> 16) & 0xFF, (t1 >>> 8) & 0xFF,
|
||||
(t2 >>> 24) & 0xFF, (t2 >>> 16) & 0xFF, (t2 >>> 8) & 0xFF,
|
||||
]
|
||||
for (let i = 0; i < 6; i++) lrgstate[i] ^= key[i]
|
||||
|
||||
let s = (
|
||||
(sbox[0][sbox_bit(lrgstate[0] >>> 2)] << 28) |
|
||||
(sbox[1][sbox_bit(((lrgstate[0] & 0x03) << 4) | (lrgstate[1] >>> 4))] << 24) |
|
||||
(sbox[2][sbox_bit(((lrgstate[1] & 0x0F) << 2) | (lrgstate[2] >>> 6))] << 20) |
|
||||
(sbox[3][sbox_bit(lrgstate[2] & 0x3F)] << 16) |
|
||||
(sbox[4][sbox_bit(lrgstate[3] >>> 2)] << 12) |
|
||||
(sbox[5][sbox_bit(((lrgstate[3] & 0x03) << 4) | (lrgstate[4] >>> 4))] << 8) |
|
||||
(sbox[6][sbox_bit(((lrgstate[4] & 0x0F) << 2) | (lrgstate[5] >>> 6))] << 4) |
|
||||
sbox[7][sbox_bit(lrgstate[5] & 0x3F)]
|
||||
) >>> 0
|
||||
|
||||
return (
|
||||
bitnum_intl(s, 15, 0) | bitnum_intl(s, 6, 1) | bitnum_intl(s, 19, 2) | bitnum_intl(s, 20, 3) |
|
||||
bitnum_intl(s, 28, 4) | bitnum_intl(s, 11, 5) | bitnum_intl(s, 27, 6) | bitnum_intl(s, 16, 7) |
|
||||
bitnum_intl(s, 0, 8) | bitnum_intl(s, 14, 9) | bitnum_intl(s, 22, 10) | bitnum_intl(s, 25, 11) |
|
||||
bitnum_intl(s, 4, 12) | bitnum_intl(s, 17, 13) | bitnum_intl(s, 30, 14) | bitnum_intl(s, 9, 15) |
|
||||
bitnum_intl(s, 1, 16) | bitnum_intl(s, 7, 17) | bitnum_intl(s, 23, 18) | bitnum_intl(s, 13, 19) |
|
||||
bitnum_intl(s, 31, 20) | bitnum_intl(s, 26, 21) | bitnum_intl(s, 2, 22) | bitnum_intl(s, 8, 23) |
|
||||
bitnum_intl(s, 18, 24) | bitnum_intl(s, 12, 25) | bitnum_intl(s, 29, 26) | bitnum_intl(s, 5, 27) |
|
||||
bitnum_intl(s, 21, 28) | bitnum_intl(s, 10, 29) | bitnum_intl(s, 3, 30) | bitnum_intl(s, 24, 31)
|
||||
) >>> 0
|
||||
}
|
||||
|
||||
// 单次 DES 加/解密一个 8 字节分组(轮密钥 schedule 为 16x6)
|
||||
const des_crypt = (input, schedule, output) => {
|
||||
let [s0, s1] = initial_permutation(input)
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const prev = s1
|
||||
s1 = (des_f(s1, schedule[i]) ^ s0) >>> 0
|
||||
s0 = prev
|
||||
}
|
||||
s0 = (des_f(s1, schedule[15]) ^ s0) >>> 0
|
||||
inverse_permutation(s0, s1, output)
|
||||
}
|
||||
|
||||
// 生成 16 轮子密钥
|
||||
const key_schedule = (key, mode) => {
|
||||
const schedule = Array.from({ length: 16 }, () => new Uint8Array(6))
|
||||
let c = 0
|
||||
let d = 0
|
||||
for (let i = 0; i < 28; i++) {
|
||||
c = (c | bitnum(key, key_perm_c[i], 31 - i)) >>> 0
|
||||
d = (d | bitnum(key, key_perm_d[i], 31 - i)) >>> 0
|
||||
}
|
||||
for (let i = 0; i < 16; i++) {
|
||||
c = ((((c << key_rnd_shift[i]) >>> 0) | (c >>> (28 - key_rnd_shift[i]))) & 0xFFFFFFF0) >>> 0
|
||||
d = ((((d << key_rnd_shift[i]) >>> 0) | (d >>> (28 - key_rnd_shift[i]))) & 0xFFFFFFF0) >>> 0
|
||||
const togen = mode === DES_DECRYPT ? 15 - i : i
|
||||
for (let j = 0; j < 24; j++) {
|
||||
schedule[togen][(j / 8) | 0] |= bitnum_intr(c, key_compression[j], 7 - (j % 8))
|
||||
}
|
||||
for (let j = 24; j < 48; j++) {
|
||||
schedule[togen][(j / 8) | 0] |= bitnum_intr(d, key_compression[j] - 27, 7 - (j % 8))
|
||||
}
|
||||
}
|
||||
return schedule
|
||||
}
|
||||
|
||||
// 3DES(EDE3)密钥编排,返回 3 组 schedule
|
||||
const tripledes_key_setup = (key, mode) => {
|
||||
if (mode === DES_ENCRYPT) {
|
||||
return [
|
||||
key_schedule(key.subarray(0, 8), DES_ENCRYPT),
|
||||
key_schedule(key.subarray(8, 16), DES_DECRYPT),
|
||||
key_schedule(key.subarray(16, 24), DES_ENCRYPT),
|
||||
]
|
||||
}
|
||||
return [
|
||||
key_schedule(key.subarray(16, 24), DES_DECRYPT),
|
||||
key_schedule(key.subarray(8, 16), DES_ENCRYPT),
|
||||
key_schedule(key.subarray(0, 8), DES_DECRYPT),
|
||||
]
|
||||
}
|
||||
|
||||
// 3DES 加/解密一个 8 字节分组
|
||||
const tripledes_crypt = (input, schedule, output) => {
|
||||
const buf = new Uint8Array(8)
|
||||
des_crypt(input, schedule[0], buf)
|
||||
des_crypt(buf, schedule[1], output)
|
||||
des_crypt(output, schedule[2], buf)
|
||||
output.set(buf)
|
||||
}
|
||||
|
||||
// QRC 固定密钥:!@#)(*$%123ZXC!@!@#)(NHL
|
||||
const QRC_KEY = Buffer.from([
|
||||
0x21, 0x40, 0x23, 0x29, 0x28, 0x2a, 0x24, 0x25, 0x31, 0x32, 0x33, 0x5a,
|
||||
0x58, 0x43, 0x21, 0x40, 0x21, 0x40, 0x23, 0x29, 0x28, 0x4e, 0x48, 0x4c,
|
||||
])
|
||||
|
||||
const handleInflate = (data) => new Promise((resolve, reject) => {
|
||||
// Z_SYNC_FLUSH:容忍尾部不完整的 zlib 流(对应原实现忽略 Z_BUF_ERROR 的行为)
|
||||
inflate(data, { finishFlush: constants.Z_SYNC_FLUSH }, (err, result) => {
|
||||
if (err) reject(err)
|
||||
else resolve(result)
|
||||
})
|
||||
})
|
||||
/**
|
||||
* 解密腾讯 QRC 歌词。
|
||||
* @param {string} hexData 服务端返回的十六进制字符串
|
||||
* @returns {Promise<string>} 解密并解压后的歌词文本(解析失败时返回空字符串)
|
||||
*/
|
||||
export const decodeQrc = async(hexData) => {
|
||||
if (!hexData || hexData.length % 2 !== 0) return ''
|
||||
const encrypted = Buffer.from(hexData, 'hex')
|
||||
if (encrypted.length === 0) return ''
|
||||
|
||||
const schedule = tripledes_key_setup(QRC_KEY, DES_DECRYPT)
|
||||
|
||||
const block = new Uint8Array(8)
|
||||
for (let i = 0; i + 8 <= encrypted.length; i += 8) {
|
||||
tripledes_crypt(encrypted.subarray(i, i + 8), schedule, block)
|
||||
encrypted.set(block, i)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handleInflate(encrypted)
|
||||
return result.toString('utf8')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user