初次提交

This commit is contained in:
2023-02-07 08:57:53 +08:00
commit d5089754f6
20 changed files with 1876 additions and 0 deletions

6
src/index.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
export * from './download'

47
src/test/index.ts Normal file
View 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()