commit d5089754f66d79409f76631a9dfe12010440734a Author: yizhi <946185759@qq.com> Date: Tue Feb 7 08:57:53 2023 +0800 初次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1364058 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/dist +/typing +/package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..2e8edfe --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +/node_modules +/src +/package-lock.json +/tsconfig.json +/.gitignore \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..bf9ed01 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "启动程序", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/dist/test/index.js", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d2a779 --- /dev/null +++ b/README.md @@ -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) //输出解析后的歌词信息 +} + +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..a0dc21d --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a8bacd0 --- /dev/null +++ b/src/index.ts @@ -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' \ No newline at end of file diff --git a/src/lib/des.ts b/src/lib/des.ts new file mode 100644 index 0000000..d916105 --- /dev/null +++ b/src/lib/des.ts @@ -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) + } +} diff --git a/src/lib/request.ts b/src/lib/request.ts new file mode 100644 index 0000000..aa0743a --- /dev/null +++ b/src/lib/request.ts @@ -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 + type?: 'json' | 'buffer' | 'text' +} + +export function request(option: IRequestOption) { + return new Promise((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) + }) +} diff --git a/src/lib/xml.ts b/src/lib/xml.ts new file mode 100644 index 0000000..1eab380 --- /dev/null +++ b/src/lib/xml.ts @@ -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 + } + + // 解析 ') 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, `"']).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(), + } +} \ No newline at end of file diff --git a/src/lyric/common.ts b/src/lyric/common.ts new file mode 100644 index 0000000..e11c40e --- /dev/null +++ b/src/lyric/common.ts @@ -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 +} \ No newline at end of file diff --git a/src/lyric/declare.ts b/src/lyric/declare.ts new file mode 100644 index 0000000..7cb3e5d --- /dev/null +++ b/src/lyric/declare.ts @@ -0,0 +1,61 @@ +/** 歌词类型 */ +export enum LyricType { + /** 普通歌词 */ + REG, + /** 卡拉OK歌词 */ + KARA, +} + +/** 卡拉OK歌词行信息 */ +export interface IKaraokeWord { + /** 开始时间,相对于行时间 */ + start: number + /** 持续时间 */ + duration: number + /** 内容 */ + content: string +} + +/** 歌词行 */ +export interface ILyricLine { + /** 开始时间 */ + 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[] +} + +/** 卡拉歌词信息 */ +export interface IKaraOKLyric extends ILyricCommon { + /** 歌词类型 */ + type: LyricType.KARA + /** 歌词内容 */ + content: ILyricLine[] +} + +/** 歌词信息 */ +export type ILyric = (IRegularLyric | IKaraOKLyric) \ No newline at end of file diff --git a/src/lyric/krc.ts b/src/lyric/krc.ts new file mode 100644 index 0000000..b1a1cce --- /dev/null +++ b/src/lyric/krc.ts @@ -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 = { + 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') +} diff --git a/src/lyric/lrc.ts b/src/lyric/lrc.ts new file mode 100644 index 0000000..ce6d06a --- /dev/null +++ b/src/lyric/lrc.ts @@ -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') +} diff --git a/src/lyric/lrcx.ts b/src/lyric/lrcx.ts new file mode 100644 index 0000000..e67b4ad --- /dev/null +++ b/src/lyric/lrcx.ts @@ -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 = { + 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') +} diff --git a/src/lyric/nrc.ts b/src/lyric/nrc.ts new file mode 100644 index 0000000..2583e90 --- /dev/null +++ b/src/lyric/nrc.ts @@ -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 = { + 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') +} diff --git a/src/lyric/qrc.ts b/src/lyric/qrc.ts new file mode 100644 index 0000000..37cf1a6 --- /dev/null +++ b/src/lyric/qrc.ts @@ -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 = { + 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 [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n') +} diff --git a/src/net/download.ts b/src/net/download.ts new file mode 100644 index 0000000..91e1fa2 --- /dev/null +++ b/src/net/download.ts @@ -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({ + 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({ 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 +} diff --git a/src/net/index.ts b/src/net/index.ts new file mode 100644 index 0000000..3e35ab8 --- /dev/null +++ b/src/net/index.ts @@ -0,0 +1 @@ +export * from './download' \ No newline at end of file diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 0000000..cd3dd11 --- /dev/null +++ b/src/test/index.ts @@ -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() \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2710b7a --- /dev/null +++ b/tsconfig.json @@ -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 ''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. */ + } +}