代码初提交
This commit is contained in:
7
src/consts.ts
Normal file
7
src/consts.ts
Normal 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
83
src/fetcher.ts
Normal 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
29
src/index.ts
Normal 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
2
src/parser/index.ts
Normal 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
83
src/parser/krc.ts
Normal 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
68
src/parser/lrc.ts
Normal 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
40
src/util.ts
Normal 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
|
||||
}
|
Reference in New Issue
Block a user