diff --git a/build-config/build-before-pack.js b/build-config/build-before-pack.js index 36f1849a..9507cb9f 100644 --- a/build-config/build-before-pack.js +++ b/build-config/build-before-pack.js @@ -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') diff --git a/build-config/build-pack.js b/build-config/build-pack.js index 569252c8..5cc09d48 100644 --- a/build-config/build-pack.js +++ b/build-config/build-pack.js @@ -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: { diff --git a/build-config/lib-update.js b/build-config/lib-update.js index 9fef65c0..20f1b7df 100644 --- a/build-config/lib-update.js +++ b/build-config/lib-update.js @@ -25,7 +25,6 @@ const unzip = async(filePath) => { } const files = [ - 'qrc_decode', 'better_sqlite3', ] const moveFile = async(filePath) => { diff --git a/build-config/lib/qrc_decode_electron-v110-win32-arm64.node b/build-config/lib/qrc_decode_electron-v110-win32-arm64.node deleted file mode 100644 index 5b46ea64..00000000 Binary files a/build-config/lib/qrc_decode_electron-v110-win32-arm64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v110-win32-ia32.node b/build-config/lib/qrc_decode_electron-v110-win32-ia32.node deleted file mode 100644 index 229c7927..00000000 Binary files a/build-config/lib/qrc_decode_electron-v110-win32-ia32.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v110-win32-x64.node b/build-config/lib/qrc_decode_electron-v110-win32-x64.node deleted file mode 100644 index dc735eae..00000000 Binary files a/build-config/lib/qrc_decode_electron-v110-win32-x64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-darwin-arm64.node b/build-config/lib/qrc_decode_electron-v143-darwin-arm64.node deleted file mode 100644 index b0ea58e8..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-darwin-arm64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-darwin-x64.node b/build-config/lib/qrc_decode_electron-v143-darwin-x64.node deleted file mode 100644 index e1047e50..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-darwin-x64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-linux-arm.node b/build-config/lib/qrc_decode_electron-v143-linux-arm.node deleted file mode 100644 index 281c14fe..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-linux-arm.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-linux-arm64.node b/build-config/lib/qrc_decode_electron-v143-linux-arm64.node deleted file mode 100644 index ed1f5d10..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-linux-arm64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-linux-x64.node b/build-config/lib/qrc_decode_electron-v143-linux-x64.node deleted file mode 100644 index 2431d790..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-linux-x64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-win32-arm64.node b/build-config/lib/qrc_decode_electron-v143-win32-arm64.node deleted file mode 100644 index 86a3ca3d..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-win32-arm64.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-win32-ia32.node b/build-config/lib/qrc_decode_electron-v143-win32-ia32.node deleted file mode 100644 index 09922838..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-win32-ia32.node and /dev/null differ diff --git a/build-config/lib/qrc_decode_electron-v143-win32-x64.node b/build-config/lib/qrc_decode_electron-v143-win32-x64.node deleted file mode 100644 index 91950d1b..00000000 Binary files a/build-config/lib/qrc_decode_electron-v143-win32-x64.node and /dev/null differ diff --git a/build-config/main/webpack.config.base.js b/build-config/main/webpack.config.base.js index bfd839c6..6c309c7a 100644 --- a/build-config/main/webpack.config.base.js +++ b/build-config/main/webpack.config.base.js @@ -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: { diff --git a/src/common/ipcNames.ts b/src/common/ipcNames.ts index 58ee3100..123a426e 100644 --- a/src/common/ipcNames.ts +++ b/src/common/ipcNames.ts @@ -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', diff --git a/src/main/modules/winMain/rendererEvent/index.ts b/src/main/modules/winMain/rendererEvent/index.ts index f2200ad7..65ea458a 100644 --- a/src/main/modules/winMain/rendererEvent/index.ts +++ b/src/main/modules/winMain/rendererEvent/index.ts @@ -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() diff --git a/src/main/modules/winMain/rendererEvent/kw_decodeLyric.ts b/src/main/modules/winMain/rendererEvent/kw_decodeLyric.ts deleted file mode 100644 index 97e55e47..00000000 --- a/src/main/modules/winMain/rendererEvent/kw_decodeLyric.ts +++ /dev/null @@ -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') - }) -} diff --git a/src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts b/src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts deleted file mode 100644 index 10303645..00000000 --- a/src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts +++ /dev/null @@ -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((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 => { - 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) - }) -} diff --git a/src/renderer/utils/musicSdk/kw/lyric.js b/src/renderer/utils/musicSdk/kw/lyric.js index 41264ce0..69c224d3 100644 --- a/src/renderer/utils/musicSdk/kw/lyric.js +++ b/src/renderer/utils/musicSdk/kw/lyric.js @@ -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 }, diff --git a/src/renderer/utils/musicSdk/kw/util.js b/src/renderer/utils/musicSdk/kw/util.js index 8ef869a4..61bffbec 100644 --- a/src/renderer/utils/musicSdk/kw/util.js +++ b/src/renderer/utils/musicSdk/kw/util.js @@ -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+)?>/, }, diff --git a/src/renderer/utils/musicSdk/tx/lyric.js b/src/renderer/utils/musicSdk/tx/lyric.js index a6e5ae1e..ec2a809b 100644 --- a/src/renderer/utils/musicSdk/tx/lyric.js +++ b/src/renderer/utils/musicSdk/tx/lyric.js @@ -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 = { diff --git a/src/renderer/utils/musicSdk/tx/qrcDecode.js b/src/renderer/utils/musicSdk/tx/qrcDecode.js new file mode 100644 index 00000000..ae5e8176 --- /dev/null +++ b/src/renderer/utils/musicSdk/tx/qrcDecode.js @@ -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} 解密并解压后的歌词文本(解析失败时返回空字符串) + */ +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 '' + } +}