代码初提交

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

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/node_modules
/dist
/typing
/package-lock.json
.DS_Store

4
.npmignore Normal file
View File

@ -0,0 +1,4 @@
/src
/tsconfig.json
/node_modules
/.DS_Store

41
README.md Normal file
View File

@ -0,0 +1,41 @@
# kugou-lyric
search、 download、decode krc(or lrc) from Kugou
## Installation
```
npm install kugou-lyric
```
## Usage
it's verry easy to use this util, if you want to get more information, read `index.d.ts`.
there is a demo to show you how to use it.
```typescript
import { parser, search, fetch, util } from 'kugou-lyric'
// parser: util for parsing krc(decode first) and lrc
// search: search krc and lrc from Kugou
// fetch: download krc and lrc from Kugou
// util: some utils maybe you will use
async function main(){
// search lyrics, "name" and "time" is required
const lyrics = await search({ name: 'linkin park - Numb', time: time2ms('03:07') })
if(!lyrics.length) return
// get krc or lrc
const krc = await fetch({
id: lyrics[0].id,
accesskey: lyrics[0].accesskey,
fmt: 'krc', // if you want to download lrc, using "lrc"
decodeKrc: true, // won't decode krc when false
// you can use "util.decodeKrc" function to decode it
})
// parse krc
const result = parser.parseKrc(krc)
// {ti:xx, ar:xx, items:[{time:xx, duration:xx, words:[{text:xx, duration:xx}, ...]]}, ...]}
}
main()
```

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "kugou-lyric",
"version": "1.0.3",
"description": "",
"main": "dist/index.js",
"types":"typing/index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/kangkang520/kugou-lyric.git"
},
"keywords": [
"kugou",
"krc",
"lyric",
"lrc",
"酷狗",
"parser",
"fetch",
"search",
"music",
"player"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/base64-js": "^1.2.5",
"@types/node": "^10.12.18"
},
"dependencies": {
"@types/request": "^2.48.1",
"base64-js": "^1.3.0",
"request": "^2.88.0"
}
}

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
}

65
tsconfig.json Normal file
View File

@ -0,0 +1,65 @@
{
"compilerOptions": {
/* Basic Options */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [
"es2015",
"esnext"
], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationDir": "typing",
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": [
"src/**/*.ts"
],
"exclude": [
"typing",
"node_modules"
]
}