初次提交

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

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')
}