代码初提交

This commit is contained in:
2018-12-21 22:55:05 +08:00
commit c31a155252
12 changed files with 464 additions and 0 deletions

7
src/consts.ts Normal file
View File

@ -0,0 +1,7 @@
export const LYRIC_SEARCH_URL = 'http://lyrics.kugou.com/search'
export const LYRIC_FETCH_URL = 'http://lyrics.kugou.com/download'
//'@Gaw^2tGQ61-ÎÒni'
export const KRC_ENCODE_KEY = new Buffer(new Uint8Array([64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105]))

83
src/fetcher.ts Normal file
View File

@ -0,0 +1,83 @@
import request from 'request'
import base64 from 'base64-js'
import { parseParam, decodeKrc } from './util'
import { LYRIC_SEARCH_URL, LYRIC_FETCH_URL } from './consts'
export interface KugouLyricInfo {
id: string
accesskey: string
duration: number
uid: string
song: string
singer: string
}
export interface LyricSearchOption {
/** music name */
name: string
/** duration (unit:ms) */
time: number
}
export interface LyricFetchOption {
id: string
accesskey: string
fmt: 'lrc' | 'krc'
/** if need decode krc, give me true */
decodeKrc?: boolean
}
/**
* search lyrics from Kugou
* @param option search option
*/
export function search(option: LyricSearchOption): Promise<Array<KugouLyricInfo>> {
return new Promise((resolve, reject) => {
//http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=歌曲名&duration=歌曲总时长(毫秒)&hash=歌曲Hash值
const url = LYRIC_SEARCH_URL + '?' + parseParam({ ver: 1, man: 'yes', client: 'pc', keyword: option.name, duration: option.time })
let buffer = ''
const req = request(url)
let err: Error
req.on('data', data => buffer += data)
req.on('error', _err => err = _err)
req.on('complete', () => {
req.removeAllListeners()
if (err) return reject(err)
try {
var { candidates } = JSON.parse(buffer)
resolve(candidates)
} catch (err) {
reject(err)
}
})
})
}
/**
* get lyric from Kugou
* @param option fetch option
*/
export function fetch(option: LyricFetchOption): Promise<Buffer> {
return new Promise((resolve, reject) => {
//http://lyrics.kugou.com/download?ver=1&client=pc&id=10515303&accesskey=3A20F6A1933DE370EBA0187297F5477D&fmt=lrc&charset=utf8
const url = LYRIC_FETCH_URL + '?' + parseParam({ ver: 1, client: 'pc', id: option.id, accesskey: option.accesskey, fmt: option.fmt, charset: 'utf8' })
const req = request(url)
let buffer = ''
let err: Error
req.on('data', data => buffer += data)
req.on('error', e => err = e)
req.on('complete', () => {
if (err) return reject(err)
try {
const res = JSON.parse(buffer)
if (res.fmt != 'lrc' && res.fmt != 'krc') throw new Error('unkown format')
if (!res.content) throw new Error('empty content')
let buf = new Buffer(base64.toByteArray(res.content))
if (res.fmt == 'krc' && option.decodeKrc) resolve(decodeKrc(buf))
else resolve(buf)
} catch (err) {
reject(err)
}
})
})
}

29
src/index.ts Normal file
View File

@ -0,0 +1,29 @@
import { time2ms, decodeKrc } from "./util"
import * as parser from './parser'
import { search, fetch } from "./fetcher"
const util = {
time2ms,
decodeKrc
}
export {
parser,
search, fetch,
util
}
async function main() {
try {
// const lrcs = await search({ name: '谢东 - 笑脸', time: time2ms('04:12') })
const lrcs = await search({ name: 'linkin park - numb', time: time2ms('03:07') })
const lyric = lrcs[0]
const lrc = await fetch({ id: lyric.id, accesskey: lyric.accesskey, fmt: 'krc', decodeKrc: true })
// const result = parseLrc(lrc)
const result = parser.parseKrc(lrc)
} catch (err) {
console.log(err)
}
}
main()

2
src/parser/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { parseLrc, LrcInfo, LrcItem } from './lrc'
export { parseKrc, KrcInfo, KrcItem, KrcWord } from './krc'

83
src/parser/krc.ts Normal file
View File

