From c31a1552529912f028985ab8f7c0823e2f61912b Mon Sep 17 00:00:00 2001 From: yizhi <946185759@qq.com> Date: Fri, 21 Dec 2018 22:55:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=88=9D=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +++ .npmignore | 4 +++ README.md | 41 ++++++++++++++++++++++ package.json | 37 ++++++++++++++++++++ src/consts.ts | 7 ++++ src/fetcher.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 29 ++++++++++++++++ src/parser/index.ts | 2 ++ src/parser/krc.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++ src/parser/lrc.ts | 68 +++++++++++++++++++++++++++++++++++++ src/util.ts | 40 ++++++++++++++++++++++ tsconfig.json | 65 +++++++++++++++++++++++++++++++++++ 12 files changed, 464 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 package.json create mode 100644 src/consts.ts create mode 100644 src/fetcher.ts create mode 100644 src/index.ts create mode 100644 src/parser/index.ts create mode 100644 src/parser/krc.ts create mode 100644 src/parser/lrc.ts create mode 100644 src/util.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1edbf59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/dist +/typing +/package-lock.json +.DS_Store \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..52285bc --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +/src +/tsconfig.json +/node_modules +/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd52648 --- /dev/null +++ b/README.md @@ -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() +``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e4685b2 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..09d7997 --- /dev/null +++ b/src/consts.ts @@ -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])) \ No newline at end of file diff --git a/src/fetcher.ts b/src/fetcher.ts new file mode 100644 index 0000000..49c25c8 --- /dev/null +++ b/src/fetcher.ts @@ -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> { + 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 { + 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) + } + }) + }) +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0524dbb --- /dev/null +++ b/src/index.ts @@ -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() \ No newline at end of file diff --git a/src/parser/index.ts b/src/parser/index.ts new file mode 100644 index 0000000..3e8faf7 --- /dev/null +++ b/src/parser/index.ts @@ -0,0 +1,2 @@ +export { parseLrc, LrcInfo, LrcItem } from './lrc' +export { parseKrc, KrcInfo, KrcItem, KrcWord } from './krc' \ No newline at end of file diff --git a/src/parser/krc.ts b/src/parser/krc.ts new file mode 100644 index 0000000..2b107b3 --- /dev/null +++ b/src/parser/krc.ts @@ -0,0 +1,83 @@ +import { stringeq } from "./../util" + +export interface KrcWord { + text: string + duration: number +} + +export interface KrcItem { + time: number + duration: number + words: Array +} + +export interface KrcInfo { + ti?: string + ar?: string + al?: string + by?: string + offset?: string + items: Array +} + +function parseWords(str: string) { + const words: Array = [] + 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 +} \ No newline at end of file diff --git a/src/parser/lrc.ts b/src/parser/lrc.ts new file mode 100644 index 0000000..9f2d6e1 --- /dev/null +++ b/src/parser/lrc.ts @@ -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 +} + +/** + * 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 +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..28aa521 --- /dev/null +++ b/src/util.ts @@ -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 +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..300bee8 --- /dev/null +++ b/tsconfig.json @@ -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" + ] +} \ No newline at end of file