初次提交
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
|
/typing
|
||||||
|
/package-lock.json
|
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
||||||
|
/package-lock.json
|
||||||
|
/tsconfig.json
|
||||||
|
/.gitignore
|
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "启动程序",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/dist/test/index.js",
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/**/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
37
README.md
Normal file
37
README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# smart-lyric
|
||||||
|
|
||||||
|
一个歌词下载、解析、转换工具。
|
||||||
|
|
||||||
|
支持格式: lrc(常规歌词), qrc(QQ音乐歌词), krc(酷狗音乐歌词), lrcx(酷我音乐歌词), nrc(网易云音乐歌词)。
|
||||||
|
|
||||||
|
|
||||||
|
## 申明
|
||||||
|
|
||||||
|
1. 工具仅可用于个人用途,请勿用于商业用途。
|
||||||
|
|
||||||
|
1. 次工具代码不开源
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
```typescript
|
||||||
|
import fs from 'fs'
|
||||||
|
import {utils, qrc, trc, krc, lrcx, lrc} from 'smart-lyric'
|
||||||
|
|
||||||
|
// 以QQ音乐歌词为例
|
||||||
|
// 其他的以此类推
|
||||||
|
async function example(){
|
||||||
|
|
||||||
|
// 从QQ音乐下载歌词
|
||||||
|
const qrcTextFromNetwork = await utils.downloadQQMusicLyric({songID: 102878776})
|
||||||
|
console.log(qrcTextFromNetwork) //输出qrc歌词的XML文本
|
||||||
|
|
||||||
|
|
||||||
|
// 解码.qrc格式的歌词
|
||||||
|
const qrcTextFromFile = qrc.decrypt(fs.readFileSync('path to .qrc file'))
|
||||||
|
console.log(qrcTextFromFile) //输出qrc歌词的XML文本
|
||||||
|
|
||||||
|
// 歌词内容解析
|
||||||
|
const lyric = qrc.parse(qrcTextFromFile)
|
||||||
|
console.log(lyric) //输出解析后的歌词信息
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "smart-lyric",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "typing/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -r dist typing && tsc"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"lyrc",
|
||||||
|
"qrc",
|
||||||
|
"krc",
|
||||||
|
"lrcx",
|
||||||
|
"nrc",
|
||||||
|
"qqmusic",
|
||||||
|
"kugou",
|
||||||
|
"neteasy",
|
||||||
|
"kuwo",
|
||||||
|
"歌词",
|
||||||
|
"解密",
|
||||||
|
"encode",
|
||||||
|
"decode"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.11.18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bytenode": "^1.3.7",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"yizhi-html-escape": "^1.0.3"
|
||||||
|
}
|
||||||
|
}
|
6
src/index.ts
Normal file
6
src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * as lrc from './lyric/lrc'
|
||||||
|
export * as qrc from './lyric/qrc'
|
||||||
|
export * as krc from './lyric/krc'
|
||||||
|
export * as lrcx from './lyric/lrcx'
|
||||||
|
export * as nrc from './lyric/nrc'
|
||||||
|
export * as utils from './net'
|
313
src/lib/des.ts
Normal file
313
src/lib/des.ts
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
const MASK32 = 0xFFFFFFFF
|
||||||
|
|
||||||
|
/*定义一个密钥置换的映射*/
|
||||||
|
const DES_TRANSFORM = Buffer.from([
|
||||||
|
57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18,
|
||||||
|
10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36,
|
||||||
|
63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22,
|
||||||
|
14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4,
|
||||||
|
])
|
||||||
|
|
||||||
|
/*定义用于计算子密钥的旋转次数*/
|
||||||
|
const DES_ROTATIONS = Buffer.from([1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1])
|
||||||
|
|
||||||
|
/*定义用于子密钥置换选择的映射*/
|
||||||
|
const DES_PERMUTED = Buffer.from([
|
||||||
|
14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10,
|
||||||
|
23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2,
|
||||||
|
41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48,
|
||||||
|
44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32,
|
||||||
|
])
|
||||||
|
|
||||||
|
/*定义用于数据块初始化转换的映射*/
|
||||||
|
const DES_INITIAL = Buffer.from([
|
||||||
|
58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4,
|
||||||
|
62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8,
|
||||||
|
57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
|
||||||
|
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7
|
||||||
|
])
|
||||||
|
|
||||||
|
/*数据块的最终置换*/
|
||||||
|
const DES_FINAL = Buffer.from([
|
||||||
|
40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31,
|
||||||
|
38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,
|
||||||
|
36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
|
||||||
|
34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25
|
||||||
|
])
|
||||||
|
|
||||||
|
/*数据块的P盒置换*/
|
||||||
|
const DES_PBOX = Buffer.from([
|
||||||
|
16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10,
|
||||||
|
2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25,
|
||||||
|
])
|
||||||
|
|
||||||
|
/*定义用于数据块扩展转换的映射*/
|
||||||
|
const DES_EXPANSION = Buffer.from([
|
||||||
|
32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9,
|
||||||
|
8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17,
|
||||||
|
16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25,
|
||||||
|
24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1,
|
||||||
|
])
|
||||||
|
|
||||||
|
/*定义用于数据块中S盒转换的S盒表*/
|
||||||
|
const DES_SBOX = Buffer.from([
|
||||||
|
0x0E, 0x00, 0x04, 0x0F, 0x0D, 0x07, 0x01, 0x04, 0x02, 0x0E, 0x0F, 0x02, 0x0B, 0x0D, 0x08, 0x01,
|
||||||
|
0x03, 0x0A, 0x0A, 0x06, 0x06, 0x0C, 0x0C, 0x0B, 0x05, 0x09, 0x09, 0x05, 0x00, 0x03, 0x07, 0x08,
|
||||||
|
0x04, 0x0F, 0x01, 0x0C, 0x0E, 0x08, 0x08, 0x02, 0x0D, 0x04, 0x06, 0x09, 0x02, 0x01, 0x0B, 0x07,
|
||||||
|
0x0F, 0x05, 0x0C, 0x0B, 0x09, 0x03, 0x07, 0x0E, 0x03, 0x0A, 0x0A, 0x00, 0x05, 0x06, 0x00, 0x0D,
|
||||||
|
0x0F, 0x03, 0x01, 0x0D, 0x08, 0x04, 0x0E, 0x07, 0x06, 0x0F, 0x0B, 0x02, 0x03, 0x08, 0x04, 0x0F,
|
||||||
|
0x09, 0x0C, 0x07, 0x00, 0x02, 0x01, 0x0D, 0x0A, 0x0C, 0x06, 0x00, 0x09, 0x05, 0x0B, 0x0A, 0x05,
|
||||||
|
0x00, 0x0D, 0x0E, 0x08, 0x07, 0x0A, 0x0B, 0x01, 0x0A, 0x03, 0x04, 0x0F, 0x0D, 0x04, 0x01, 0x02,
|
||||||
|
0x05, 0x0B, 0x08, 0x06, 0x0C, 0x07, 0x06, 0x0C, 0x09, 0x00, 0x03, 0x05, 0x02, 0x0E, 0x0F, 0x09,
|
||||||
|
0x0A, 0x0D, 0x00, 0x07, 0x09, 0x00, 0x0E, 0x09, 0x06, 0x03, 0x03, 0x04, 0x0F, 0x06, 0x05, 0x0A,
|
||||||
|
0x01, 0x02, 0x0D, 0x08, 0x0C, 0x05, 0x07, 0x0E, 0x0B, 0x0C, 0x04, 0x0B, 0x02, 0x0F, 0x08, 0x01,
|
||||||
|
0x0D, 0x01, 0x06, 0x0A, 0x04, 0x0D, 0x09, 0x00, 0x08, 0x06, 0x0F, 0x09, 0x03, 0x08, 0x00, 0x07,
|
||||||
|
0x0B, 0x04, 0x01, 0x0F, 0x02, 0x0E, 0x0C, 0x03, 0x05, 0x0B, 0x0A, 0x05, 0x0E, 0x02, 0x07, 0x0C,
|
||||||
|
0x07, 0x0D, 0x0D, 0x08, 0x0E, 0x0B, 0x03, 0x05, 0x00, 0x06, 0x06, 0x0F, 0x09, 0x00, 0x0A, 0x03,
|
||||||
|
0x01, 0x04, 0x02, 0x07, 0x08, 0x02, 0x05, 0x0C, 0x0B, 0x01, 0x0C, 0x0A, 0x04, 0x0E, 0x0F, 0x09,
|
||||||
|
0x0A, 0x03, 0x06, 0x0F, 0x09, 0x00, 0x00, 0x06, 0x0C, 0x0A, 0x0B, 0x0A, 0x07, 0x0D, 0x0D, 0x08,
|
||||||
|
0x0F, 0x09, 0x01, 0x04, 0x03, 0x05, 0x0E, 0x0B, 0x05, 0x0C, 0x02, 0x07, 0x08, 0x02, 0x04, 0x0E,
|
||||||
|
0x02, 0x0E, 0x0C, 0x0B, 0x04, 0x02, 0x01, 0x0C, 0x07, 0x04, 0x0A, 0x07, 0x0B, 0x0D, 0x06, 0x01,
|
||||||
|
0x08, 0x05, 0x05, 0x00, 0x03, 0x0F, 0x0F, 0x0A, 0x0D, 0x03, 0x00, 0x09, 0x0E, 0x08, 0x09, 0x06,
|
||||||
|
0x04, 0x0B, 0x02, 0x08, 0x01, 0x0C, 0x0B, 0x07, 0x0A, 0x01, 0x0D, 0x0E, 0x07, 0x02, 0x08, 0x0D,
|
||||||
|
0x0F, 0x06, 0x09, 0x0F, 0x0C, 0x00, 0x05, 0x09, 0x06, 0x0A, 0x03, 0x04, 0x00, 0x05, 0x0E, 0x03,
|
||||||
|
0x0C, 0x0A, 0x01, 0x0F, 0x0A, 0x04, 0x0F, 0x02, 0x09, 0x07, 0x02, 0x0C, 0x06, 0x09, 0x08, 0x05,
|
||||||
|
0x00, 0x06, 0x0D, 0x01, 0x03, 0x0D, 0x04, 0x0E, 0x0E, 0x00, 0x07, 0x0B, 0x05, 0x03, 0x0B, 0x08,
|
||||||
|
0x09, 0x04, 0x0E, 0x03, 0x0F, 0x02, 0x05, 0x0C, 0x02, 0x09, 0x08, 0x05, 0x0C, 0x0F, 0x03, 0x0A,
|
||||||
|
0x07, 0x0B, 0x00, 0x0E, 0x04, 0x01, 0x0A, 0x07, 0x01, 0x06, 0x0D, 0x00, 0x0B, 0x08, 0x06, 0x0D,
|
||||||
|
0x04, 0x0D, 0x0B, 0x00, 0x02, 0x0B, 0x0E, 0x07, 0x0F, 0x04, 0x00, 0x09, 0x08, 0x01, 0x0D, 0x0A,
|
||||||
|
0x03, 0x0E, 0x0C, 0x03, 0x09, 0x05, 0x07, 0x0C, 0x05, 0x02, 0x0A, 0x0F, 0x06, 0x08, 0x01, 0x06,
|
||||||
|
0x01, 0x06, 0x04, 0x0B, 0x0B, 0x0D, 0x0D, 0x08, 0x0C, 0x01, 0x03, 0x04, 0x07, 0x0A, 0x0E, 0x07,
|
||||||
|
0x0A, 0x09, 0x0F, 0x05, 0x06, 0x00, 0x08, 0x0F, 0x00, 0x0E, 0x05, 0x02, 0x09, 0x03, 0x02, 0x0C,
|
||||||
|
0x0D, 0x01, 0x02, 0x0F, 0x08, 0x0D, 0x04, 0x08, 0x06, 0x0A, 0x0F, 0x03, 0x0B, 0x07, 0x01, 0x04,
|
||||||
|
0x0A, 0x0C, 0x09, 0x05, 0x03, 0x06, 0x0E, 0x0B, 0x05, 0x00, 0x00, 0x0E, 0x0C, 0x09, 0x07, 0x02,
|
||||||
|
0x07, 0x02, 0x0B, 0x01, 0x04, 0x0E, 0x01, 0x07, 0x09, 0x04, 0x0C, 0x0A, 0x0E, 0x08, 0x02, 0x0D,
|
||||||
|
0x00, 0x0F, 0x06, 0x0C, 0x0A, 0x09, 0x0D, 0x00, 0x0F, 0x03, 0x03, 0x05, 0x05, 0x06, 0x08, 0x0B,
|
||||||
|
])
|
||||||
|
|
||||||
|
/* 位掩码 */
|
||||||
|
const BITOF = (n: number) => (1 << n) >>> 0
|
||||||
|
const DES_BITMAP = [
|
||||||
|
BITOF(31), BITOF(30), BITOF(29), BITOF(28), BITOF(27), BITOF(26), BITOF(25), BITOF(24),
|
||||||
|
BITOF(23), BITOF(22), BITOF(21), BITOF(20), BITOF(19), BITOF(18), BITOF(17), BITOF(16),
|
||||||
|
BITOF(15), BITOF(14), BITOF(13), BITOF(12), BITOF(11), BITOF(10), BITOF(9), BITOF(8),
|
||||||
|
BITOF(7), BITOF(6), BITOF(5), BITOF(4), BITOF(3), BITOF(2), BITOF(1), BITOF(0),
|
||||||
|
BITOF(31), BITOF(30), BITOF(29), BITOF(28), BITOF(27), BITOF(26), BITOF(25), BITOF(24),
|
||||||
|
BITOF(23), BITOF(22), BITOF(21), BITOF(20), BITOF(19), BITOF(18), BITOF(17), BITOF(16),
|
||||||
|
BITOF(15), BITOF(14), BITOF(13), BITOF(12), BITOF(11), BITOF(10), BITOF(9), BITOF(8),
|
||||||
|
BITOF(7), BITOF(6), BITOF(5), BITOF(4), BITOF(3), BITOF(2), BITOF(1), BITOF(0),
|
||||||
|
]
|
||||||
|
|
||||||
|
const DES_ROTATION_MASK = [0, 0x80000000, 0xc0000000];
|
||||||
|
|
||||||
|
|
||||||
|
//子密钥
|
||||||
|
interface ISubKey {
|
||||||
|
low: number
|
||||||
|
high: number
|
||||||
|
}
|
||||||
|
//16个子密钥
|
||||||
|
const subKeys: ISubKey[] = []
|
||||||
|
const tempKey: ISubKey = { low: 0, high: 0 }
|
||||||
|
|
||||||
|
/** 数据信息 */
|
||||||
|
interface IData64 {
|
||||||
|
/** 低32位 */
|
||||||
|
low: number
|
||||||
|
/** 高32位 */
|
||||||
|
high: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DES方法 */
|
||||||
|
export enum DesType {
|
||||||
|
/** 加密 */
|
||||||
|
Encode,
|
||||||
|
/** 解密 */
|
||||||
|
Decode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将数字转换为无符号32位整数 */
|
||||||
|
function uint32(v: number) {
|
||||||
|
return v >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成子密钥
|
||||||
|
* @param index 子密钥位置
|
||||||
|
*/
|
||||||
|
function createSubKey(index: number) {
|
||||||
|
const sub = subKeys[index]
|
||||||
|
const rcnt = DES_ROTATIONS[index]
|
||||||
|
|
||||||
|
let v6 = tempKey.low & DES_ROTATION_MASK[rcnt]
|
||||||
|
let v7 = tempKey.high & DES_ROTATION_MASK[rcnt]
|
||||||
|
if (rcnt == 1) {
|
||||||
|
v6 = v6 >>> 27
|
||||||
|
v7 = v7 >>> 27
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
v6 = v6 >>> 26
|
||||||
|
v7 = v7 >>> 26
|
||||||
|
}
|
||||||
|
|
||||||
|
tempKey.low = uint32(tempKey.low << rcnt) & MASK32
|
||||||
|
tempKey.high = uint32(tempKey.high << rcnt) & MASK32
|
||||||
|
tempKey.low = uint32(tempKey.low | uint32(v6 & 0xFFFFFFF0))
|
||||||
|
tempKey.high = uint32(tempKey.high | uint32(v7 & 0xFFFFFFF0))
|
||||||
|
|
||||||
|
for (let i = 0; i < 48; ++i) {
|
||||||
|
const v10 = DES_PERMUTED[i]
|
||||||
|
if (i >= 24) {
|
||||||
|
if (DES_BITMAP[v10 - 29 + 1] & tempKey.high) sub.high = uint32(sub.high | DES_BITMAP[i - 24])
|
||||||
|
}
|
||||||
|
else if (tempKey.low & DES_BITMAP[v10 - 1]) sub.low = uint32(sub.low | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化子密钥
|
||||||
|
* @param key 密钥
|
||||||
|
*/
|
||||||
|
function makeFirstKey(key: Buffer) {
|
||||||
|
const first: ISubKey = { low: key.readUint32LE(0), high: key.readUint32LE(4) }
|
||||||
|
tempKey.high = tempKey.low = 0
|
||||||
|
|
||||||
|
//初始化自密钥
|
||||||
|
if (!subKeys.length) {
|
||||||
|
for (let i = 0; i < 16; ++i) subKeys.push({ low: 0, high: 0 });
|
||||||
|
}
|
||||||
|
else subKeys.forEach(k => k.high = k.low = 0)
|
||||||
|
|
||||||
|
// 将key转置压缩为56位
|
||||||
|
for (let i = 0; i < 28; ++i) {
|
||||||
|
let item = DES_TRANSFORM[i]
|
||||||
|
let got = (item <= 32 ? first.low : first.high) & DES_BITMAP[item - 1] //置换映射表从1开始的,这里-1
|
||||||
|
if (got) tempKey.low = uint32(tempKey.low | DES_BITMAP[i])
|
||||||
|
|
||||||
|
item = DES_TRANSFORM[i + 28]
|
||||||
|
got = (item <= 32 ? first.low : first.high) & DES_BITMAP[item - 1]
|
||||||
|
if (got) tempKey.high = uint32(tempKey.high | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
//计算16个子密钥
|
||||||
|
for (let i = 0; i < 16; ++i) createSubKey(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据迭代
|
||||||
|
* @param data 要迭代的数据
|
||||||
|
* @param subKey 子密钥
|
||||||
|
*/
|
||||||
|
function makeData(data: IData64, subKey: ISubKey) {
|
||||||
|
let low = 0
|
||||||
|
let high = 0;
|
||||||
|
const tempHigh = data.high
|
||||||
|
|
||||||
|
for (let i = 0; i < 48; ++i) {
|
||||||
|
const mask = DES_BITMAP[DES_EXPANSION[i] - 1];
|
||||||
|
if (i >= 24) {
|
||||||
|
if (mask & tempHigh) high = uint32(high | DES_BITMAP[i - 24])
|
||||||
|
}
|
||||||
|
else if (mask & tempHigh) low = uint32(low | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
low = uint32(low ^ subKey.low)
|
||||||
|
high = uint32(high ^ subKey.high)
|
||||||
|
|
||||||
|
const buffer = Buffer.from([
|
||||||
|
(low >>> 26) & 0x3F,
|
||||||
|
(low >>> 20) & 0x3F,
|
||||||
|
(low >>> 14) & 0x3f,
|
||||||
|
(low >>> 8) & 0x3f,
|
||||||
|
(high >>> 26) & 0x3F,
|
||||||
|
(high >>> 20) & 0x3F,
|
||||||
|
(high >>> 14) & 0x3f,
|
||||||
|
(high >>> 8) & 0x3f,
|
||||||
|
])
|
||||||
|
|
||||||
|
data.high = 0;
|
||||||
|
let byte_index = 0;
|
||||||
|
for (let i = 0; i < 448; i += 64, ++byte_index) {
|
||||||
|
data.high = uint32(data.high | DES_SBOX[buffer[byte_index] + i]) * 16
|
||||||
|
}
|
||||||
|
data.high = uint32(data.high | DES_SBOX[64 * byte_index + buffer[byte_index]])
|
||||||
|
|
||||||
|
let tmp = 0;
|
||||||
|
for (let i = 0; i < 32; ++i) {
|
||||||
|
if (data.high & DES_BITMAP[DES_PBOX[i] - 1]) tmp = uint32(tmp | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
data.high = uint32(tmp ^ data.low)
|
||||||
|
data.low = tempHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理数据
|
||||||
|
* @param type 操作类型
|
||||||
|
* @param data 操作的单个64位数据
|
||||||
|
*/
|
||||||
|
function handleData(type: DesType, data: IData64) {
|
||||||
|
let mask_ok: number;
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let high = 0;
|
||||||
|
for (let i = 0; i < 64; ++i) {
|
||||||
|
const v5 = DES_INITIAL[i];
|
||||||
|
if (i >= 32) {
|
||||||
|
if (v5 <= 32) mask_ok = uint32(DES_BITMAP[v5 - 1] & data.low)
|
||||||
|
else mask_ok = uint32(DES_BITMAP[v5 - 1] & data.high)
|
||||||
|
if (mask_ok) high = uint32(high | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (v5 <= 32) mask_ok = uint32(DES_BITMAP[v5 - 1] & data.low)
|
||||||
|
else mask_ok = uint32(DES_BITMAP[v5 - 1] & data.high)
|
||||||
|
if (mask_ok) low = uint32(low | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.low = low;
|
||||||
|
data.high = high;
|
||||||
|
|
||||||
|
// 16轮迭代
|
||||||
|
if (type == DesType.Encode) {
|
||||||
|
for (let i = 0; i < 16; ++i) makeData(data, subKeys[i]);
|
||||||
|
}
|
||||||
|
if (type == DesType.Decode) {
|
||||||
|
for (let i = 15; i >= 0; --i) makeData(data, subKeys[i]);
|
||||||
|
}
|
||||||
|
const tmp = data.low;
|
||||||
|
data.low = data.high;
|
||||||
|
data.high = tmp;
|
||||||
|
|
||||||
|
low = 0;
|
||||||
|
high = 0;
|
||||||
|
for (let i = 0; i < 64; ++i) {
|
||||||
|
if (i >= 32) {
|
||||||
|
if (DES_FINAL[i] <= 32) mask_ok = uint32(DES_BITMAP[DES_FINAL[i] - 1] & data.low)
|
||||||
|
else mask_ok = uint32(data.high & DES_BITMAP[DES_FINAL[i] - 1])
|
||||||
|
if (mask_ok) high = uint32(high | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (DES_FINAL[i] <= 32) mask_ok = uint32(DES_BITMAP[DES_FINAL[i] - 1] & data.low)
|
||||||
|
else mask_ok = uint32(data.high & DES_BITMAP[DES_FINAL[i] - 1])
|
||||||
|
if (mask_ok) low = uint32(low | DES_BITMAP[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.low = low;
|
||||||
|
data.high = high;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行des加密/解密,内容存回src
|
||||||
|
* @param src 原始数据
|
||||||
|
* @param key 密码
|
||||||
|
* @param type 加密、解密
|
||||||
|
*/
|
||||||
|
export function des(src: Buffer, key: Buffer, type: DesType) {
|
||||||
|
makeFirstKey(key);
|
||||||
|
for (let i = 0; i < src.byteLength; i += 8) {
|
||||||
|
const data: IData64 = { low: src.readUint32LE(i), high: src.readUint32LE(i + 4) }
|
||||||
|
handleData(type, data);
|
||||||
|
src.writeUint32LE(data.low, i)
|
||||||
|
src.writeUint32LE(data.high, i + 4)
|
||||||
|
}
|
||||||
|
}
|
76
src/lib/request.ts
Normal file
76
src/lib/request.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import zlib from 'zlib'
|
||||||
|
import http from 'http'
|
||||||
|
import https from 'https'
|
||||||
|
|
||||||
|
|
||||||
|
interface IRequestBodyUtil {
|
||||||
|
json(data: any): Buffer
|
||||||
|
urlencoded(data: any): Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRequestOption {
|
||||||
|
url: string,
|
||||||
|
method?: string
|
||||||
|
body?: (util: IRequestBodyUtil) => Buffer
|
||||||
|
headers?: Record<string, string | number>
|
||||||
|
type?: 'json' | 'buffer' | 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function request<T = any>(option: IRequestOption) {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
|
||||||
|
const requestFunc = option.url.startsWith('https://') ? https.request : http.request
|
||||||
|
const method = (option.method ?? 'get').toLowerCase()
|
||||||
|
|
||||||
|
//处理body
|
||||||
|
let contentType: string | null = null
|
||||||
|
const body = option.body ? option.body({
|
||||||
|
json(data) {
|
||||||
|
contentType = 'application/json'
|
||||||
|
return Buffer.from(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
urlencoded(data) {
|
||||||
|
contentType = 'application/x-www-form-urlencoded'
|
||||||
|
return Buffer.from(Object.keys(data).map(k => `${k}=${encodeURIComponent(data[k])}`).join('&'))
|
||||||
|
},
|
||||||
|
}) : null
|
||||||
|
|
||||||
|
//发起请求
|
||||||
|
const req = requestFunc(option.url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
...option.headers,
|
||||||
|
...(['get', 'options'].includes(method) || !body) ? {} : { 'Content-Length': body.byteLength },
|
||||||
|
...contentType ? { 'Content-Type': contentType } : {},
|
||||||
|
},
|
||||||
|
}, res => {
|
||||||
|
const buffer: Buffer[] = []
|
||||||
|
res.on('data', d => buffer.push(d))
|
||||||
|
res.on('error', reject)
|
||||||
|
res.once('close', () => {
|
||||||
|
let data = Buffer.concat(buffer)
|
||||||
|
switch (res.headers['content-encoding']) {
|
||||||
|
case 'gzip':
|
||||||
|
data = zlib.gunzipSync(data)
|
||||||
|
break
|
||||||
|
case 'deflate':
|
||||||
|
data = zlib.inflateSync(data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch (option.type) {
|
||||||
|
case 'text':
|
||||||
|
resolve(data.toString() as any)
|
||||||
|
break
|
||||||
|
case 'json':
|
||||||
|
resolve(JSON.parse(data.toString()))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
resolve(data as any)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
req.on('error', reject)
|
||||||
|
req.end(body)
|
||||||
|
})
|
||||||
|
}
|
188
src/lib/xml.ts
Normal file
188
src/lib/xml.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
|
||||||
|
interface IXMLProp {
|
||||||
|
name: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IXMLTag {
|
||||||
|
name: string
|
||||||
|
props: IXMLProp[]
|
||||||
|
children?: string | IXMLTag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IXMLDocument {
|
||||||
|
props: IXMLProp[]
|
||||||
|
root: IXMLTag
|
||||||
|
}
|
||||||
|
|
||||||
|
/** XML错误 */
|
||||||
|
export class XMLError extends Error {
|
||||||
|
constructor(public readonly line: number, message: string) {
|
||||||
|
super(`line ${line + 1}: ${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseXML(data: string): IXMLDocument {
|
||||||
|
const WHITE_SPACES = [' ', '\r', '\n', '\t']
|
||||||
|
|
||||||
|
//当前位置
|
||||||
|
let cur = 0
|
||||||
|
let line = 0
|
||||||
|
let xmlProps: IXMLProp[] = []
|
||||||
|
|
||||||
|
//前进
|
||||||
|
function forward(n: number = 1) {
|
||||||
|
for (let i = 0; i < n; ++i) {
|
||||||
|
++cur
|
||||||
|
if (data[cur] == '\n') ++line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//跳过空白
|
||||||
|
function skipWhite() {
|
||||||
|
while (WHITE_SPACES.includes(data[cur])) forward()
|
||||||
|
}
|
||||||
|
|
||||||
|
//EOF检测
|
||||||
|
function checkEOF() {
|
||||||
|
if (!data[cur]) throw new XMLError(line, 'xml EOF')
|
||||||
|
}
|
||||||
|
|
||||||
|
//读取字符串
|
||||||
|
function readString() {
|
||||||
|
forward()
|
||||||
|
let beg = cur
|
||||||
|
while (true) {
|
||||||
|
checkEOF()
|
||||||
|
if (data[cur] === '"') {
|
||||||
|
const result = data.substring(beg, cur)
|
||||||
|
forward()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
forward()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//读取名称
|
||||||
|
function readName(endChars: string[]) {
|
||||||
|
let beg = cur
|
||||||
|
while (true) {
|
||||||
|
checkEOF()
|
||||||
|
if (endChars.includes(data[cur])) return data.substring(beg, cur).trim()
|
||||||
|
forward()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//读取标签属性
|
||||||
|
function parseProps() {
|
||||||
|
const props: IXMLProp[] = []
|
||||||
|
while (true) {
|
||||||
|
checkEOF()
|
||||||
|
if ([...WHITE_SPACES, '/', '>', '?'].includes(data[cur])) break //结束
|
||||||
|
|
||||||
|
//读取名称
|
||||||
|
skipWhite()
|
||||||
|
const name = readName(['=', ...WHITE_SPACES, '/', '>', '?'])
|
||||||
|
if (!name) throw new Error(`Expected TAG name`)
|
||||||
|
skipWhite()
|
||||||
|
|
||||||
|
//等号
|
||||||
|
if (data[cur] == '=') {
|
||||||
|
forward()
|
||||||
|
skipWhite()
|
||||||
|
const value = readString()
|
||||||
|
props.push({ name, value })
|
||||||
|
skipWhite()
|
||||||
|
}
|
||||||
|
//没有值
|
||||||
|
else props.push({ name })
|
||||||
|
}
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 <?xml
|
||||||
|
function parseXMLTag() {
|
||||||
|
forward(2)
|
||||||
|
if (data.substring(cur, cur + 4) != 'xml ') throw new XMLError(line, `xml tag must start whit "<?xml "`)
|
||||||
|
forward(4)
|
||||||
|
|
||||||
|
xmlProps = parseProps()
|
||||||
|
skipWhite()
|
||||||
|
|
||||||
|
if (data[cur] != '?' && data[cur + 1] != '>') throw new XMLError(line, 'xml tag must end with ?>')
|
||||||
|
forward(2)
|
||||||
|
skipWhite()
|
||||||
|
|
||||||
|
//接着需要是标签
|
||||||
|
if (data[cur] != '<') throw new Error('TAG expected')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTag(): IXMLTag {
|
||||||
|
forward()
|
||||||
|
//标签名称
|
||||||
|
const name = readName([...WHITE_SPACES, '/', '>'])
|
||||||
|
skipWhite()
|
||||||
|
|
||||||
|
//属性
|
||||||
|
const props = parseProps()
|
||||||
|
skipWhite()
|
||||||
|
|
||||||
|
let children: IXMLTag['children'] = undefined
|
||||||
|
|
||||||
|
//结束判断
|
||||||
|
if (data[cur] == '/') {
|
||||||
|
if (data[cur + 1] != '>') throw new XMLError(line, `"/>" expected`)
|
||||||
|
forward(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
//读取子内容
|
||||||
|
else if (data[cur] == '>') {
|
||||||
|
forward()
|
||||||
|
skipWhite()
|
||||||
|
|
||||||
|
//存在子标签
|
||||||
|
if (data[cur] === '<' && data[cur + 1] != '/') {
|
||||||
|
children = []
|
||||||
|
while (data[cur] === '<' && data[cur + 1] != '/') {
|
||||||
|
children.push(parseTag())
|
||||||
|
skipWhite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//直接就是文本
|
||||||
|
else children = parseContent()
|
||||||
|
|
||||||
|
//结束标签
|
||||||
|
if (data[cur] != '<' && data[cur] != '/') throw new XMLError(line, `"</" expected`)
|
||||||
|
forward(2)
|
||||||
|
skipWhite()
|
||||||
|
const end = readName(['>']).trim()
|
||||||
|
if (end != name) throw new XMLError(line, `tag ${name} not match to ${end}`)
|
||||||
|
skipWhite()
|
||||||
|
forward(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
//错误
|
||||||
|
else throw new Error(`">" or "/>" expected`)
|
||||||
|
|
||||||
|
skipWhite()
|
||||||
|
return { name, props, children }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContent(): string {
|
||||||
|
let beg = cur
|
||||||
|
while (true) {
|
||||||
|
checkEOF()
|
||||||
|
if (data[cur] == '<') break
|
||||||
|
forward()
|
||||||
|
}
|
||||||
|
return data.substring(beg, cur).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
//解析内容
|
||||||
|
skipWhite()
|
||||||
|
if (data[cur + 1] == '?') parseXMLTag()
|
||||||
|
return {
|
||||||
|
props: xmlProps,
|
||||||
|
root: parseTag(),
|
||||||
|
}
|
||||||
|
}
|
76
src/lyric/common.ts
Normal file
76
src/lyric/common.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { ILyric } from "./declare"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将给定的值处理为整数或undefined
|
||||||
|
* @param v 要处理的值
|
||||||
|
*/
|
||||||
|
function toInt(v: string | number) {
|
||||||
|
if (typeof v == 'string') v = parseInt(v)
|
||||||
|
if (typeof v != 'number') return undefined
|
||||||
|
if (isNaN(v)) return undefined
|
||||||
|
return parseInt(v as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析歌词标签
|
||||||
|
* @param lyric 歌词对象
|
||||||
|
* @param line 歌词行
|
||||||
|
*/
|
||||||
|
export function parseLyricTag(lyric: ILyric, line: string) {
|
||||||
|
const match = line.match(/^\[([a-zA-Z][a-zA-Z0-9_]*):(.*)\]$/)
|
||||||
|
if (!match) return false
|
||||||
|
switch (match[1]) {
|
||||||
|
case 'ti':
|
||||||
|
lyric.ti = match[2].trim()
|
||||||
|
break
|
||||||
|
case 'ar':
|
||||||
|
lyric.ar = match[2].trim()
|
||||||
|
break
|
||||||
|
case 'al':
|
||||||
|
lyric.al = match[2].trim()
|
||||||
|
break
|
||||||
|
case 'by':
|
||||||
|
lyric.by = match[2].trim()
|
||||||
|
break
|
||||||
|
case 'offset':
|
||||||
|
lyric.offset = toInt(match[2].trim())
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
lyric.ext ??= {}
|
||||||
|
lyric.ext[match[1]] = match[2].trim()
|
||||||
|
}
|
||||||
|
return !!match?.[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成标签 */
|
||||||
|
interface IGenLyricTagOption {
|
||||||
|
/** 忽略空的 */
|
||||||
|
skipEmpty?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
//生成歌词标签
|
||||||
|
export function genLyricTag(lyric: ILyric, option?: IGenLyricTagOption) {
|
||||||
|
const buffer: string[] = []
|
||||||
|
|
||||||
|
const { skipEmpty = true } = option ?? {}
|
||||||
|
|
||||||
|
//kuwo特别处理
|
||||||
|
if (lyric.ext?.kuwo) buffer.push(`[kuwo:${lyric.ext.kuwo}]`)
|
||||||
|
|
||||||
|
//常规标签
|
||||||
|
if (!skipEmpty || lyric.ti) buffer.push(`[ti:${lyric.ti ?? ''}]`)
|
||||||
|
if (!skipEmpty || lyric.ar) buffer.push(`[ar:${lyric.ar ?? ''}]`)
|
||||||
|
if (!skipEmpty || lyric.al) buffer.push(`[al:${lyric.al ?? ''}]`)
|
||||||
|
if (!skipEmpty || lyric.by) buffer.push(`[by:${lyric.by ?? ''}]`)
|
||||||
|
if (lyric.offset) buffer.push(`[offset:${lyric.offset ?? ''}]`)
|
||||||
|
|
||||||
|
//其他标签
|
||||||
|
if (lyric.ext) {
|
||||||
|
for (let key in lyric.ext) {
|
||||||
|
if (key === 'kuwo') continue
|
||||||
|
if (!skipEmpty || lyric.ext[key]) buffer.push(`[${key}:${lyric.ext[key] ?? ''}]`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
61
src/lyric/declare.ts
Normal file
61
src/lyric/declare.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/** 歌词类型 */
|
||||||
|
export enum LyricType {
|
||||||
|
/** 普通歌词 */
|
||||||
|
REG,
|
||||||
|
/** 卡拉OK歌词 */
|
||||||
|
KARA,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡拉OK歌词行信息 */
|
||||||
|
export interface IKaraokeWord {
|
||||||
|
/** 开始时间,相对于行时间 */
|
||||||
|
start: number
|
||||||
|
/** 持续时间 */
|
||||||
|
duration: number
|
||||||
|
/** 内容 */
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 歌词行 */
|
||||||
|
export interface ILyricLine<T> {
|
||||||
|
/** 开始时间 */
|
||||||
|
start: number
|
||||||
|
/** 持续时间,普通歌词无此字段 */
|
||||||
|
duration?: number
|
||||||
|
/** 文字内容 */
|
||||||
|
content: T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILyricCommon {
|
||||||
|
/** 标题 */
|
||||||
|
ti?: string
|
||||||
|
/** 艺术家 */
|
||||||
|
ar?: string
|
||||||
|
/** 专辑 */
|
||||||
|
al?: string
|
||||||
|
/** 制作人 */
|
||||||
|
by?: string
|
||||||
|
/** 偏移 */
|
||||||
|
offset?: number
|
||||||
|
/** 其他扩展的标记 */
|
||||||
|
ext?: { [K in string]: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 普通歌词信息 */
|
||||||
|
export interface IRegularLyric extends ILyricCommon {
|
||||||
|
/** 歌词类型 */
|
||||||
|
type: LyricType.REG
|
||||||
|
/** 歌词内容 */
|
||||||
|
content: ILyricLine<string>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡拉歌词信息 */
|
||||||
|
export interface IKaraOKLyric extends ILyricCommon {
|
||||||
|
/** 歌词类型 */
|
||||||
|
type: LyricType.KARA
|
||||||
|
/** 歌词内容 */
|
||||||
|
content: ILyricLine<IKaraokeWord[]>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 歌词信息 */
|
||||||
|
export type ILyric = (IRegularLyric | IKaraOKLyric)
|
115
src/lyric/krc.ts
Normal file
115
src/lyric/krc.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
import { unescape } from 'yizhi-html-escape'
|
||||||
|
import zlib from 'zlib'
|
||||||
|
import { genLyricTag, parseLyricTag } from './common'
|
||||||
|
import { IKaraokeWord, ILyric, ILyricLine, LyricType } from './declare'
|
||||||
|
|
||||||
|
const KUGOU_KEY = Buffer.from([64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105])
|
||||||
|
const KRC_TAG = Buffer.from([0x6b, 0x72, 0x63, 0x31])
|
||||||
|
|
||||||
|
//加密、解密
|
||||||
|
function kugouConvert(buffer: Buffer) {
|
||||||
|
for (let i = 0; i < buffer.byteLength; ++i) {
|
||||||
|
buffer[i] ^= KUGOU_KEY[i % KUGOU_KEY.length]
|
||||||
|
}
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 酷狗歌词解密,并返回krc文本
|
||||||
|
* @param buffer 歌词内容
|
||||||
|
*/
|
||||||
|
export function decrypt(buffer: Buffer) {
|
||||||
|
//检测开头4个字节
|
||||||
|
if (buffer.compare(KRC_TAG, 0, KRC_TAG.length, 0, KRC_TAG.length)) return null
|
||||||
|
try {
|
||||||
|
//解密
|
||||||
|
buffer = kugouConvert(buffer.subarray(4))
|
||||||
|
//解压
|
||||||
|
return zlib.inflateSync(buffer).toString()
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密krc歌词
|
||||||
|
* @param krcText krc歌词文本
|
||||||
|
*/
|
||||||
|
export function encrypt(krcText: string) {
|
||||||
|
//压缩
|
||||||
|
let buffer = zlib.deflateSync(Buffer.from(krcText))
|
||||||
|
//加密
|
||||||
|
buffer = kugouConvert(buffer)
|
||||||
|
//加上标记
|
||||||
|
return Buffer.concat([KRC_TAG, buffer])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析KRC歌词
|
||||||
|
* @param krcText KRC歌词文本内容
|
||||||
|
*/
|
||||||
|
export function parse(krcText: string) {
|
||||||
|
//结果
|
||||||
|
const result: ILyric = { type: LyricType.KARA, content: [] }
|
||||||
|
|
||||||
|
//逐行处理
|
||||||
|
krcText.split(/\r?\n/).forEach(line => {
|
||||||
|
line = line.trim()
|
||||||
|
//歌词行
|
||||||
|
const match = line.match(/^\[\s*(\d+)\s*,\s*(\d+)\s*\](.*)$/)
|
||||||
|
if (match) {
|
||||||
|
//目标信息
|
||||||
|
const lrcLine: ILyricLine<IKaraokeWord[]> = {
|
||||||
|
start: parseInt(match[1]),
|
||||||
|
duration: parseInt(match[2]),
|
||||||
|
content: [],
|
||||||
|
}
|
||||||
|
//一直往后所搜词语
|
||||||
|
let rem = match[3]
|
||||||
|
while (rem.length) {
|
||||||
|
const match = rem.match(/^<(\d+),(\d+),(\d+)>([^<]+)/)
|
||||||
|
if (match) {
|
||||||
|
const word: IKaraokeWord = {
|
||||||
|
start: parseInt(match[1]),
|
||||||
|
duration: parseInt(match[2]),
|
||||||
|
content: unescape(match[4]),
|
||||||
|
}
|
||||||
|
if (word.content) lrcLine.content.push(word)
|
||||||
|
rem = rem.substring(match[0].length)
|
||||||
|
}
|
||||||
|
else break
|
||||||
|
}
|
||||||
|
//保存行
|
||||||
|
if (lrcLine.content.length) result.content.push(lrcLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
//标签
|
||||||
|
else parseLyricTag(result, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将歌词转换为KRC文本
|
||||||
|
* @param lyric 歌词(只能是卡拉OK歌词)
|
||||||
|
*/
|
||||||
|
export function stringify(lyric: ILyric) {
|
||||||
|
if (lyric.type != LyricType.KARA) throw new Error(`lrc cannot stringify to krc`)
|
||||||
|
|
||||||
|
const buffer: string[] = [...genLyricTag(lyric)]
|
||||||
|
|
||||||
|
lyric.content.forEach(line => {
|
||||||
|
if (typeof line.content === 'string') return
|
||||||
|
const dur = line.duration ?? (() => {
|
||||||
|
const last = line.content[line.content.length - 1]
|
||||||
|
if (!last) return 0
|
||||||
|
return last.start + line.start + last.duration
|
||||||
|
})()
|
||||||
|
const text = line.content.map(word => `<${word.start},${word.duration},0>${word.content}`).join('')
|
||||||
|
buffer.push(`[${line.start},${dur}]${text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return buffer.join('\n')
|
||||||
|
}
|
74
src/lyric/lrc.ts
Normal file
74
src/lyric/lrc.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { parseLyricTag } from "./common"
|
||||||
|
import { ILyric, LyricType } from "./declare"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析LRC歌词
|
||||||
|
* @param lyricText 歌词文本
|
||||||
|
*/
|
||||||
|
export function parse(lyricText: string) {
|
||||||
|
//结果
|
||||||
|
const result: ILyric = { type: LyricType.REG, content: [] }
|
||||||
|
|
||||||
|
//逐行处理
|
||||||
|
lyricText.split(/\r?\n/).forEach(line => {
|
||||||
|
line = line.trim()
|
||||||
|
|
||||||
|
//匹配歌词时间
|
||||||
|
const startTimes: number[] = []
|
||||||
|
while (true) {
|
||||||
|
//匹配歌词时间
|
||||||
|
const match = line.match(/^\[(\d+):(\d+)(\.(\d+))?\]/)
|
||||||
|
if (match) {
|
||||||
|
//取得时间
|
||||||
|
const min = parseInt(match[1].replace(/^0+/, ''))
|
||||||
|
const sec = parseInt(match[2].replace(/^0+/, ''))
|
||||||
|
let sec100 = match[4] ? parseInt(match[4].replace(/^0+/, '')) : 0
|
||||||
|
if (isNaN(min) || isNaN(sec)) return
|
||||||
|
if (isNaN(sec100)) sec100 = 0
|
||||||
|
const startTime = (min * 60 * 1000) + (sec * 1000) + (sec100 * 10);
|
||||||
|
//保存时间继续处理
|
||||||
|
if (!startTimes.includes(startTime)) startTimes.push(startTime)
|
||||||
|
line = line.substring(match[0].length).trim()
|
||||||
|
}
|
||||||
|
//没有匹配到
|
||||||
|
else break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line) return
|
||||||
|
//如果匹配到时间
|
||||||
|
if (startTimes.length) result.content.push(...startTimes.map(start => ({ start, content: line })))
|
||||||
|
//没有匹配到歌词
|
||||||
|
else parseLyricTag(result, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
//排序一下吧
|
||||||
|
result.content = result.content.sort((a, b) => a.start - b.start)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌词转换为文本
|
||||||
|
* @param lyric 歌词
|
||||||
|
*/
|
||||||
|
export function stringify(lyric: ILyric) {
|
||||||
|
|
||||||
|
const buffer: string[] = []
|
||||||
|
|
||||||
|
if (lyric.ti) buffer.push(`[ti:${lyric.ti}]`)
|
||||||
|
if (lyric.ar) buffer.push(`[ar:${lyric.ar}]`)
|
||||||
|
if (lyric.al) buffer.push(`[al:${lyric.al}]`)
|
||||||
|
if (lyric.by) buffer.push(`[by:${lyric.by}]`)
|
||||||
|
if (lyric.offset) buffer.push(`[offset:${lyric.offset}]`)
|
||||||
|
|
||||||
|
lyric.content.forEach(line => {
|
||||||
|
const min = parseInt(line.start / 1000 / 60 as any).toString().padStart(2, '0')
|
||||||
|
const sec = parseInt((line.start / 1000) % 60 as any).toString().padStart(2, '0')
|
||||||
|
const sec100 = parseInt((line.start % 1000) / 10 as any).toString().padStart(2, '0')
|
||||||
|
//文本处理
|
||||||
|
const text = (typeof line.content == 'string') ? line.content : line.content.map(s => s.content).join('')
|
||||||
|
buffer.push(`[${min}:${sec}.${sec100}]${text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return buffer.join('\n')
|
||||||
|
}
|
161
src/lyric/lrcx.ts
Normal file
161
src/lyric/lrcx.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import zlib from 'zlib'
|
||||||
|
import iconv from 'iconv-lite'
|
||||||
|
import { IKaraokeWord, ILyric, ILyricLine, LyricType } from './declare'
|
||||||
|
import { genLyricTag, parseLyricTag } from './common'
|
||||||
|
|
||||||
|
|
||||||
|
const KUWO_LYRIC_KEY = Buffer.from('yeelion')
|
||||||
|
|
||||||
|
//加密、解密
|
||||||
|
function cryptLRCX(buffer: Buffer) {
|
||||||
|
for (let i = 0; i < buffer.length; ++i) {
|
||||||
|
buffer[i] ^= KUWO_LYRIC_KEY[i % KUWO_LYRIC_KEY.byteLength]
|
||||||
|
}
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密LRCX歌词
|
||||||
|
* @param buffer 歌词内容
|
||||||
|
* @returns 解密后的歌词,解密失败返回null
|
||||||
|
*/
|
||||||
|
export function decrypt(buffer: Buffer) {
|
||||||
|
try {
|
||||||
|
//解压
|
||||||
|
const b64Str = zlib.inflateSync(buffer).toString()
|
||||||
|
//取得内容
|
||||||
|
const content = Buffer.from(b64Str, 'base64')
|
||||||
|
//解密
|
||||||
|
cryptLRCX(content)
|
||||||
|
//完成
|
||||||
|
return iconv.decode(content, 'gb18030')
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码歌词
|
||||||
|
* @param content 歌词的文本内容
|
||||||
|
*/
|
||||||
|
export function encrypt(content: string | Buffer) {
|
||||||
|
//编码处理
|
||||||
|
const codeData = iconv.encode(content.toString(), 'gb18030')
|
||||||
|
//加密
|
||||||
|
cryptLRCX(codeData)
|
||||||
|
//转为base64
|
||||||
|
const b64Str = codeData.toString('base64')
|
||||||
|
//压缩
|
||||||
|
return zlib.deflateSync(b64Str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析LRCX歌词
|
||||||
|
* @param lrcxText LRCX歌词文本内容
|
||||||
|
*/
|
||||||
|
export function parse(lrcxText: string) {
|
||||||
|
//结果
|
||||||
|
const result: ILyric = { type: LyricType.KARA, content: [] }
|
||||||
|
|
||||||
|
//逐行处理
|
||||||
|
lrcxText.split(/\r?\n/).forEach(line => {
|
||||||
|
line = line.trim()
|
||||||
|
const match = line.match(/^\[(\d+):(\d+).(\d+)\]/)
|
||||||
|
|
||||||
|
//需要读取到[kuwo:xxx]后才能解析
|
||||||
|
if (match && result.ext?.kuwo) {
|
||||||
|
const m = parseInt(match[1])
|
||||||
|
const s = parseInt(match[2])
|
||||||
|
const ms = parseInt(match[3])
|
||||||
|
line = line.substring(match[0].length)
|
||||||
|
|
||||||
|
//得到kuwo标志中的8进制值
|
||||||
|
const kuwo = parseInt(result.ext.kuwo, 8)
|
||||||
|
const k1 = parseInt(kuwo / 10 as any)
|
||||||
|
const k2 = parseInt(kuwo % 10 as any)
|
||||||
|
|
||||||
|
//解析歌词文字
|
||||||
|
const lyricLine: ILyricLine<IKaraokeWord[]> = {
|
||||||
|
start: (m * 60 * 1000) + (s * 1000) + ms,
|
||||||
|
duration: 0,
|
||||||
|
content: []
|
||||||
|
}
|
||||||
|
while (line.length) {
|
||||||
|
const match = line.match(/<(\d+),([-\d]+)>([^<]+)/)
|
||||||
|
if (!match) break
|
||||||
|
const v1 = parseInt(match[1])
|
||||||
|
const v2 = parseInt(match[2])
|
||||||
|
line = line.substring(match[0].length)
|
||||||
|
|
||||||
|
const start = (v1 + v2) / (k1 * 2)
|
||||||
|
const dur = (v1 - v2) / (k2 * 2)
|
||||||
|
lyricLine.content.push({ start, duration: dur, content: match[3] })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lyricLine.content.length) {
|
||||||
|
//计算一下行持续时间
|
||||||
|
const last = lyricLine.content[lyricLine.content.length - 1]
|
||||||
|
lyricLine.duration = last.start + last.duration
|
||||||
|
result.content.push(lyricLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else parseLyricTag(result, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将LRCX歌词转换为文本
|
||||||
|
*
|
||||||
|
* _注意:如果没有设置lyric.ext.kuwo,系统会自动生成一个_
|
||||||
|
*
|
||||||
|
* @param lyric 歌词(只能是卡拉OK歌词)
|
||||||
|
*/
|
||||||
|
export function stringify(lyric: ILyric) {
|
||||||
|
if (lyric.type != LyricType.KARA) throw new Error(`lrc cannot stringify to lrcx`)
|
||||||
|
|
||||||
|
//处理一下歌词信息
|
||||||
|
lyric = {
|
||||||
|
...lyric,
|
||||||
|
ext: {
|
||||||
|
...lyric.ext ?? {},
|
||||||
|
kuwo: lyric.ext?.kuwo ?? (() => {
|
||||||
|
while (true) {
|
||||||
|
const k = parseInt(Math.random() * (0o77 - 10) as any) + 10
|
||||||
|
if (k % 10 != 0) return '0' + k.toString(8)
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: string[] = [...genLyricTag(lyric, { skipEmpty: false })]
|
||||||
|
|
||||||
|
//取得kuwo标记
|
||||||
|
const kuwo = parseInt(lyric.ext?.kuwo!, 8)
|
||||||
|
const k1 = parseInt(kuwo / 10 as any)
|
||||||
|
const k2 = parseInt(kuwo % 10 as any)
|
||||||
|
|
||||||
|
lyric.content.forEach(line => {
|
||||||
|
if (typeof line.content === 'string') return
|
||||||
|
const m = parseInt(line.start / 1000 / 60 as any).toString().padStart(2, '0')
|
||||||
|
const s = parseInt(line.start / 1000 % 60 as any).toString().padStart(2, '0')
|
||||||
|
const ms = parseInt(line.start % 1000 as any).toString().padStart(3, '0')
|
||||||
|
|
||||||
|
const text = line.content.map(word => {
|
||||||
|
const v1 = word.start * (k1 * 2)
|
||||||
|
const v2 = word.duration * (k2 * 2)
|
||||||
|
|
||||||
|
//求解方程:
|
||||||
|
// a + b = v1
|
||||||
|
// a - b = v2
|
||||||
|
const a = parseInt((v1 + v2) / 2 as any)
|
||||||
|
const b = parseInt((v1 - v2) / 2 as any)
|
||||||
|
//完成
|
||||||
|
return `<${a},${b}>${word.content}`
|
||||||
|
}).join('')
|
||||||
|
buffer.push(`[${m}:${s}.${ms}]${text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return buffer.join('\n')
|
||||||
|
}
|
65
src/lyric/nrc.ts
Normal file
65
src/lyric/nrc.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { IKaraokeWord, ILyric, ILyricLine, LyricType } from './declare'
|
||||||
|
import { genLyricTag, parseLyricTag } from './common'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析nrc歌词
|
||||||
|
* @param nrcxText nrc歌词文本内容
|
||||||
|
*/
|
||||||
|
export function parse(nrcxText: string) {
|
||||||
|
//结果
|
||||||
|
const result: ILyric = { type: LyricType.KARA, content: [] }
|
||||||
|
|
||||||
|
//逐行处理
|
||||||
|
nrcxText.split(/\r?\n/).forEach(line => {
|
||||||
|
line = line.trim()
|
||||||
|
const match = line.match(/^\[(\d+),(\d+)\]/)
|
||||||
|
if (match) {
|
||||||
|
const lyricLine: ILyricLine<IKaraokeWord[]> = {
|
||||||
|
start: parseInt(match[1]),
|
||||||
|
duration: parseInt(match[2]),
|
||||||
|
content: [],
|
||||||
|
}
|
||||||
|
line = line.substring(match[0].length)
|
||||||
|
while (line.length) {
|
||||||
|
const match = line.match(/^\((\d+),(\d+)(,(\d+))?\)([^\(]+)/)
|
||||||
|
if (!match) break
|
||||||
|
const start = parseInt(match[1])
|
||||||
|
const dur = parseInt(match[2])
|
||||||
|
const txt = match[5]
|
||||||
|
lyricLine.content.push({
|
||||||
|
start: start - lyricLine.start,
|
||||||
|
duration: dur,
|
||||||
|
content: txt,
|
||||||
|
})
|
||||||
|
line = line.substring(match[0].length)
|
||||||
|
}
|
||||||
|
if (lyricLine.content.length) result.content.push(lyricLine)
|
||||||
|
}
|
||||||
|
else parseLyricTag(result, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将nrc歌词转换为文本
|
||||||
|
*
|
||||||
|
* @param lyric 歌词(只能是卡拉OK歌词)
|
||||||
|
*/
|
||||||
|
export function stringify(lyric: ILyric) {
|
||||||
|
if (lyric.type != LyricType.KARA) throw new Error(`lrc cannot stringify to nrc`)
|
||||||
|
|
||||||
|
const buffer: string[] = [...genLyricTag(lyric)]
|
||||||
|
|
||||||
|
|
||||||
|
lyric.content.forEach(line => {
|
||||||
|
if (typeof line.content === 'string') return
|
||||||
|
|
||||||
|
const text = line.content.map(word => {
|
||||||
|
return `(${word.start + line.start},${word.duration},${0})${word.content}`
|
||||||
|
}).join('')
|
||||||
|
buffer.push(`[${line.start},${line.duration}]${text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return buffer.join('\n')
|
||||||
|
}
|
230
src/lyric/qrc.ts
Normal file
230
src/lyric/qrc.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import zlib from 'zlib'
|
||||||
|
import { des, DesType } from '../lib/des'
|
||||||
|
import { parseXML, XMLError } from '../lib/xml';
|
||||||
|
import { IKaraokeWord, ILyric, ILyricLine, LyricType } from './declare';
|
||||||
|
import { escape, unescape } from 'yizhi-html-escape'
|
||||||
|
import { genLyricTag, parseLyricTag } from './common';
|
||||||
|
|
||||||
|
//歌词密码
|
||||||
|
const QRC_KEY1 = Buffer.from("!@#)(NHLiuy*$%^&");
|
||||||
|
const QRC_KEY2 = Buffer.from("123ZXC!@#)(*$%^&");
|
||||||
|
const QRC_KEY3 = Buffer.from("!@#)(*$%^&abcDEF");
|
||||||
|
|
||||||
|
/** 新旧版转换密码 */
|
||||||
|
const VERSION_CONVERT_CODE = [
|
||||||
|
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, 0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0,
|
||||||
|
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, 0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6,
|
||||||
|
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, 0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0,
|
||||||
|
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, 0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7,
|
||||||
|
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, 0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43,
|
||||||
|
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, 0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95,
|
||||||
|
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, 0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC,
|
||||||
|
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, 0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66,
|
||||||
|
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, 0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37,
|
||||||
|
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, 0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74,
|
||||||
|
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, 0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95,
|
||||||
|
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, 0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F,
|
||||||
|
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, 0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16,
|
||||||
|
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, 0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09,
|
||||||
|
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, 0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76,
|
||||||
|
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, 0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11,
|
||||||
|
]
|
||||||
|
|
||||||
|
/*** 歌词标识 */
|
||||||
|
const OFFSET_TAG = Buffer.from('[offset:')
|
||||||
|
|
||||||
|
//新旧版本转换
|
||||||
|
function convertNewQRC(content: Buffer) {
|
||||||
|
const result = Buffer.alloc(content.byteLength)
|
||||||
|
for (let i = 0; i < content.byteLength; ++i) {
|
||||||
|
result[i] = content[i] ^ VERSION_CONVERT_CODE[(i * i + 0x013c1b) % 256]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
//填充Buffer为8的倍数
|
||||||
|
function padBuffer(buffer: Buffer, offset: number) {
|
||||||
|
let len = buffer.byteLength - offset
|
||||||
|
const mod = len % 8
|
||||||
|
if (mod) len += 8 - mod
|
||||||
|
const result = Buffer.alloc(len, 0)
|
||||||
|
buffer.copy(result, 0, offset, buffer.length)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密QRC歌词,得到歌词XML文本
|
||||||
|
* @param buffer 歌词内容,可以从.qrc文件读取,也可以通过QQ音乐API下载
|
||||||
|
* @returns 解密后的歌词,解密失败返回null
|
||||||
|
*/
|
||||||
|
export function decrypt(buffer: Buffer) {
|
||||||
|
try {
|
||||||
|
//如果是新版的,转换为旧版
|
||||||
|
if (buffer[0] == 0x98 && buffer[1] == 0x25) buffer = convertNewQRC(buffer)
|
||||||
|
|
||||||
|
//检测是否有"[offset:", 如果有则为歌词
|
||||||
|
if (!buffer.compare(OFFSET_TAG, 0, OFFSET_TAG.length, 0, OFFSET_TAG.length)) {
|
||||||
|
//填补剩余空间
|
||||||
|
const content = padBuffer(buffer, 0x0b)
|
||||||
|
|
||||||
|
//解密
|
||||||
|
des(content, QRC_KEY1, DesType.Decode)
|
||||||
|
des(content, QRC_KEY2, DesType.Encode)
|
||||||
|
des(content, QRC_KEY3, DesType.Decode)
|
||||||
|
|
||||||
|
//解压,得到歌词
|
||||||
|
const result = zlib.inflateSync(content)
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
//尝试解码网络获取的歌词
|
||||||
|
const result = padBuffer(buffer, 0)
|
||||||
|
des(result, QRC_KEY1, DesType.Decode)
|
||||||
|
des(result, QRC_KEY2, DesType.Encode)
|
||||||
|
des(result, QRC_KEY3, DesType.Decode)
|
||||||
|
return zlib.unzipSync(result).toString()
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** QRC歌词编码类型 */
|
||||||
|
export enum EncryptType {
|
||||||
|
/** 新版歌词 */
|
||||||
|
QRC,
|
||||||
|
/** 旧版歌词 */
|
||||||
|
OLD_QRC,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码歌词
|
||||||
|
* @param content 歌词的XML内容
|
||||||
|
* @param type 歌词类型,默认为新版的歌词
|
||||||
|
*/
|
||||||
|
export function encrypt(content: string | Buffer, type = EncryptType.QRC) {
|
||||||
|
//压缩
|
||||||
|
let result = zlib.deflateSync(content)
|
||||||
|
|
||||||
|
//加密
|
||||||
|
result = padBuffer(result, 0)
|
||||||
|
des(result, QRC_KEY3, DesType.Encode)
|
||||||
|
des(result, QRC_KEY2, DesType.Decode)
|
||||||
|
des(result, QRC_KEY1, DesType.Encode)
|
||||||
|
|
||||||
|
//加上标签
|
||||||
|
result = Buffer.concat([
|
||||||
|
Buffer.from('[offset:0]\n'),
|
||||||
|
result,
|
||||||
|
])
|
||||||
|
|
||||||
|
//转换为新版的歌词
|
||||||
|
if (type === EncryptType.QRC) result = convertNewQRC(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析QRC歌词
|
||||||
|
* @param qrcText QRC歌词文本内容(解码后的XML格式)
|
||||||
|
*/
|
||||||
|
export function parse(qrcText: string) {
|
||||||
|
//解析XML数据,得到歌词数据
|
||||||
|
const lyric = (() => {
|
||||||
|
try {
|
||||||
|
const res = parseXML(qrcText).root
|
||||||
|
//QrcInfos标签
|
||||||
|
if (res.name.toLowerCase() != 'QrcInfos'.toLowerCase()) throw new Error('match QrcInfos TAG failed')
|
||||||
|
if (!(res.children instanceof Array)) throw new Error('match QrcInfos TAG failed')
|
||||||
|
|
||||||
|
//LyricInfo标签
|
||||||
|
const lyricInfoTag = res.children.find(e => e.name.toLowerCase() === 'LyricInfo'.toLowerCase())
|
||||||
|
if (!lyricInfoTag) throw new Error(`match LyricInfo TAG failed`)
|
||||||
|
if (!(lyricInfoTag.children instanceof Array)) throw new Error(`match LyricInfo TAG failed`)
|
||||||
|
|
||||||
|
//歌词列表
|
||||||
|
const lyricTags = lyricInfoTag.children.filter(t => /^Lyric_\d+/.test(t.name))
|
||||||
|
return lyricTags.map(t => {
|
||||||
|
const lyric = { type: 1, content: '' }
|
||||||
|
t.props.forEach(p => {
|
||||||
|
if (p.name.toLowerCase() == 'LyricContent'.toLowerCase()) lyric.content = p.value ?? ''
|
||||||
|
else if (p.name.toLowerCase() == 'LyricType') lyric.type = parseInt(p.value ?? '1')
|
||||||
|
})
|
||||||
|
return lyric
|
||||||
|
}).filter(l => l.content)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof XMLError) throw new Error(`parse QRC XML data failed`)
|
||||||
|
else throw err
|
||||||
|
}
|
||||||
|
})()[0]
|
||||||
|
if (!lyric) throw new Error(`QRC file not contains any lyric data`)
|
||||||
|
|
||||||
|
//结果
|
||||||
|
const result: ILyric = { type: LyricType.KARA, content: [] }
|
||||||
|
|
||||||
|
//逐行处理
|
||||||
|
lyric.content.split(/\r?\n/).forEach(line => {
|
||||||
|
line = line.trim()
|
||||||
|
//歌词行
|
||||||
|
const match = line.match(/^\[\s*(\d+)\s*,\s*(\d+)\s*\](.*)$/)
|
||||||
|
if (match) {
|
||||||
|
//目标信息
|
||||||
|
const lrcLine: ILyricLine<IKaraokeWord[]> = {
|
||||||
|
start: parseInt(match[1]),
|
||||||
|
duration: parseInt(match[2]),
|
||||||
|
content: [],
|
||||||
|
}
|
||||||
|
//一直往后所搜词语
|
||||||
|
let rem = match[3]
|
||||||
|
while (rem.length) {
|
||||||
|
const match = rem.match(/\(\s*(\d+)\s*,\s*(\d+)\s*\)/)
|
||||||
|
if (match) {
|
||||||
|
const word: IKaraokeWord = {
|
||||||
|
start: parseInt(match[1]) - lrcLine.start,
|
||||||
|
duration: parseInt(match[2]),
|
||||||
|
content: unescape(rem.substring(0, match.index!)),
|
||||||
|
}
|
||||||
|
if (word.content) lrcLine.content.push(word)
|
||||||
|
rem = rem.substring(match.index! + match[0].length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//保存行
|
||||||
|
if (lrcLine.content.length) result.content.push(lrcLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
//标签
|
||||||
|
else parseLyricTag(result, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将歌词转换为XML文本
|
||||||
|
* @param lyric 歌词(只能是卡拉OK歌词)
|
||||||
|
*/
|
||||||
|
export function stringify(lyric: ILyric) {
|
||||||
|
if (lyric.type != LyricType.KARA) throw new Error(`lrc cannot stringify to qrc`)
|
||||||
|
|
||||||
|
const buffer: string[] = [...genLyricTag(lyric)]
|
||||||
|
|
||||||
|
lyric.content.forEach(line => {
|
||||||
|
if (typeof line.content === 'string') return
|
||||||
|
const dur = line.duration ?? (() => {
|
||||||
|
const last = line.content[line.content.length - 1]
|
||||||
|
if (!last) return 0
|
||||||
|
return last.start + line.start + last.duration
|
||||||
|
})()
|
||||||
|
const text = line.content.map(word => `${word.content}(${word.start + line.start},${word.duration})`).join('')
|
||||||
|
buffer.push(`[${line.start},${dur}]${text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
`<?xml version="1.0" encoding="utf-8"?>`,
|
||||||
|
`<QrcInfos>`,
|
||||||
|
` <QrcHeadInfo SaveTime="${parseInt(Date.now() / 1000 as any)}" Version="100" />`,
|
||||||
|
` <LyricInfo LyricCount="1">`,
|
||||||
|
` <Lyric_1 LyricType="1" LyricContent="${escape(buffer.join('\n'))}\n" />`,
|
||||||
|
` </LyricInfo>`,
|
||||||
|
`</QrcInfos>`,
|
||||||
|
].join('\n')
|
||||||
|
}
|
259
src/net/download.ts
Normal file
259
src/net/download.ts
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import zlib from 'zlib'
|
||||||
|
import http from 'http'
|
||||||
|
import https from 'https'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import * as qrcUtil from '../lyric/qrc'
|
||||||
|
import * as krcUtil from '../lyric/krc'
|
||||||
|
import * as lrcxUtil from '../lyric/lrcx'
|
||||||
|
import { request } from '../lib/request'
|
||||||
|
|
||||||
|
|
||||||
|
/** QQ音乐歌词下载选项 */
|
||||||
|
interface IQQMusicLyricDownloadOption {
|
||||||
|
/** 歌曲ID */
|
||||||
|
songID: string | number
|
||||||
|
/** 指定专辑名称 */
|
||||||
|
albumName?: string | string[]
|
||||||
|
/** 指定歌曲名称 */
|
||||||
|
songName?: string
|
||||||
|
/** 指定歌手名称 */
|
||||||
|
singerName?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** QQ音乐歌词下载结果 */
|
||||||
|
interface IQQMusicDownloadResult {
|
||||||
|
/** 歌词内容 */
|
||||||
|
lyric: string
|
||||||
|
/** 翻译内容 */
|
||||||
|
trans: string | null
|
||||||
|
/** 音译内容 */
|
||||||
|
roma: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从QQ音乐API下载歌词
|
||||||
|
* @param option 下载选项
|
||||||
|
*/
|
||||||
|
export async function downloadQQMusicLyric(option: IQQMusicLyricDownloadOption) {
|
||||||
|
//创建一个参数
|
||||||
|
const createParam = (paranName: string, value: string | undefined | string[]) => {
|
||||||
|
if (value && typeof value === 'string') return { [paranName]: Buffer.from(value).toString('base64') }
|
||||||
|
else if (value instanceof Array) return { [paranName]: Buffer.from(value.filter(s => !!s).join(' ')).toString('base64') }
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
//请求歌词
|
||||||
|
const resp = await request({
|
||||||
|
url: `https://u.y.qq.com/cgi-bin/musicu.fcg?pcachetime=1675229492`,
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Accept-Language': 'zh-CN',
|
||||||
|
'Host': 'u.y.qq.com',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Connection': 'Keep-Alive',
|
||||||
|
},
|
||||||
|
body: utils => {
|
||||||
|
return utils.json({
|
||||||
|
"music.musichallSong.PlayLyricInfo.GetPlayLyricInfo": {
|
||||||
|
"method": "GetPlayLyricInfo",
|
||||||
|
"module": "music.musichallSong.PlayLyricInfo",
|
||||||
|
"param": {
|
||||||
|
...createParam('albumName', option.albumName),
|
||||||
|
"crypt": 0,
|
||||||
|
// "ct": 19,
|
||||||
|
// "cv": 1906,
|
||||||
|
// "interval": 304,
|
||||||
|
// "lrc_t": 0,
|
||||||
|
qrc: 1,
|
||||||
|
// "qrc": (option.qrc ?? true) ? 1 : 0,
|
||||||
|
// "qrc_t": 0,
|
||||||
|
// "roma": option.roma ? 1 : 0,
|
||||||
|
// "roma_t": 0,
|
||||||
|
...createParam('singerName', option.singerName),
|
||||||
|
"songID": parseInt(option.songID as any),
|
||||||
|
...createParam('songName', option.songName),
|
||||||
|
// "trans": option.trans ? 0 : 1,
|
||||||
|
// "trans_t": 0,
|
||||||
|
// "type": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
type: 'json',
|
||||||
|
})
|
||||||
|
|
||||||
|
const lyricInfo = resp?.['music.musichallSong.PlayLyricInfo.GetPlayLyricInfo']?.data
|
||||||
|
|
||||||
|
|
||||||
|
const parseLyric = (str: string, type: 'hex' | 'base64') => {
|
||||||
|
if (!str) return null
|
||||||
|
if (type === 'hex') return qrcUtil.decrypt(Buffer.from(str, 'hex'))?.toString() ?? null
|
||||||
|
else return Buffer.from(str, 'base64').toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lyricInfo?.qrc) return parseLyric(lyricInfo.lyric, 'hex')
|
||||||
|
return null
|
||||||
|
|
||||||
|
// let result: IQQMusicDownloadResult | null = null
|
||||||
|
// //内容为qrc
|
||||||
|
// if (lyricInfo?.qrc === 1) result = {
|
||||||
|
// lyric: parseLyric(lyricInfo.lyric, 'hex')!,
|
||||||
|
// roma: parseLyric(lyricInfo.roma, 'hex'),
|
||||||
|
// trans: parseLyric(lyricInfo.trans, 'hex'),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// //内容为lrc
|
||||||
|
// else if (lyricInfo?.qrc === 0) result = {
|
||||||
|
// lyric: parseLyric(lyricInfo.lyric, lyricInfo.crypt ? 'hex' : 'base64')!,
|
||||||
|
// //这两个都是qrc歌词
|
||||||
|
// roma: parseLyric(lyricInfo.roma, 'hex'),
|
||||||
|
// trans: parseLyric(lyricInfo.trans, 'hex'),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return result?.lyric ? result : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 酷狗音乐歌词下载选项 */
|
||||||
|
interface IKugouLyricDownloadOption {
|
||||||
|
/** 歌词ID */
|
||||||
|
id: string | number
|
||||||
|
/** Access Key */
|
||||||
|
accesskey: string
|
||||||
|
/** 下载格式,默认"krc" */
|
||||||
|
fmt?: 'krc' | 'lrc'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从酷狗音乐下载歌词
|
||||||
|
*
|
||||||
|
* __`id`和`accessKey`可以通过歌词搜索api获得__
|
||||||
|
*
|
||||||
|
* @param option 下载选项
|
||||||
|
* @returns krc歌词文本内容
|
||||||
|
*/
|
||||||
|
export async function downloadKugouLyric(option: IKugouLyricDownloadOption) {
|
||||||
|
//请求数据
|
||||||
|
const resp = await request<any>({
|
||||||
|
url: `http://lyrics.kugou.com/download?ver=1&client=pc&id=${option.id}&accesskey=${option.accesskey}&fmt=${option.fmt ?? 'krc'}&charset=utf8`,
|
||||||
|
method: 'get',
|
||||||
|
type: 'json',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resp.status !== 200 || !resp.content) return null
|
||||||
|
|
||||||
|
return krcUtil.decrypt(Buffer.from(resp.content, 'base64'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 酷我音乐歌词下载选项 */
|
||||||
|
interface IKuwoLyricDownloadOption {
|
||||||
|
/** 歌词KEY */
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载酷我音乐歌词
|
||||||
|
* @param option 下载选项
|
||||||
|
*/
|
||||||
|
export async function downloadKuwoLyric(option: IKuwoLyricDownloadOption) {
|
||||||
|
//请求歌词
|
||||||
|
let buffer = await request<Buffer>({ url: `http://newlyric.kuwo.cn/newlyric.lrc?${option.key}`, method: 'get' })
|
||||||
|
|
||||||
|
//读取基本信息(其实没有用),并得到歌词内容
|
||||||
|
const baseInfo: any = {}
|
||||||
|
let beg = 0
|
||||||
|
for (let i = 0; i < buffer.byteLength; ++i) {
|
||||||
|
//读取到换行
|
||||||
|
if (buffer.readUint16BE(i) == 0x0d0a) {
|
||||||
|
//得到信息
|
||||||
|
const infoStr = buffer.subarray(beg, i).toString()
|
||||||
|
const eqAt = infoStr.indexOf('=')
|
||||||
|
if (eqAt > 0) {
|
||||||
|
const key = infoStr.substring(0, eqAt).trim()
|
||||||
|
const val = infoStr.substring(eqAt + 1).trim()
|
||||||
|
switch (key) {
|
||||||
|
case 'path':
|
||||||
|
case 'score':
|
||||||
|
case 'lrc_length':
|
||||||
|
case 'cand_lrc_count':
|
||||||
|
case 'wiki_entry_sig':
|
||||||
|
case 'lrcx':
|
||||||
|
case 'show':
|
||||||
|
baseInfo[key] = val ? parseInt(val) : null
|
||||||
|
if (typeof baseInfo[key] === 'number' && isNaN(baseInfo[key])) baseInfo[key] = val
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
baseInfo[key] = val
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beg = i + 2
|
||||||
|
//连续换行,结束
|
||||||
|
if (buffer.readUint16BE(beg) == 0x0d0a) {
|
||||||
|
buffer = buffer.subarray(beg + 2)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//解码歌词
|
||||||
|
return lrcxUtil.decrypt(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网易云音乐下载选项
|
||||||
|
*/
|
||||||
|
interface IDownloadNeteasyLyric {
|
||||||
|
/** 歌曲ID */
|
||||||
|
musicID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载网易云音乐歌词
|
||||||
|
* @param option 下载选项
|
||||||
|
*/
|
||||||
|
export async function downloadNeteasyLyric(option: IDownloadNeteasyLyric) {
|
||||||
|
|
||||||
|
const encryptData = (paramData: any) => {
|
||||||
|
const text = JSON.stringify(paramData);
|
||||||
|
const url = '/api/song/lyric';
|
||||||
|
const message = `nobody${url}use${text}md5forencrypt`;
|
||||||
|
const digest = crypto.createHash('md5').update(message).digest("hex");
|
||||||
|
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
|
||||||
|
const cipher = crypto.createCipheriv(`aes-128-ecb`, 'e82ckenh8dichen8', '');
|
||||||
|
return Buffer.concat([cipher.update(Buffer.from(data)), cipher.final()]).toString('hex').toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request({
|
||||||
|
url: 'https://interface.music.163.com/eapi/song/lyric',
|
||||||
|
method: 'post',
|
||||||
|
body: util => util.urlencoded({
|
||||||
|
params: encryptData({
|
||||||
|
os: "pc",
|
||||||
|
id: option.musicID.toString(),
|
||||||
|
lv: "-1",
|
||||||
|
kv: "-1",
|
||||||
|
tv: "-1",
|
||||||
|
rv: "-1",
|
||||||
|
yv: "1",
|
||||||
|
showRole: "true",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
// 'Origin': 'orpheus://orpheus',
|
||||||
|
// 'Cookie': 'os=pc; deviceId=9E6E85D4C25B23CD18B4781301BF5AA8C83A260E525400485AC4; osver=Microsoft-Windows-10-Professional-build-22000-64bit; appver=2.10.6.200601; NMTID=00OxyKBYOYjYuWko0YsqwDxN_KA3-kAAAGGFcYW_g; channel=netease; WEVNSM=1.0.0; mode=Standard%20PC%20(Q35%20%2B%20ICH9%2C%202009); __csrf=98ba68b228a2c0065e9820245d7c3605; MUSIC_A=e177342ffaa63031618420730fe7c83bd0d668d4476a15687eb986f8c79eeb1c062890f94ac493a999e7ad1570fb37952038bc2b7b0dc759fd95a411e59159fe033fad747150c42ddd2c085dbd859f914156aea54e160355b25a13460616b0f319145b139d7e79695bdffc390ee188a1f246a3151047f27c4212382188fe1965; ntes_kaola_ad=1; WNMCID=qbecdc.1675402548248.01.0',
|
||||||
|
// 'MConfig-Info': '{"IuRPVVmc3WWul9fT":{"version":288768,"appver":"2.10.6.200601"}}',
|
||||||
|
// 'MG-Product-Name': 'music',
|
||||||
|
// 'Nm-GCore-Status': 1,
|
||||||
|
},
|
||||||
|
type: 'json',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (res.yrc.lyric as string | null) || null
|
||||||
|
}
|
1
src/net/index.ts
Normal file
1
src/net/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './download'
|
47
src/test/index.ts
Normal file
47
src/test/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import http from 'http'
|
||||||
|
import https from 'https'
|
||||||
|
import path from 'path'
|
||||||
|
import { qrc, krc, lrc, lrcx, nrc, utils } from '../index'
|
||||||
|
|
||||||
|
|
||||||
|
//解析QRC
|
||||||
|
async function test() {
|
||||||
|
// const xml = qrc.decrypt(fs.readFileSync('data/new.qrc'))?.toString()
|
||||||
|
// if (xml) {
|
||||||
|
// qrc.encrypt(xml)
|
||||||
|
// }
|
||||||
|
|
||||||
|
console.log('==============================测试qrc==========================================')
|
||||||
|
|
||||||
|
await utils.downloadQQMusicLyric({ songID: 102878776 }).then(data => {
|
||||||
|
const lyric = qrc.parse(data!)
|
||||||
|
console.log(lyric)
|
||||||
|
console.log(qrc.stringify(lyric))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('==============================测试krc==========================================')
|
||||||
|
await utils.downloadKugouLyric({ id: 10515303, accesskey: '3A20F6A1933DE370EBA0187297F5477D' }).then(data => {
|
||||||
|
const lyric = krc.parse(data!)
|
||||||
|
console.log(lyric)
|
||||||
|
console.log(lrcx.stringify(lyric))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('==============================测试lrcx==========================================')
|
||||||
|
await utils.downloadKuwoLyric({
|
||||||
|
key: 'DBYAHlRcX0pcV1RFIjsqLCYzUEFfV1RLVDY4WFUOEgEcHAcaOhIJCzBYWU1URUcKFhxJLhskGh0QBkMeDB4bHBYRCRtSAhYGBAABAB0NQxcJGFJfWUMWAwcIABgIAFG9ra+40Z/dhKG8zKG1qE8OHA0MFhhU2qW5k9uRTx0HHVgoOTomLSZTUl9dX1tJU0MAGwwWRFRDAwUdDURU',
|
||||||
|
}).then(txt => {
|
||||||
|
const lyric = lrcx.parse(txt!)
|
||||||
|
console.log(lyric)
|
||||||
|
console.log(lrcx.stringify(lyric))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('==============================测试nrc==========================================')
|
||||||
|
await utils.downloadNeteasyLyric({ musicID: '210049' }).then(data => {
|
||||||
|
const lyric = nrc.parse(data!)
|
||||||
|
console.log(lyric)
|
||||||
|
console.log(nrc.stringify(lyric))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test()
|
103
tsconfig.json
Normal file
103
tsconfig.json
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
"declarationDir": "./typing", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user