@ -0,0 +1,83 @@
import { stringeq } from "./../util"
export interface KrcWord {
text: string
duration: number
}
export interface KrcItem {
time: number
duration: number
words: Array<KrcWord>
}
export interface KrcInfo {
ti?: string
ar?: string
al?: string
by?: string
offset?: string
items: Array<KrcItem>
}
function parseWords(str: string) {
const words: Array<KrcWord> = []
do {
const match = str.match(/^<(\d+),(\d+),(\d+)>([\s\S]+?)(<|$)/)
if (!match) break
const [sub, offset, duration, _, text] = match
const isEnd = sub[sub.length - 1] != '<'
words.push({ text: text, duration: parseInt(duration) })
if (isEnd) break
else str = str.substr(sub.length - 1)
} while (true)
return words
}
/**
* parse krc
* @param content content of krc
*/
export function parseKrc(content: Buffer | string): KrcInfo {
const krc: KrcInfo = { items: [] }
//行分割
const lines = (content + '').split(/\r?\n/).map(s => s.trim()).filter(s => !!s)
//逐行转换
lines.forEach(line => {
//基本信息
if (stringeq(line, '[ti:')) krc.ti = line.match(/\[ti:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[ar:')) krc.ar = line.match(/\[ar:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[al:')) krc.al = line.match(/\[al:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[by:')) krc.by = line.match(/\[by:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[offset:')) krc.offset = line.match(/\[offset:([\s\S]*?)\]/)![1]
else {
//时间和文本
const match = line.match(/^((\[\d+,\d+\])+)([\s\S]+)$/)
if (!match) return
const times = match[1]
const body = match[3]
//时间
const tmatch = times.match(/\[\d+,\d+\]/g)
if (!tmatch) return
//文本
const words = parseWords(body)
//每个时间
tmatch.forEach(time => {
const match = time.match(/^\[(\d+),(\d+)\]$/)
if (!match) return
krc.items.push({
time: parseInt(match[1]),
duration: parseInt(match[2]),
words
})
})
}
})
//排序
krc.items = krc.items.sort((a, b) => {
if (a.time == b.time) return 0
if (a.time > b.time) return 1
return -1
})
return krc
}

68
src/parser/lrc.ts Normal file
View File

@ -0,0 +1,68 @@
import { stringeq } from "./../util"
export interface LrcItem {
time: number
content: string
}
export interface LrcInfo {
ti?: string
ar?: string
al?: string
by?: string
offset?: string
items: Array<LrcItem>
}
/**
* parse lrc
* @param content content of lrc
*/
export function parseLrc(content: Buffer | string): LrcInfo {
const lrc: LrcInfo = { items: [] }
const lines = (content + '').trim().split(/\r?\n/).map(s => s.trim()).filter(s => !!s)
//逐行转换
lines.forEach(line => {
//基本信息
if (stringeq(line, '[ti:')) lrc.ti = line.match(/\[ti:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[ar:')) lrc.ar = line.match(/\[ar:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[al:')) lrc.al = line.match(/\[al:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[by:')) lrc.by = line.match(/\[by:([\s\S]*?)\]/)![1]
else if (stringeq(line, '[offset:')) lrc.offset = line.match(/\[offset:([\s\S]*?)\]/)![1]
//歌词信息
else {
//整体匹配
const match = line.match(/^((\[\d+:\d+(\.\d+)?\])+)([\s\S]+?)$/)
if (!match) return
const times = match[1].trim()
const text = match[4].trim()
//时间匹配
const tmatch = times.match(/(\[\d+:\d+(\.\d+)?\])/g)
if (!tmatch) return
//逐个时间处理
tmatch.map(t => t.trim()).map(t => t.substr(1, t.length - 2)).forEach(time => {
//分秒匹配
const match = time.match(/^(\d+):(\d+)(\.\d+)?$/)
if (!match) return
let [_, m, s, ms] = match
//计算时间
let t = (parseInt(m) * 60 + parseInt(s)) * 1000
if (ms) {
ms = ms.substr(1)
if (ms.length == 2) t += parseInt(ms) * 10
else t += parseInt(ms)
}
if (isNaN(t)) return
//保存歌词
lrc.items.push({ time: t, content: text })
})
}
})
//时间排序
lrc.items = lrc.items.sort((a, b) => {
if (a.time == b.time) return 0
if (a.time > b.time) return 1
return -1
})
return lrc
}

40
src/util.ts Normal file
View File

@ -0,0 +1,40 @@
import zlib from 'zlib'
import { KRC_ENCODE_KEY } from './consts'
export function parseParam(param: { [i: string]: string | number }) {
return Object.keys(param).map(k => `${k}=${encodeURIComponent(param[k] + '')}`).join('&')
}
/**
* parse time ($Minutes:$Seconds) to ms
* @param time music time, such as: xx:xx
*/
export function time2ms(time: string) {
const [m, s] = time.split(/:/).map(s => parseInt(s.trim()))
if (isNaN(m) || isNaN(s)) throw new Error('time format error')
return (m * 60 + s) * 1000
}
/**
* decode krc
* @param content krc content
*/
export function decodeKrc(content: Buffer): Buffer {
const buffer = new Buffer(content.length - 4)
//解码
for (let i = 4; i < content.length; i++) {
buffer[i - 4] = content[i] ^ KRC_ENCODE_KEY[(i - 4) % 16]
}
//解压
return zlib.unzipSync(buffer)
}
export function stringeq(str: string, sub: string, offset = 0) {
let eq = true
if (sub.length + offset > str.length) return false
for (let i = 0; i < sub.length; i++) {
if (sub[i] != str[i + offset]) return false
}
return eq
}