├── .gitignore ├── .npmignore ├── HISTORY.md ├── README.md ├── bin └── lyric-dl ├── example ├── download.js └── search_and_download_all.sh ├── index.js ├── lib ├── command │ ├── download.js │ └── search.js └── core │ ├── common.js │ ├── loader.js │ ├── loader │ ├── ntes.js │ ├── qq.js │ └── xiami.js │ ├── logger.js │ ├── lyric.js │ ├── request.js │ └── rsa.js ├── package.json └── test ├── command_tests.sh └── tests.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.lrc 2 | .DS_store 3 | .vscode 4 | out 5 | example/download 6 | node_modules 7 | lyric-dl 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gitignore 3 | .git 4 | .vscode 5 | out 6 | example 7 | node_modules 8 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # HISTORY 2 | 3 | ### 2017.1.29 4 | 5 | * support search from qq music 6 | * some code refactoring 7 | 8 | ### 2017.1.28 9 | 10 | * update http service 11 | * bug fix 12 | 13 | ### 2017.1.21 14 | 15 | * support download multi-url and receive urls form stdin in command line 16 | 17 | ### 2017.1.10 18 | 19 | * support ntes search in command line 20 | 21 | ### 2016.12.17 22 | 23 | * support qq music 24 | * support http service -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Lyrics Download Tools 2 | 3 | **lyric-dl** 是一个下载在线音乐歌词的命令行工具,它是一个 Node.js 程序,在使用它之前你必须安装 [Node.js](https://nodejs.org/en/) 4 | 5 | 目前支持的网站: 6 | 7 | * **网易云音乐 (ntes)** : 8 | * **QQ音乐 (qq)** 9 | * **虾米音乐 (xiami)** 10 | 11 | 安装 12 | 13 | npm install lyric-dl -g 14 | 15 | 更新 16 | 17 | npm update lyric-dl -g 18 | 19 | OR 20 | 21 | git clone https://github.com/frimin/lyric-dl 22 | cd lyric-dl 23 | npm install 24 | ln -s bin/lyric-dl lyric-dl 25 | ./lyric-dl dl 26 | 27 | ### 使用 28 | 29 | 命令格式 30 | 31 | lyric-dl [命令 [参数 [参数 ...]]] [选项] 32 | 33 | 拥有一个歌曲地址,可以通过 URL 来下载歌词,以 JSON 格式输出到标准输出以获取歌曲信息 34 | 35 | lyric-dl download --out-format json 36 | 37 | 通过歌曲名来获取搜索结果 38 | 39 | lyric-dl search 40 | 41 | 使用管道批量下载搜索结果的歌词到当前目录 42 | 43 | lyric-dl search --output-url | lyric-dl dl -d . - 44 | 45 | 在下载命令中可以指定 **--extract** 选项,该选项会提取歌词的正文内容,剔除标题等其他信息 -------------------------------------------------------------------------------- /bin/lyric-dl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var program = require('commander'); 4 | var common = require('../lib/core/common'); 5 | var loader = require('../lib/core/loader'); 6 | 7 | var state = {}; 8 | 9 | program 10 | .description("Lyrics download tools, support:\n\ 11 | - ntes (163 cloud music) \n\ 12 | - qq (qq music)\n\ 13 | - xiami (xiami music)"); 14 | 15 | state.globalOptions = function (options) { 16 | } 17 | 18 | require('../lib/command/download').init(state, program); 19 | require('../lib/command/search').init(state, program); 20 | 21 | program 22 | .command('list') 23 | .description('show all available loader') 24 | .alias('l') 25 | .action(function(args, options){ 26 | state.execCmd = true; 27 | console.log(loader.list.join('\n')); 28 | }); 29 | 30 | program.parse(process.argv); 31 | 32 | if (typeof state.execCmd === 'undefined') { 33 | console.error('no command given, use "--help" or " --help" option to get usage information.'); 34 | common.errorExit(); 35 | } 36 | -------------------------------------------------------------------------------- /example/download.js: -------------------------------------------------------------------------------- 1 | var lyric_dl = require('../index') 2 | 3 | var ntes = lyric_dl.getLoader('ntes') 4 | 5 | ntes.search({ name: 'I' }, (ret) => { 6 | ret.search.forEach((e) => { 7 | ntes.download({ id: e.id }, (ret) => { 8 | console.log(JSON.stringify(ret)) 9 | }) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /example/search_and_download_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [[ -z "$1" ]]; then 4 | echo "no search name" >&2 5 | exit 2 6 | fi 7 | 8 | if [[ ! -d samples/download ]]; then 9 | mkdir samples/download 10 | fi 11 | 12 | echo "searching '$1'..." 13 | 14 | lyric-dl search "$1" -o samples/download/search_result --output-url 15 | 16 | if [[ $? -ne 0 ]]; then 17 | echo "search failed" >&2 18 | exit 2 19 | fi 20 | 21 | cat samples/download/search_result | lyric-dl dl - -d samples/download 22 | 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var loader = require('./lib/core/loader') 2 | 3 | // APIs 4 | 5 | module.exports.getLoader = function (name) { 6 | return loader.all[name]; 7 | } 8 | 9 | module.exports.getLoaderFromUrl = function (url) { 10 | return loader.parseUrl(url) 11 | } 12 | -------------------------------------------------------------------------------- /lib/command/download.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var async = require('async'); 3 | var loader = require('../core/loader'); 4 | var common = require('../core/common'); 5 | var logger = require('../core/logger'); 6 | 7 | var nonAllPrintOpt = [ 'outputOriginal', 'outputTranslate' ]; 8 | 9 | function tryExtractLyricToStr(lrc) { 10 | if (Array.isArray(lrc)) { 11 | var lines = [] 12 | lrc.forEach((e) => { 13 | lines.push(`[${e[0]}]${e[1]}`) 14 | }) 15 | return lines.join('\n') 16 | } 17 | return lrc 18 | } 19 | 20 | function createWriteTask(log, options, id, name, content, ext) { 21 | return function (cb) { 22 | var wstream; 23 | 24 | var outdir = options['d'] || options['dir']; 25 | 26 | if (outdir) { 27 | if (!outdir.endsWith('/')) 28 | outdir = outdir + '/'; 29 | 30 | var outfile = outdir + (options['o'] || options['output'] || ('' + name + ' (' + id + ').' + ext)); 31 | 32 | log('SAVE ' + outfile); 33 | 34 | wstream = fs.createWriteStream(outfile); 35 | } else { 36 | wstream = process.stdout; 37 | } 38 | 39 | wstream.write(content, (err) => { 40 | cb(err); 41 | }); 42 | } 43 | } 44 | 45 | function createDownloadTask(url, options, downloaded) { 46 | return function (cb) { 47 | var rt 48 | 49 | try { 50 | rt = loader.parseUrl(url); 51 | } catch (e) { 52 | return cb(e + ': ' + url); 53 | } 54 | 55 | var songid = rt.id; 56 | 57 | var log = logger.createLog('debug', rt.loader.config.name + '#' + songid); 58 | 59 | if (downloaded[songid]) { 60 | return cb(null, true) // skip current url 61 | } 62 | 63 | rt.loader.download({ 64 | id: songid, 65 | extract: options['extract'] && true || false, 66 | log: log, 67 | }, function (result) { 68 | downloaded[songid] = true 69 | 70 | if (result['err']) { 71 | console.error(result['err']) 72 | process.exit(2) 73 | } 74 | 75 | var format = options['O'] || options['outFormat'] || 'lrc' 76 | 77 | var writeTasks = [] 78 | 79 | var printAll = true 80 | 81 | nonAllPrintOpt.forEach((e) => { 82 | printAll = !options[e] && printAll 83 | }) 84 | 85 | switch (format) { 86 | case 'lrc': 87 | if (result['lrc'] && (printAll || options['outputOriginal'] )) 88 | writeTasks.push(createWriteTask(log, options, songid, result['name'], tryExtractLyricToStr(result['lrc']), format)) 89 | if (result['tlrc'] && (printAll || options['outputTranslate'])) 90 | writeTasks.push(createWriteTask(log, options, songid, result['name'], tryExtractLyricToStr(result['tlrc']), 'tr.' + format)) 91 | break 92 | case 'json': 93 | writeTasks.push(createWriteTask(log, options, songid, result['name'], JSON.stringify(result), format)) 94 | break 95 | default: 96 | console.error("invalid out format:" + outformat) 97 | break 98 | } 99 | 100 | async.series(writeTasks, function (err, results) { 101 | cb(err); 102 | }); 103 | }) 104 | } 105 | } 106 | 107 | function downloadList(urlList, options) { 108 | if (urlList.length == 0) { 109 | console.error("nothing to do"); 110 | return process.exit(2); 111 | } 112 | 113 | var downloadTasks = []; 114 | var downloaded = {}; 115 | 116 | for (var i in urlList) { 117 | downloadTasks.push(createDownloadTask(urlList[i], options, downloaded)); 118 | } 119 | 120 | async.series(downloadTasks, function (err, results) { 121 | if (!err) { 122 | process.exit(0); 123 | } else { 124 | console.error(`download abort: ${err}`) 125 | process.exit(2); 126 | } 127 | }); 128 | } 129 | 130 | function readStdin(callback) { 131 | var data = '' 132 | 133 | process.stdin.on('data', function(chunk) { 134 | data += chunk; 135 | }) 136 | 137 | process.stdin.on('end', function() { 138 | callback(data) 139 | }) 140 | } 141 | 142 | function handler(urlList, options) { 143 | // url list from stdin 144 | if (urlList.length == 1 && urlList[0] == '-') { 145 | readStdin((data) => { 146 | data = data.split('\n') 147 | 148 | var list = [] 149 | 150 | for (var i in data) { 151 | var url = data[i].trim() 152 | if (url.length > 0) { 153 | list.push(url) 154 | } 155 | } 156 | 157 | downloadList(list, options) 158 | }) 159 | } else { 160 | downloadList(urlList, options) 161 | } 162 | } 163 | 164 | var description= "download lyric, support url format: \n\ 165 | \n\ 166 | * ntes (cloudmusic) : http://music.163.com/#/m/song?id= \n\ 167 | * qq (qqmusic) : https://y.qq.com/portal/song/.html \n" 168 | 169 | exports.init = function (state, program) { 170 | program 171 | .command('download ') 172 | .description('download lyric') 173 | .alias('dl') 174 | .option('-o, --output ', 'name of output file, if translate lyric exists, named: .tr') 175 | .option('-d, --dir ', 'name of output directory') 176 | .option('-O, --out-format ', 'output file in given format: =[lrc, json]') 177 | .option('--extract', 'extract lyrics content, match time and lines') 178 | .option('--output-original', 'enable output original lyrics content') 179 | .option('--output-translate', 'enable output translate lyrics content') 180 | .action(function(args, options){ 181 | state.execCmd = true; 182 | state.globalOptions(options.parent); 183 | handler(args, options); 184 | }); 185 | } -------------------------------------------------------------------------------- /lib/command/search.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var loader = require('../core/loader'); 3 | var common = require('../core/common'); 4 | var logger = require('../core/logger'); 5 | 6 | var nonAllPrintOpt = [ 'outputUrl', 'outputName', 'outputSingler', 'outputAlbum' ] 7 | 8 | function handler(name, options) { 9 | var fromSource = options['f'] || options['from'] || 'ntes' 10 | var loaderInst = loader.all[fromSource] 11 | 12 | if (!loaderInst) { 13 | console.error('failed: search source "' + fromSource + '" not exists') 14 | process.exit(2) 15 | } 16 | 17 | var log = logger.createLog('debug', 'search'); 18 | 19 | var page = 1; 20 | 21 | if (options['p'] || options['page']) { 22 | page = parseInt(options['p'] || options['page']); 23 | } 24 | 25 | loaderInst.search({ name: name, page: page, log: log }, function(result) { 26 | if (result['err']) { 27 | console.log(result['err']) 28 | process.exit(2) 29 | } else { 30 | var list = result['search'] 31 | var separator = options['separator'] || ' ' 32 | var lines = [] 33 | 34 | var printAll = true 35 | 36 | nonAllPrintOpt.forEach((e) => { 37 | printAll = !options[e] && printAll 38 | }) 39 | 40 | for (var i = 0; i != list.length; i++) { 41 | var item = list[i] 42 | var line = [] 43 | 44 | if (printAll || options['outputUrl']) { 45 | line.push(item.href) 46 | } 47 | 48 | if (printAll || options['outputName']) { 49 | line.push(item.name) 50 | } 51 | 52 | if (printAll || options['outputSingler']) { 53 | var names = [] 54 | 55 | for (var j = 0; j != item.singler.length; ++j) { 56 | names.push(item.singler[j].name) 57 | } 58 | 59 | line.push(names.join('/')) 60 | } 61 | 62 | if (printAll || options['outputAlbum']) { 63 | line.push(item.album.name) 64 | } 65 | 66 | if (line.length > 0) 67 | lines.push(line.join(separator)) 68 | } 69 | 70 | var outfile = options['output'] 71 | 72 | if (outfile) { 73 | fs.writeFile(outfile, lines.join('\n'), (err) => { 74 | if (err) { 75 | console.error(err) 76 | process.exit(2) 77 | } 78 | }) 79 | } else { 80 | console.log(lines.join('\n')) 81 | process.exit(0) 82 | } 83 | } 84 | }) 85 | } 86 | 87 | exports.init = function (state, program) { 88 | program 89 | .command('search ') 90 | .description('search from song name') 91 | .option('-f, --from ', 'search from source (default: ntes)') 92 | .option('--separator ', 'set separator string') 93 | .option('-p --page ', 'page for search') 94 | .option('--output-url', 'output url') 95 | .option('--output-name', 'output name') 96 | .option('--output-singler', 'output singler') 97 | .option('--output-album', 'output album') 98 | .action(function(args, options){ 99 | state.execCmd = true; 100 | state.globalOptions(options.parent); 101 | handler(args, options); 102 | }); 103 | } -------------------------------------------------------------------------------- /lib/core/common.js: -------------------------------------------------------------------------------- 1 | var requestCount = -1 2 | var logVisible = true 3 | 4 | exports.createLog = function (name, id) { 5 | if (id) 6 | requestCount += 1 7 | 8 | var count = requestCount 9 | 10 | return function(message) { 11 | if (logVisible) { 12 | if (id) { 13 | console.log("#" + requestCount + " [" + name + " - " + id + "] " + message) 14 | } else { 15 | console.log("## [" + name + "] " + message) 16 | } 17 | } 18 | } 19 | } 20 | 21 | exports.setLogVisible = function (b) { 22 | logVisible = b 23 | } 24 | 25 | exports.errorExit = function(text, code) { 26 | console.log(text) 27 | process.exit(code || 2) 28 | } 29 | 30 | exports.makeFailedData = function(text) { 31 | return { 'err': text || "failed to request" } 32 | } 33 | 34 | exports.makeLyricResponseData = function(dataList) { 35 | var merge = {} 36 | 37 | for (var i in dataList) { 38 | var e = dataList[i] 39 | if (typeof (e) == 'object') { 40 | for (var k in e) { 41 | merge[k] = e[k] 42 | } 43 | } 44 | } 45 | 46 | var rt = { 47 | 'version': 2, 48 | 'id': merge['id'], 49 | 'href': merge['href'] || null, 50 | 'name': merge['name'], 51 | 'tname': merge['tname'] || null, 52 | 'singer': merge['singer'], // [ { name: "", id: "" }, ...] 53 | 'lrc': merge['lrc'] || null, 54 | 'tlrc': merge['tlrc'] || null, 55 | 'album': merge['album'] || null, 56 | 'source': merge['source'], 57 | } 58 | 59 | if (!rt['id'] || !['name'] || !['album_name']) { 60 | return exports.makeFailedData('missing base data') 61 | } 62 | 63 | return rt 64 | } 65 | 66 | exports.makeSearchResponseData = function(searchResult, page) { 67 | if (searchResult['err']) { 68 | return exports.makeFailedData(searchResult['err']) 69 | } 70 | 71 | var rt = { 72 | 'search' : searchResult, 73 | 'page': page, 74 | } 75 | 76 | return rt 77 | } 78 | 79 | exports.removeEmptyFromHead = function(list) { 80 | var i = 0 81 | 82 | for(var j = 0; j != list.length; j++) { 83 | if (list[j][1] == '') { 84 | i = j + 1 85 | } else { 86 | break 87 | } 88 | } 89 | 90 | if (i != 0) { 91 | list.splice(0, i) 92 | } 93 | } 94 | 95 | exports.errorExit = function() { 96 | process.exit(1) 97 | } 98 | 99 | exports.checkHttpResponseError = function (err, res) { 100 | if (err) { 101 | throw 'failed to request'; 102 | } 103 | 104 | if (res.statusCode != 200) { 105 | throw 'response status code ' + res.statusCode; 106 | } 107 | } -------------------------------------------------------------------------------- /lib/core/loader.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var loader = {}; 3 | 4 | var loaderList = [ 5 | 'ntes', 6 | 'qq', 7 | 'xiami' 8 | ]; 9 | 10 | for (var i = 0; i != loaderList.length; i++) { 11 | loader[loaderList[i]] = require('./loader/' + loaderList[i]); 12 | } 13 | 14 | exports.all = loader; 15 | 16 | exports.list = loaderList; 17 | 18 | exports.parseUrl = function (urlAddress) { 19 | var songUrl = url.parse(urlAddress); 20 | for (var k in loader) { 21 | var inst = loader[k]; 22 | 23 | if (!inst.config.regHost.test(songUrl.hostname)) 24 | continue; 25 | 26 | var m = songUrl[inst.config.regID[0]].match(inst.config.regID[1]); 27 | 28 | if (!m || (m.length != 2)) { 29 | throw 'parse song id failed'; 30 | } 31 | 32 | return { loader: inst, id: m[1] }; 33 | } 34 | 35 | throw 'can\'t found loader, url may invalid or not support'; 36 | } -------------------------------------------------------------------------------- /lib/core/loader/ntes.js: -------------------------------------------------------------------------------- 1 | var CryptoJS = require("crypto-js"); 2 | var async = require('async') 3 | var common = require('../common') 4 | var querystring = require('querystring') 5 | var request = require('../request') 6 | var rsa = require('../rsa') 7 | var lyric = require('../lyric'); 8 | var logger = require('../logger'); 9 | 10 | exports.config = { 11 | name : 'ntes', 12 | regHost : /^music\.163\.com$/, 13 | regID: [ 'href', /id=(\d+)/, ], 14 | searchNums: 30, 15 | lyricsExtract : { 16 | deleteLineLimit: 5, 17 | reDeleteLine: /(作曲|作词|by::编曲)/, 18 | } 19 | } 20 | 21 | var localconfig = { 22 | encrypt: { 23 | rsaEncryptionExponent: '010001', 24 | rsaModulus: '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7', 25 | aseiv: '0102030405060708', 26 | aseSecretPassphrase: '0CoJUm6Qyw8W8jud', 27 | }, 28 | } 29 | 30 | function randKey(a) { 31 | var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = ""; 32 | for (d = 0; a > d; d += 1) 33 | e = Math.random() * b.length, 34 | e = Math.floor(e), 35 | c += b.charAt(e); 36 | return c 37 | } 38 | 39 | function aesEncrypt(a, b) { 40 | var c = CryptoJS.enc.Utf8.parse(b) 41 | , d = CryptoJS.enc.Utf8.parse(localconfig.encrypt.aseiv) 42 | , e = CryptoJS.enc.Utf8.parse(a) 43 | , f = CryptoJS.AES.encrypt(e, c, { 44 | iv: d, 45 | mode: CryptoJS.mode.CBC 46 | }); 47 | 48 | return f.toString() 49 | } 50 | function rsaEncrypt(text, encryptionExponent, modulus) { 51 | rsa.BigInt.setMaxDigits(131) 52 | var key = new rsa.RSAKeyPair(encryptionExponent,"",modulus) 53 | return rsa.encryptedString(key, text) 54 | } 55 | 56 | function encrypt(d, e, f, g) { 57 | var h = {}, i = randKey(16); 58 | return h.params = aesEncrypt(d, g), 59 | h.params = aesEncrypt(h.params, i), 60 | h.encSecKey = rsaEncrypt(i, e, f), 61 | h 62 | } 63 | 64 | function encryptEncodeArgs(args) { 65 | return querystring.stringify(encrypt( 66 | JSON.stringify(args), 67 | localconfig.encrypt.rsaEncryptionExponent, 68 | localconfig.encrypt.rsaModulus, 69 | localconfig.encrypt.aseSecretPassphrase)) 70 | } 71 | 72 | function invokeResult(cb, err, res, body) { 73 | if (err) { 74 | return cb('failed to request ') 75 | } 76 | 77 | if (res.statusCode != 200) { 78 | return cb(`response status code ${res.statusCode}`) 79 | } else { 80 | if (!body || body.length == 0) { 81 | return cb('empty response body') 82 | } 83 | 84 | var ret 85 | 86 | try { 87 | ret = JSON.parse(body) 88 | } catch(e) { 89 | return cb('failed to parse json') 90 | } 91 | 92 | if (ret.code == 200) { 93 | return cb(null, ret) 94 | } else { 95 | return cb('invalid result') 96 | } 97 | } 98 | } 99 | 100 | exports.download = function (options, response) { 101 | var id = options.id; 102 | var log = options.log || logger.createLog('debug'); 103 | 104 | var postData = encryptEncodeArgs({ 105 | id: id, 106 | lv: -1, 107 | tv: -1, 108 | csrf_token: "" 109 | }); 110 | 111 | var query = { 112 | id: id, 113 | ids: `[${id}]`, 114 | }; 115 | 116 | async.parallel({ 117 | info: (cb) => { 118 | var reqOptions = { 119 | host: 'music.163.com', 120 | path: '/api/song/detail/?id=' + querystring.stringify(query), 121 | method: 'GET', 122 | headers: { 123 | 'Referer': 'http://music.163.com/', 124 | 'Origin':'http://music.163.com', 125 | 'DNT': 1, 126 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,', 127 | }, 128 | }; 129 | 130 | log('REQUEST ' + reqOptions.host + reqOptions.path); 131 | 132 | request.http_request(reqOptions, (err, res, body) => { 133 | invokeResult(cb, err, res, body) 134 | }) 135 | }, 136 | lyric : (cb) => { 137 | var reqOptions = { 138 | host: 'music.163.com', 139 | path: '/weapi/song/lyric?csrf_token=', 140 | method: 'POST', 141 | headers: { 142 | 'Content-Length': Buffer.byteLength(postData), 143 | 'Content-Type': 'application/x-www-form-urlencoded', 144 | 'Referer': 'http://music.163.com/song?id=' + id, 145 | 'Origin':'http://music.163.com', 146 | 'DNT': 1, 147 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,', 148 | }, 149 | }; 150 | 151 | log('REQUEST ' + reqOptions.host + reqOptions.path); 152 | 153 | request.http_request(reqOptions, (err, res, body) => { 154 | invokeResult(cb, err, res, body) 155 | }, postData) 156 | }, 157 | }, (err, results) => { 158 | try 159 | { 160 | if (err) { 161 | throw err 162 | } 163 | 164 | if (!results.info.songs || results.info.songs.length != 1) 165 | throw 'failed to read information' 166 | 167 | var song = results.info.songs[0] 168 | var lrc = results.lyric 169 | 170 | var singer = [] 171 | 172 | song.artists.forEach(function(e) { 173 | singer.push({ 174 | name: e.name, 175 | id: e.id, 176 | href: 'http://music.163.com/#/artist?id=' + e.id, 177 | }) 178 | }); 179 | 180 | var result = common.makeLyricResponseData([{ 181 | source: 'ntes', 182 | id: song.id, 183 | href: 'http://music.163.com/#/m/song?id=' + song.id, 184 | name: song.name, 185 | singer: singer, 186 | lrc: lrc.lrc && lrc.lrc.lyric || null, 187 | tlrc: lrc.tlyric && lrc.tlyric.lyric || null, 188 | nolyric: lrc.nolyric, 189 | album: { 190 | name: song.album.name, 191 | id: song.album.id, 192 | href: 'http://music.163.com/#/album?id=' + song.album.id, 193 | }, 194 | }]) 195 | 196 | if (options.extract == true) { 197 | var ret = lyric.extractLyrics(result.lrc, result.tlrc, exports.config.lyricsExtract); 198 | result.lrc = ret[0]; 199 | result.tlrc = ret[1]; 200 | } 201 | 202 | response(result) 203 | } catch (e) { 204 | response(common.makeFailedData(e)) 205 | } 206 | }) 207 | } 208 | 209 | exports.search = function (options, response) { 210 | var name = options.name; 211 | var log = options.log || logger.createLog('debug'); 212 | 213 | var page = options.page || 1; 214 | 215 | var postData = encryptEncodeArgs({ 216 | s: name, 217 | type: '1', 218 | offset: (page - 1) * exports.config.searchNums, 219 | total: true, 220 | limit: exports.config.searchNums, 221 | csrf_token: '', 222 | }) 223 | 224 | async.parallel({ 225 | search: (cb) => { 226 | var reqOptions = { 227 | host: 'music.163.com', 228 | path: '/weapi/cloudsearch/get/web?csrf_token=', 229 | method: 'POST', 230 | headers: { 231 | 'Content-Length': Buffer.byteLength(postData), 232 | 'Content-Type': 'application/x-www-form-urlencoded', 233 | 'Referer': 'http://music.163.com/search/', 234 | 'Origin':'http://music.163.com', 235 | 'DNT': 1, 236 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,', 237 | }, 238 | }; 239 | 240 | log('REQUEST ' + reqOptions.host + reqOptions.path); 241 | 242 | request.http_request(reqOptions, (err, res, body) => { 243 | invokeResult(cb, err, res, body) 244 | }, postData) 245 | } 246 | }, (err, results) => { 247 | try 248 | { 249 | if (err) { 250 | throw err 251 | } 252 | 253 | var songs = results.search.result.songs 254 | 255 | if (!songs) 256 | throw 'failed to read information' 257 | 258 | var ret = [] 259 | 260 | songs.forEach(function(e) { 261 | var singler = [] 262 | 263 | e.ar.forEach(function(e) { 264 | singler.push({ 265 | id: e.id, 266 | name: e.name, 267 | href: `http://music.163.com/#/artist?id=${e.id}`, 268 | }) 269 | }) 270 | 271 | ret.push({ 272 | name: e.name, 273 | href: `http://music.163.com/#/song?id=${e.id}`, 274 | id: e.id, 275 | singler: singler, 276 | album: { 277 | id: e.al.id, 278 | name: e.al.name, 279 | href: `http://music.163.com/artist?id=${e.al.id}`, 280 | } 281 | }) 282 | }) 283 | 284 | var totalCount = results.search.result.songCount; 285 | 286 | response(common.makeSearchResponseData(ret, { 287 | page: page, 288 | pagetotal: Math.floor(totalCount / exports.config.searchNums) + 1, 289 | total: totalCount, 290 | })); 291 | } catch (e) { 292 | response(common.makeFailedData(e)) 293 | } 294 | }) 295 | } 296 | -------------------------------------------------------------------------------- /lib/core/loader/qq.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var common = require('../common') 3 | var querystring = require('querystring') 4 | var request = require('../request') 5 | var HTMLDecoder = require("html-decoder") 6 | var lyric = require('../lyric'); 7 | var logger = require('../logger'); 8 | 9 | exports.config = { 10 | name : 'qq', 11 | regHost : /^y\.qq\.com$/, 12 | regID: [ 'pathname', /song\/(\w+)\.html$/], 13 | searchNums: 20, 14 | lyricsExtract : { 15 | reDeleteLine: /(腾讯|词:|曲:)/, 16 | reDeleteWord: /\/\//g, 17 | } 18 | } 19 | 20 | function invokeResult(cb, err, res, body) { 21 | if (err) { 22 | return cb('failed to request') 23 | } 24 | 25 | if (res.statusCode != 200) { 26 | return cb(`response status code ${res.statusCode}`) 27 | } else { 28 | if (!body || body.length == 0) { 29 | return cb('empty response body') 30 | } 31 | 32 | var ret 33 | 34 | try { 35 | var start = body.indexOf('(') 36 | var end = body.lastIndexOf(')') 37 | ret = JSON.parse(body.substr(start + 1, end - start - 1)) 38 | } catch(e) { 39 | return cb('failed to parse json') 40 | } 41 | 42 | if (ret.code == 0) { 43 | return cb(null, ret) 44 | } else { 45 | if (ret.code == -1901) { 46 | return cb(null, {}) // no lyrics 47 | } else { 48 | return cb('invalid result') 49 | } 50 | } 51 | } 52 | } 53 | 54 | function doDownload(options, response) { 55 | var id = options.id; 56 | var log = options.log || logger.createLog('debug'); 57 | 58 | var infoQuery = { 59 | callback: 'getOneSongInfoCallback', 60 | pcachetime: new Date().getTime(), 61 | tpl:'yqq_song_detail', 62 | songmid: id, 63 | g_tk:5381, 64 | jsonpCallback: 'getOneSongInfoCallback', 65 | loginUin:0, 66 | hostUin:0, 67 | format:'jsonp', 68 | inCharset:'utf8', 69 | outCharset:'utf-8', 70 | notice:0, 71 | platform:'yqq', 72 | needNewCode: 0, 73 | } 74 | 75 | var lyricQuery = { 76 | callback: 'MusicJsonCallback_lrc', 77 | pcachetime: new Date().getTime(), 78 | songmid: id, 79 | g_tk:5381, 80 | jsonpCallback: 'MusicJsonCallback_lrc', 81 | loginUin:0, 82 | hostUin:0, 83 | format:'jsonp', 84 | inCharset:'utf8', 85 | outCharset:'utf-8', 86 | notice:0, 87 | platform:'yqq', 88 | needNewCode: 0, 89 | } 90 | 91 | async.parallel({ 92 | info : (cb) => { 93 | var reqOptions = { 94 | host: 'c.y.qq.com', 95 | path: '/v8/fcg-bin/fcg_play_single_song.fcg?' + querystring.stringify(infoQuery), 96 | method: 'GET', 97 | headers: { 98 | 'Referer': `https://y.qq.com/portal/song/${id}.html`, 99 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 100 | }, 101 | }; 102 | 103 | log('REQUEST ' + reqOptions.host + reqOptions.path); 104 | 105 | request.http_request(reqOptions, (err, res, body) => { 106 | invokeResult(cb, err, res, body) 107 | }) 108 | }, 109 | lyric: (cb) => { 110 | var reqOptions = { 111 | host: 'c.y.qq.com', 112 | path: '/lyric/fcgi-bin/fcg_query_lyric_new.fcg?' + querystring.stringify(lyricQuery), 113 | method: 'GET', 114 | headers: { 115 | 'Referer': 'https://y.qq.com/portal/player.html', 116 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 117 | }, 118 | }; 119 | 120 | log('REQUEST ' + reqOptions.host + reqOptions.path); 121 | 122 | request.http_request(reqOptions, (err, res, body) => { 123 | invokeResult(cb, err, res, body) 124 | }) 125 | }, 126 | }, (err, results) => { 127 | try 128 | { 129 | if (err) { 130 | throw err 131 | } 132 | 133 | if (!results.info.data || results.info.data.length != 1) 134 | throw 'failed to read information' 135 | 136 | var song = results.info.data[0] 137 | var lrc = results.lyric 138 | 139 | var singer = [] 140 | 141 | song.singer.forEach(function(e) { 142 | singer.push({ 143 | name: e.name, 144 | id: e.mid, 145 | href: 'https://y.qq.com/n/yqq/singer/' + e.mid + '.html', 146 | }) 147 | }); 148 | 149 | var result = { 150 | source: 'qq', 151 | id: song.mid, 152 | href: 'https://y.qq.com/n/yqq/song/' + song.mid + '.html', 153 | name: song.name, 154 | singer: singer, 155 | lrc: lrc.lyric && new Buffer(lrc.lyric, 'base64').toString() || null, 156 | tlrc: lrc.trans && new Buffer(lrc.trans, 'base64').toString() || null, 157 | nolyric: false, 158 | album: { 159 | name: song.album.name, 160 | id: song.album.mid, 161 | href: 'https://y.qq.com/n/yqq/album/' + song.album.mid + '.html', 162 | }, 163 | } 164 | 165 | if (result.lrc && result.lrc.indexOf('[00:00:00]此歌曲为没有填词的纯音乐') != -1) { 166 | result.nolyric = true; 167 | result.lrc = null; 168 | result.tlrc = null; 169 | } 170 | 171 | if (options.extract == true) { 172 | var ret = lyric.extractLyrics(result.lrc, result.tlrc, exports.config.lyricsExtract); 173 | result.lrc = ret[0]; 174 | result.tlrc = ret[1]; 175 | } 176 | 177 | response(common.makeLyricResponseData([ result ])); 178 | } catch (e) { 179 | response(common.makeFailedData(e)); 180 | } 181 | }) 182 | } 183 | 184 | function tryConvertIDToMID(options, response) { 185 | var log = options.log || logger.createLog('debug');; 186 | 187 | if (/^\d+_num/.test(options.id)) { 188 | var reqOptions = { 189 | host: 'y.qq.com', 190 | path: `/n/yqq/song/${options.id}.html`, 191 | method: 'GET', 192 | headers: { 193 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 194 | }, 195 | }; 196 | 197 | log('REQUEST ' + reqOptions.host + reqOptions.path); 198 | 199 | request.http_request(reqOptions, (err, res, body) => { 200 | try { 201 | common.checkHttpResponseError(err, res); 202 | 203 | var m = body.match(/"songmid":"(\w+)",/) 204 | 205 | if (m && m.length == 2) { 206 | options.id = m[1] 207 | doDownload(options, response) 208 | } else { 209 | throw 'failed to match mid' 210 | } 211 | } catch (e) { 212 | response(common.makeFailedData(e)) 213 | } 214 | }) 215 | } else { 216 | doDownload(options, response) 217 | } 218 | } 219 | 220 | exports.download = tryConvertIDToMID 221 | 222 | exports.search = function (options, response) { 223 | var name = options.name; 224 | var log = options.log || logger.createLog('debug'); 225 | 226 | var page = Math.floor(options.page || 1) 227 | 228 | var query = { 229 | remoteplace:'txt.yqq.song', 230 | t:0, 231 | aggr:1, 232 | cr:1, 233 | catZhida:1, 234 | lossless:0, 235 | flag_qc:0, 236 | p:page, 237 | n:exports.config.searchNums, 238 | w:name, 239 | callback: 'searchCallbacksong8927', 240 | pcachetime: new Date().getTime(), 241 | g_tk:5381, 242 | jsonpCallback: 'searchCallbacksong8927', 243 | loginUin:0, 244 | hostUin:0, 245 | format:'jsonp', 246 | inCharset:'utf8', 247 | outCharset:'utf-8', 248 | notice:0, 249 | platform:'yqq', 250 | needNewCode: 0, 251 | } 252 | 253 | async.parallel({ 254 | search: (cb) => { 255 | var reqOptions = { 256 | host: 'c.y.qq.com', 257 | path: '/soso/fcgi-bin/search_cp?' + querystring.stringify(query), 258 | method: 'GET', 259 | headers: { 260 | 'Referer': 'https://y.qq.com/portal/search.html', 261 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 262 | }, 263 | }; 264 | 265 | log('REQUEST ' + reqOptions.host + reqOptions.path); 266 | 267 | request.http_request(reqOptions, (err, res, body) => { 268 | invokeResult(cb, err, res, body) 269 | }) 270 | } 271 | }, (err, results) => { 272 | try 273 | { 274 | if (err) { 275 | throw err 276 | } 277 | 278 | var songs = results.search.data.song 279 | 280 | if (!songs || !songs.list) 281 | throw 'failed to read information' 282 | 283 | var ret = [] 284 | 285 | songs.list.forEach(function(e) { 286 | if (!e.strMediaMid) 287 | return 288 | 289 | var singler = [] 290 | 291 | e.singer.forEach(function(e) { 292 | singler.push({ 293 | id: e.mid, 294 | name: HTMLDecoder.decode(e.name), 295 | href: `https://y.qq.com/portal/singer/${e.mid}.html`, 296 | }) 297 | }) 298 | 299 | ret.push({ 300 | name: HTMLDecoder.decode(e.songname), 301 | href: `https://y.qq.com/portal/song/${e.strMediaMid}.html`, 302 | id: e.strMediaMid, 303 | singler: singler, 304 | album: { 305 | id: e.albummid, 306 | name: HTMLDecoder.decode(e.albumname), 307 | href: `https://y.qq.com/portal/album/${e.albummid}.html`, 308 | } 309 | }) 310 | }) 311 | 312 | 313 | var totalCount = results.search.data.song.totalnum; 314 | 315 | response(common.makeSearchResponseData(ret, { 316 | page: page, 317 | pagetotal: Math.floor(totalCount / exports.config.searchNums) + 1, 318 | total: totalCount, 319 | })); 320 | } catch (e) { 321 | response(common.makeFailedData(e)) 322 | } 323 | }) 324 | } -------------------------------------------------------------------------------- /lib/core/loader/xiami.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var common = require('../common'); 3 | var querystring = require('querystring'); 4 | var request = require('../request'); 5 | var HTMLDecoder = require("html-decoder"); 6 | var url = require('url'); 7 | var lyric = require('../lyric'); 8 | var cheerio = null; 9 | var logger = require('../logger'); 10 | 11 | exports.config = { 12 | name : 'xiami', 13 | regHost : /^(www\.)?xiami\.com$/, 14 | regID: [ 'pathname', /\/\w+\/(.*)$/ ], 15 | searchNums: 20, 16 | lyricsExtract : { 17 | reDeleteLine: /(作词:|作曲:|编曲:)/, 18 | } 19 | } 20 | 21 | exports.download = function (options, response) { 22 | var id = options.id; 23 | var log = options.log || logger.createLog('debug'); 24 | 25 | async.waterfall([ 26 | // load song info page, for read song id 27 | function (callback) { 28 | var reqOptions = { 29 | host: 'www.xiami.com', 30 | path: '/song/' + id, 31 | method: 'GET', 32 | headers: { 33 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 34 | }, 35 | }; 36 | 37 | log('REQUEST ' + reqOptions.host + reqOptions.path); 38 | 39 | request.http_request(reqOptions, (err, res, body) => { 40 | var m; 41 | 42 | try { 43 | common.checkHttpResponseError(err, res); 44 | 45 | m = body.match(/http:\/\/www.xiami.com\/play\?ids=\/song\/playlist\/id\/(\d+)/); 46 | 47 | if (!m || m.length != 2) { 48 | throw 'failed to match song id'; 49 | } 50 | } catch (e) { 51 | return callback(e); 52 | } 53 | 54 | callback(null, m[1]); 55 | }) 56 | }, 57 | // load song info from song id 58 | function (songid, callback) { 59 | var reqOptions = { 60 | host: 'www.xiami.com', 61 | path: '/song/playlist/id/' + songid + '/object_name/default/object_id/0/cat/json', 62 | method: 'GET', 63 | headers: { 64 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 65 | }, 66 | }; 67 | 68 | log('REQUEST ' + reqOptions.host + reqOptions.path); 69 | 70 | request.http_request(reqOptions, (err, res, body) => { 71 | try { 72 | common.checkHttpResponseError(err, res); 73 | } catch (e) { 74 | callback(e); 75 | return; 76 | } 77 | callback(null, body); 78 | }) 79 | }, 80 | // load lyrics from song info if exists 81 | function (infoJson, callback) { 82 | try { 83 | var info = JSON.parse(infoJson) 84 | 85 | if (!info.status) 86 | throw 'response json data not readly'; 87 | 88 | var songinfo = (info.data && info.data.trackList && info.data.trackList[0]) ? info.data.trackList[0] : null; 89 | 90 | if (!songinfo) 91 | throw 'no song data'; 92 | 93 | if (!songinfo.lyricInfo || !songinfo.lyricInfo.lyricFile) { // no lyrics 94 | callback(null, songinfo, null); 95 | return; 96 | } 97 | 98 | var urlinfo = url.parse(songinfo.lyricInfo.lyricFile) 99 | 100 | var reqOptions = { 101 | host: urlinfo.host, 102 | path: urlinfo.path, 103 | method: 'GET', 104 | headers: { 105 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 106 | }, 107 | }; 108 | 109 | log('REQUEST ' + reqOptions.host + reqOptions.path); 110 | 111 | request.http_request(reqOptions, (err, res, body) => { 112 | try { 113 | common.checkHttpResponseError(err, res) 114 | } catch (e) { 115 | callback(e); 116 | return; 117 | } 118 | 119 | callback(null, songinfo, body); 120 | }) 121 | } catch (e) { 122 | callback(e); 123 | } 124 | }, 125 | function (song, lyricdata, callback) { 126 | var lyricLines; 127 | var original; 128 | var translate; 129 | 130 | if (lyricdata) { 131 | lyricLines = lyricdata.split('\n'); 132 | 133 | original = []; 134 | 135 | for(var i in lyricLines) { 136 | var line = lyricLines[i].trim(); 137 | // it's translate line ? 138 | if (line.startsWith('[x-trans]')) { 139 | if (!translate) { 140 | translate = original.slice(); // copy array 141 | translate.pop(); // remove last one 142 | } 143 | translate.push(line.replace('[x-trans]', original[original.length - 1].match(/\[.+\]/)[0])); 144 | } else { 145 | original.push(line.replace(/<\d+>/g, '')); 146 | } 147 | } 148 | } 149 | 150 | var singer = []; 151 | 152 | for (var i in song.singersSource) { 153 | singer.push({ 154 | name: song.singersSource[i].artistName, 155 | id: song.singersSource[i].artistStringId, 156 | href: 'http://www.xiami.com/artist/' + song.singersSource[i].artistStringId, 157 | }); 158 | } 159 | 160 | if (options.extract == true) { 161 | var ret = lyric.extractLyrics(original, translate, exports.config.lyricsExtract); 162 | original = ret[0]; 163 | translate = ret[1]; 164 | } else { 165 | if (original) 166 | original = original.join('\n'); 167 | if (translate) 168 | translate = translate.join('\n'); 169 | } 170 | 171 | var result = { 172 | source: 'xiami', 173 | id: song.songStringId, 174 | href: 'http://www.xiami.com/song/' + song.songStringId, 175 | name: song.songName, 176 | tname: song.subName, 177 | singer: singer, 178 | lrc: original || null, 179 | tlrc: translate || null, 180 | album: { 181 | name: song.album_name, 182 | id: song.albumStringId, 183 | href: 'http://www.xiami.com/album/' + song.albumStringId, 184 | }, 185 | }; 186 | 187 | callback(null, common.makeLyricResponseData([ result ])); 188 | } 189 | ], (err, result) => { 190 | if (err) { 191 | response(common.makeFailedData(err)); 192 | } else { 193 | response(result); 194 | } 195 | }) 196 | } 197 | 198 | exports.search = function (options, response) { 199 | cheerio = cheerio || require('cheerio'); // load if need 200 | 201 | var name = options.name; 202 | var log = options.log || logger.createLog('debug'); 203 | 204 | var page = Math.floor(options.page || 1) 205 | 206 | var query = { 207 | key: name, 208 | category:-1, 209 | } 210 | 211 | var reqOptions = { 212 | host: 'www.xiami.com', 213 | path: '/search/song/page/' + page + '?' + querystring.stringify(query), 214 | method: 'GET', 215 | headers: { 216 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36,' 217 | }, 218 | }; 219 | 220 | log('REQUEST ' + reqOptions.host + reqOptions.path); 221 | 222 | request.http_request(reqOptions, (err, res, body) => { 223 | try { 224 | common.checkHttpResponseError(err, res); 225 | } catch (e) { 226 | response(common.makeFailedData(e)); 227 | return; 228 | } 229 | 230 | const $ = cheerio.load(body); 231 | 232 | var totalCount = $('.seek_counts b')[0].children[0].data 233 | 234 | var trackList = $('.track_list tbody tr'); 235 | 236 | var nameList = trackList.find('.song_name'); 237 | var artistList = trackList.find('.song_artist'); 238 | var albumList = trackList.find('.song_album'); 239 | 240 | if (nameList.length != artistList.length || artistList.length != albumList.length) { 241 | response(common.makeFailedData('search results amount error')); 242 | return; 243 | } 244 | 245 | var results = []; 246 | 247 | for (var i = 0; nameList.length != i; i++) { 248 | var info = {}; 249 | 250 | for (var j = 0; nameList[i].children.length != j; j++) { 251 | var n = nameList[i].children[j]; 252 | 253 | if (n.type != 'tag' || n.name != 'a') 254 | continue; 255 | 256 | if (!info.name && (n.attribs.href.indexOf('http://') != -1)) { 257 | info.name = n.attribs.title; 258 | info.href = n.attribs.href; 259 | info.id = n.attribs.href.match(/\/\w+\/(.*)$/)[1]; 260 | break; 261 | } 262 | } 263 | 264 | for (var j = 0; albumList[i].children.length != j; j++) { 265 | var n = albumList[i].children[j]; 266 | 267 | if (n.type != 'tag' || n.name != 'a') 268 | continue; 269 | 270 | if (!info.album && (n.attribs.href.indexOf('http://') != -1)) { 271 | info.album = { 272 | name: n.attribs.title, 273 | href: n.attribs.href, 274 | id: n.attribs.href.match(/\/\w+\/(.*)$/)[1], 275 | }; 276 | break; 277 | } 278 | } 279 | 280 | var singler = []; 281 | 282 | for (var j = 0; artistList[i].children.length != j; j++) { 283 | var n = artistList[i].children[j]; 284 | 285 | if (n.type != 'tag' || n.name != 'a') 286 | continue; 287 | 288 | singler.push({ 289 | name: n.children[0].data ? n.children[0].data.trim() : n.attribs.title, 290 | href: n.attribs.href, 291 | iid: n.attribs.href.match(/\/\w+\/(.*)$/)[1], 292 | }); 293 | } 294 | 295 | info.singler = singler; 296 | 297 | results.push(info); 298 | } 299 | 300 | response(common.makeSearchResponseData(results, { 301 | page: page, 302 | pagetotal: Math.floor(parseInt(totalCount) / exports.config.searchNums) + 1, 303 | total: parseInt(totalCount), 304 | })); 305 | }) 306 | } 307 | 308 | -------------------------------------------------------------------------------- /lib/core/logger.js: -------------------------------------------------------------------------------- 1 | var createDebug = require('debug'); 2 | 3 | function fakeLog() { } 4 | 5 | exports.createLog = function(level, namespace) { 6 | var logLevel = level; 7 | 8 | if (logLevel == 'error') { 9 | return function(content) { 10 | console.error(content); 11 | } 12 | } 13 | 14 | if (logLevel == 'debug') { 15 | return createDebug(namespace || 'LYRIC-DL'); 16 | } 17 | 18 | if (logLevel == 'fake') { 19 | return fakeLog; 20 | } 21 | 22 | throw 'invalid logger type: ' + level; 23 | } -------------------------------------------------------------------------------- /lib/core/lyric.js: -------------------------------------------------------------------------------- 1 | function removeEmptyFromHead(list) { 2 | var i = 0 3 | 4 | for(var j = 0; j != list.length; j++) { 5 | if (list[j][1] == '') { 6 | i = j + 1 7 | } else { 8 | break 9 | } 10 | } 11 | 12 | if (i != 0) { 13 | list.splice(0, i) 14 | } 15 | } 16 | 17 | function toLines(lyric) { 18 | if (!lyric) { 19 | return null; 20 | } 21 | 22 | if (Array.isArray(lyric)) { 23 | return lyric.slice(); 24 | } else { 25 | return lyric.split('\n'); 26 | } 27 | } 28 | 29 | function doExtract(lyric, options) { 30 | var ret = [] 31 | 32 | var lines = toLines(lyric); 33 | 34 | var lastContent = null; 35 | 36 | for(var i in lines) { 37 | if (!options.deleteLineLimit || (i <= options.deleteLineLimit)) { 38 | if (options.reDeleteLine.test(lines[i])) 39 | continue; 40 | } 41 | 42 | var m = lines[i].match(/\[(\d+:\d+\.\d+)\](.*)/) 43 | 44 | if (m && m.length == 3) { 45 | var content 46 | if (options.reDeleteWord) { 47 | ret.push([m[1], m[2].trim().replace(options.reDeleteWord, '')]); 48 | } else { 49 | ret.push([m[1], m[2].trim()]); 50 | } 51 | } 52 | } 53 | 54 | return ret 55 | } 56 | 57 | exports.extractLyrics = function(lyric, tlyric, options) { 58 | return [doExtract(lyric, options), doExtract(tlyric, options)]; 59 | } -------------------------------------------------------------------------------- /lib/core/request.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var querystring = require('querystring') 3 | 4 | exports.http_request = function (options, response, postData){ 5 | var req = http.request(options, function(res) { 6 | var chunklist = [] 7 | res.on('data', (chunk) => { 8 | chunklist.push(chunk) 9 | }); 10 | res.on('end', () => { 11 | var size = 0 12 | chunklist.forEach((e) => { 13 | size += e.length 14 | }) 15 | var buffer = new Buffer(size) 16 | var pos = 0 17 | chunklist.forEach((e) => { 18 | e.copy(buffer, pos) 19 | pos += e.length 20 | }) 21 | response(null, res, buffer.toString()) 22 | }); 23 | }) 24 | 25 | req.on('error', (err) => { 26 | console.error(err.message) 27 | response(err.message, null, null) 28 | }) 29 | 30 | if (postData) { 31 | req.write(postData) 32 | } 33 | 34 | req.end() 35 | } -------------------------------------------------------------------------------- /lib/core/rsa.js: -------------------------------------------------------------------------------- 1 | // ---------- BigInt.js copied ---------- 2 | 3 | // BigInt, a suite of routines for performing multiple-precision arithmetic in 4 | // JavaScript. 5 | // 6 | // Copyright 1998-2005 David Shapiro. 7 | // 8 | // You may use, re-use, abuse, 9 | // copy, and modify this code to your liking, but please keep this header. 10 | // Thanks! 11 | // 12 | // Dave Shapiro 13 | // dave@ohdave.com 14 | 15 | // IMPORTANT THING: Be sure to set maxDigits according to your precision 16 | // needs. Use the setMaxDigits() function to do this. See comments below. 17 | // 18 | // Tweaked by Ian Bunning 19 | // Alterations: 20 | // Fix bug in function biFromHex(s) to allow 21 | // parsing of strings of length != 0 (mod 4) 22 | 23 | // Changes made by Dave Shapiro as of 12/30/2004: 24 | // 25 | // The BigInt() constructor doesn't take a string anymore. If you want to 26 | // create a BigInt from a string, use biFromDecimal() for base-10 27 | // representations, biFromHex() for base-16 representations, or 28 | // biFromString() for base-2-to-36 representations. 29 | // 30 | // biFromArray() has been removed. Use biCopy() instead, passing a BigInt 31 | // instead of an array. 32 | // 33 | // The BigInt() constructor now only constructs a zeroed-out array. 34 | // Alternatively, if you pass , it won't construct any array. See the 35 | // biCopy() method for an example of this. 36 | // 37 | // Be sure to set maxDigits depending on your precision needs. The default 38 | // zeroed-out array ZERO_ARRAY is constructed inside the setMaxDigits() 39 | // function. So use this function to set the variable. DON'T JUST SET THE 40 | // VALUE. USE THE FUNCTION. 41 | // 42 | // ZERO_ARRAY exists to hopefully speed up construction of BigInts(). By 43 | // precalculating the zero array, we can just use slice(0) to make copies of 44 | // it. Presumably this calls faster native code, as opposed to setting the 45 | // elements one at a time. I have not done any timing tests to verify this 46 | // claim. 47 | 48 | // Max number = 10^16 - 2 = 9999999999999998; 49 | // 2^53 = 9007199254740992; 50 | 51 | var biRadixBase = 2; 52 | var biRadixBits = 16; 53 | var bitsPerDigit = biRadixBits; 54 | var biRadix = 1 << 16; // = 2^16 = 65536 55 | var biHalfRadix = biRadix >>> 1; 56 | var biRadixSquared = biRadix * biRadix; 57 | var maxDigitVal = biRadix - 1; 58 | var maxInteger = 9999999999999998; 59 | 60 | // maxDigits: 61 | // Change this to accommodate your largest number size. Use setMaxDigits() 62 | // to change it! 63 | // 64 | // In general, if you're working with numbers of size N bits, you'll need 2*N 65 | // bits of storage. Each digit holds 16 bits. So, a 1024-bit key will need 66 | // 67 | // 1024 * 2 / 16 = 128 digits of storage. 68 | // 69 | 70 | var maxDigits; 71 | var ZERO_ARRAY; 72 | var bigZero, bigOne; 73 | 74 | function setMaxDigits(value) 75 | { 76 | maxDigits = value; 77 | ZERO_ARRAY = new Array(maxDigits); 78 | for (var iza = 0; iza < ZERO_ARRAY.length; iza++) ZERO_ARRAY[iza] = 0; 79 | bigZero = new BigInt(); 80 | bigOne = new BigInt(); 81 | bigOne.digits[0] = 1; 82 | } 83 | 84 | setMaxDigits(20); 85 | 86 | // The maximum number of digits in base 10 you can convert to an 87 | // integer without JavaScript throwing up on you. 88 | var dpl10 = 15; 89 | // lr10 = 10 ^ dpl10 90 | var lr10 = biFromNumber(1000000000000000); 91 | 92 | function BigInt(flag) 93 | { 94 | if (typeof flag == "boolean" && flag == true) { 95 | this.digits = null; 96 | } 97 | else { 98 | this.digits = ZERO_ARRAY.slice(0); 99 | } 100 | this.isNeg = false; 101 | } 102 | 103 | function biFromDecimal(s) 104 | { 105 | var isNeg = s.charAt(0) == '-'; 106 | var i = isNeg ? 1 : 0; 107 | var result; 108 | // Skip leading zeros. 109 | while (i < s.length && s.charAt(i) == '0') ++i; 110 | if (i == s.length) { 111 | result = new BigInt(); 112 | } 113 | else { 114 | var digitCount = s.length - i; 115 | var fgl = digitCount % dpl10; 116 | if (fgl == 0) fgl = dpl10; 117 | result = biFromNumber(Number(s.substr(i, fgl))); 118 | i += fgl; 119 | while (i < s.length) { 120 | result = biAdd(biMultiply(result, lr10), 121 | biFromNumber(Number(s.substr(i, dpl10)))); 122 | i += dpl10; 123 | } 124 | result.isNeg = isNeg; 125 | } 126 | return result; 127 | } 128 | 129 | function biCopy(bi) 130 | { 131 | var result = new BigInt(true); 132 | result.digits = bi.digits.slice(0); 133 | result.isNeg = bi.isNeg; 134 | return result; 135 | } 136 | 137 | function biFromNumber(i) 138 | { 139 | var result = new BigInt(); 140 | result.isNeg = i < 0; 141 | i = Math.abs(i); 142 | var j = 0; 143 | while (i > 0) { 144 | result.digits[j++] = i & maxDigitVal; 145 | i >>= biRadixBits; 146 | } 147 | return result; 148 | } 149 | 150 | function reverseStr(s) 151 | { 152 | var result = ""; 153 | for (var i = s.length - 1; i > -1; --i) { 154 | result += s.charAt(i); 155 | } 156 | return result; 157 | } 158 | 159 | var hexatrigesimalToChar = new Array( 160 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 161 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 162 | 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 163 | 'u', 'v', 'w', 'x', 'y', 'z' 164 | ); 165 | 166 | function biToString(x, radix) 167 | // 2 <= radix <= 36 168 | { 169 | var b = new BigInt(); 170 | b.digits[0] = radix; 171 | var qr = biDivideModulo(x, b); 172 | var result = hexatrigesimalToChar[qr[1].digits[0]]; 173 | while (biCompare(qr[0], bigZero) == 1) { 174 | qr = biDivideModulo(qr[0], b); 175 | digit = qr[1].digits[0]; 176 | result += hexatrigesimalToChar[qr[1].digits[0]]; 177 | } 178 | return (x.isNeg ? "-" : "") + reverseStr(result); 179 | } 180 | 181 | function biToDecimal(x) 182 | { 183 | var b = new BigInt(); 184 | b.digits[0] = 10; 185 | var qr = biDivideModulo(x, b); 186 | var result = String(qr[1].digits[0]); 187 | while (biCompare(qr[0], bigZero) == 1) { 188 | qr = biDivideModulo(qr[0], b); 189 | result += String(qr[1].digits[0]); 190 | } 191 | return (x.isNeg ? "-" : "") + reverseStr(result); 192 | } 193 | 194 | var hexToChar = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 195 | 'a', 'b', 'c', 'd', 'e', 'f'); 196 | 197 | function digitToHex(n) 198 | { 199 | var mask = 0xf; 200 | var result = ""; 201 | for (i = 0; i < 4; ++i) { 202 | result += hexToChar[n & mask]; 203 | n >>>= 4; 204 | } 205 | return reverseStr(result); 206 | } 207 | 208 | function biToHex(x) 209 | { 210 | var result = ""; 211 | var n = biHighIndex(x); 212 | for (var i = biHighIndex(x); i > -1; --i) { 213 | result += digitToHex(x.digits[i]); 214 | } 215 | return result; 216 | } 217 | 218 | function charToHex(c) 219 | { 220 | var ZERO = 48; 221 | var NINE = ZERO + 9; 222 | var littleA = 97; 223 | var littleZ = littleA + 25; 224 | var bigA = 65; 225 | var bigZ = 65 + 25; 226 | var result; 227 | 228 | if (c >= ZERO && c <= NINE) { 229 | result = c - ZERO; 230 | } else if (c >= bigA && c <= bigZ) { 231 | result = 10 + c - bigA; 232 | } else if (c >= littleA && c <= littleZ) { 233 | result = 10 + c - littleA; 234 | } else { 235 | result = 0; 236 | } 237 | return result; 238 | } 239 | 240 | function hexToDigit(s) 241 | { 242 | var result = 0; 243 | var sl = Math.min(s.length, 4); 244 | for (var i = 0; i < sl; ++i) { 245 | result <<= 4; 246 | result |= charToHex(s.charCodeAt(i)) 247 | } 248 | return result; 249 | } 250 | 251 | function biFromHex(s) 252 | { 253 | var result = new BigInt(); 254 | var sl = s.length; 255 | for (var i = sl, j = 0; i > 0; i -= 4, ++j) { 256 | result.digits[j] = hexToDigit(s.substr(Math.max(i - 4, 0), Math.min(i, 4))); 257 | } 258 | return result; 259 | } 260 | 261 | function biFromString(s, radix) 262 | { 263 | var isNeg = s.charAt(0) == '-'; 264 | var istop = isNeg ? 1 : 0; 265 | var result = new BigInt(); 266 | var place = new BigInt(); 267 | place.digits[0] = 1; // radix^0 268 | for (var i = s.length - 1; i >= istop; i--) { 269 | var c = s.charCodeAt(i); 270 | var digit = charToHex(c); 271 | var biDigit = biMultiplyDigit(place, digit); 272 | result = biAdd(result, biDigit); 273 | place = biMultiplyDigit(place, radix); 274 | } 275 | result.isNeg = isNeg; 276 | return result; 277 | } 278 | 279 | function biToBytes(x) 280 | // Returns a string containing raw bytes. 281 | { 282 | var result = ""; 283 | for (var i = biHighIndex(x); i > -1; --i) { 284 | result += digitToBytes(x.digits[i]); 285 | } 286 | return result; 287 | } 288 | 289 | function digitToBytes(n) 290 | // Convert two-byte digit to string containing both bytes. 291 | { 292 | var c1 = String.fromCharCode(n & 0xff); 293 | n >>>= 8; 294 | var c2 = String.fromCharCode(n & 0xff); 295 | return c2 + c1; 296 | } 297 | 298 | function biDump(b) 299 | { 300 | return (b.isNeg ? "-" : "") + b.digits.join(" "); 301 | } 302 | 303 | function biAdd(x, y) 304 | { 305 | var result; 306 | 307 | if (x.isNeg != y.isNeg) { 308 | y.isNeg = !y.isNeg; 309 | result = biSubtract(x, y); 310 | y.isNeg = !y.isNeg; 311 | } 312 | else { 313 | result = new BigInt(); 314 | var c = 0; 315 | var n; 316 | for (var i = 0; i < x.digits.length; ++i) { 317 | n = x.digits[i] + y.digits[i] + c; 318 | result.digits[i] = n & 0xffff; 319 | c = Number(n >= biRadix); 320 | } 321 | result.isNeg = x.isNeg; 322 | } 323 | return result; 324 | } 325 | 326 | function biSubtract(x, y) 327 | { 328 | var result; 329 | if (x.isNeg != y.isNeg) { 330 | y.isNeg = !y.isNeg; 331 | result = biAdd(x, y); 332 | y.isNeg = !y.isNeg; 333 | } else { 334 | result = new BigInt(); 335 | var n, c; 336 | c = 0; 337 | for (var i = 0; i < x.digits.length; ++i) { 338 | n = x.digits[i] - y.digits[i] + c; 339 | result.digits[i] = n & 0xffff; 340 | // Stupid non-conforming modulus operation. 341 | if (result.digits[i] < 0) result.digits[i] += biRadix; 342 | c = 0 - Number(n < 0); 343 | } 344 | // Fix up the negative sign, if any. 345 | if (c == -1) { 346 | c = 0; 347 | for (var i = 0; i < x.digits.length; ++i) { 348 | n = 0 - result.digits[i] + c; 349 | result.digits[i] = n & 0xffff; 350 | // Stupid non-conforming modulus operation. 351 | if (result.digits[i] < 0) result.digits[i] += biRadix; 352 | c = 0 - Number(n < 0); 353 | } 354 | // Result is opposite sign of arguments. 355 | result.isNeg = !x.isNeg; 356 | } else { 357 | // Result is same sign. 358 | result.isNeg = x.isNeg; 359 | } 360 | } 361 | return result; 362 | } 363 | 364 | function biHighIndex(x) 365 | { 366 | var result = x.digits.length - 1; 367 | while (result > 0 && x.digits[result] == 0) --result; 368 | return result; 369 | } 370 | 371 | function biNumBits(x) 372 | { 373 | var n = biHighIndex(x); 374 | var d = x.digits[n]; 375 | var m = (n + 1) * bitsPerDigit; 376 | var result; 377 | for (result = m; result > m - bitsPerDigit; --result) { 378 | if ((d & 0x8000) != 0) break; 379 | d <<= 1; 380 | } 381 | return result; 382 | } 383 | 384 | function biMultiply(x, y) 385 | { 386 | var result = new BigInt(); 387 | var c; 388 | var n = biHighIndex(x); 389 | var t = biHighIndex(y); 390 | var u, uv, k; 391 | 392 | for (var i = 0; i <= t; ++i) { 393 | c = 0; 394 | k = i; 395 | for (j = 0; j <= n; ++j, ++k) { 396 | uv = result.digits[k] + x.digits[j] * y.digits[i] + c; 397 | result.digits[k] = uv & maxDigitVal; 398 | c = uv >>> biRadixBits; 399 | } 400 | result.digits[i + n + 1] = c; 401 | } 402 | // Someone give me a logical xor, please. 403 | result.isNeg = x.isNeg != y.isNeg; 404 | return result; 405 | } 406 | 407 | function biMultiplyDigit(x, y) 408 | { 409 | var n, c, uv; 410 | 411 | result = new BigInt(); 412 | n = biHighIndex(x); 413 | c = 0; 414 | for (var j = 0; j <= n; ++j) { 415 | uv = result.digits[j] + x.digits[j] * y + c; 416 | result.digits[j] = uv & maxDigitVal; 417 | c = uv >>> biRadixBits; 418 | } 419 | result.digits[1 + n] = c; 420 | return result; 421 | } 422 | 423 | function arrayCopy(src, srcStart, dest, destStart, n) 424 | { 425 | var m = Math.min(srcStart + n, src.length); 426 | for (var i = srcStart, j = destStart; i < m; ++i, ++j) { 427 | dest[j] = src[i]; 428 | } 429 | } 430 | 431 | var highBitMasks = new Array(0x0000, 0x8000, 0xC000, 0xE000, 0xF000, 0xF800, 432 | 0xFC00, 0xFE00, 0xFF00, 0xFF80, 0xFFC0, 0xFFE0, 433 | 0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF); 434 | 435 | function biShiftLeft(x, n) 436 | { 437 | var digitCount = Math.floor(n / bitsPerDigit); 438 | var result = new BigInt(); 439 | arrayCopy(x.digits, 0, result.digits, digitCount, 440 | result.digits.length - digitCount); 441 | var bits = n % bitsPerDigit; 442 | var rightBits = bitsPerDigit - bits; 443 | for (var i = result.digits.length - 1, i1 = i - 1; i > 0; --i, --i1) { 444 | result.digits[i] = ((result.digits[i] << bits) & maxDigitVal) | 445 | ((result.digits[i1] & highBitMasks[bits]) >>> 446 | (rightBits)); 447 | } 448 | result.digits[0] = ((result.digits[i] << bits) & maxDigitVal); 449 | result.isNeg = x.isNeg; 450 | return result; 451 | } 452 | 453 | var lowBitMasks = new Array(0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 454 | 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 455 | 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF); 456 | 457 | function biShiftRight(x, n) 458 | { 459 | var digitCount = Math.floor(n / bitsPerDigit); 460 | var result = new BigInt(); 461 | arrayCopy(x.digits, digitCount, result.digits, 0, 462 | x.digits.length - digitCount); 463 | var bits = n % bitsPerDigit; 464 | var leftBits = bitsPerDigit - bits; 465 | for (var i = 0, i1 = i + 1; i < result.digits.length - 1; ++i, ++i1) { 466 | result.digits[i] = (result.digits[i] >>> bits) | 467 | ((result.digits[i1] & lowBitMasks[bits]) << leftBits); 468 | } 469 | result.digits[result.digits.length - 1] >>>= bits; 470 | result.isNeg = x.isNeg; 471 | return result; 472 | } 473 | 474 | function biMultiplyByRadixPower(x, n) 475 | { 476 | var result = new BigInt(); 477 | arrayCopy(x.digits, 0, result.digits, n, result.digits.length - n); 478 | return result; 479 | } 480 | 481 | function biDivideByRadixPower(x, n) 482 | { 483 | var result = new BigInt(); 484 | arrayCopy(x.digits, n, result.digits, 0, result.digits.length - n); 485 | return result; 486 | } 487 | 488 | function biModuloByRadixPower(x, n) 489 | { 490 | var result = new BigInt(); 491 | arrayCopy(x.digits, 0, result.digits, 0, n); 492 | return result; 493 | } 494 | 495 | function biCompare(x, y) 496 | { 497 | if (x.isNeg != y.isNeg) { 498 | return 1 - 2 * Number(x.isNeg); 499 | } 500 | for (var i = x.digits.length - 1; i >= 0; --i) { 501 | if (x.digits[i] != y.digits[i]) { 502 | if (x.isNeg) { 503 | return 1 - 2 * Number(x.digits[i] > y.digits[i]); 504 | } else { 505 | return 1 - 2 * Number(x.digits[i] < y.digits[i]); 506 | } 507 | } 508 | } 509 | return 0; 510 | } 511 | 512 | function biDivideModulo(x, y) 513 | { 514 | var nb = biNumBits(x); 515 | var tb = biNumBits(y); 516 | var origYIsNeg = y.isNeg; 517 | var q, r; 518 | if (nb < tb) { 519 | // |x| < |y| 520 | if (x.isNeg) { 521 | q = biCopy(bigOne); 522 | q.isNeg = !y.isNeg; 523 | x.isNeg = false; 524 | y.isNeg = false; 525 | r = biSubtract(y, x); 526 | // Restore signs, 'cause they're references. 527 | x.isNeg = true; 528 | y.isNeg = origYIsNeg; 529 | } else { 530 | q = new BigInt(); 531 | r = biCopy(x); 532 | } 533 | return new Array(q, r); 534 | } 535 | 536 | q = new BigInt(); 537 | r = x; 538 | 539 | // Normalize Y. 540 | var t = Math.ceil(tb / bitsPerDigit) - 1; 541 | var lambda = 0; 542 | while (y.digits[t] < biHalfRadix) { 543 | y = biShiftLeft(y, 1); 544 | ++lambda; 545 | ++tb; 546 | t = Math.ceil(tb / bitsPerDigit) - 1; 547 | } 548 | // Shift r over to keep the quotient constant. We'll shift the 549 | // remainder back at the end. 550 | r = biShiftLeft(r, lambda); 551 | nb += lambda; // Update the bit count for x. 552 | var n = Math.ceil(nb / bitsPerDigit) - 1; 553 | 554 | var b = biMultiplyByRadixPower(y, n - t); 555 | while (biCompare(r, b) != -1) { 556 | ++q.digits[n - t]; 557 | r = biSubtract(r, b); 558 | } 559 | for (var i = n; i > t; --i) { 560 | var ri = (i >= r.digits.length) ? 0 : r.digits[i]; 561 | var ri1 = (i - 1 >= r.digits.length) ? 0 : r.digits[i - 1]; 562 | var ri2 = (i - 2 >= r.digits.length) ? 0 : r.digits[i - 2]; 563 | var yt = (t >= y.digits.length) ? 0 : y.digits[t]; 564 | var yt1 = (t - 1 >= y.digits.length) ? 0 : y.digits[t - 1]; 565 | if (ri == yt) { 566 | q.digits[i - t - 1] = maxDigitVal; 567 | } else { 568 | q.digits[i - t - 1] = Math.floor((ri * biRadix + ri1) / yt); 569 | } 570 | 571 | var c1 = q.digits[i - t - 1] * ((yt * biRadix) + yt1); 572 | var c2 = (ri * biRadixSquared) + ((ri1 * biRadix) + ri2); 573 | while (c1 > c2) { 574 | --q.digits[i - t - 1]; 575 | c1 = q.digits[i - t - 1] * ((yt * biRadix) | yt1); 576 | c2 = (ri * biRadix * biRadix) + ((ri1 * biRadix) + ri2); 577 | } 578 | 579 | b = biMultiplyByRadixPower(y, i - t - 1); 580 | r = biSubtract(r, biMultiplyDigit(b, q.digits[i - t - 1])); 581 | if (r.isNeg) { 582 | r = biAdd(r, b); 583 | --q.digits[i - t - 1]; 584 | } 585 | } 586 | r = biShiftRight(r, lambda); 587 | // Fiddle with the signs and stuff to make sure that 0 <= r < y. 588 | q.isNeg = x.isNeg != origYIsNeg; 589 | if (x.isNeg) { 590 | if (origYIsNeg) { 591 | q = biAdd(q, bigOne); 592 | } else { 593 | q = biSubtract(q, bigOne); 594 | } 595 | y = biShiftRight(y, lambda); 596 | r = biSubtract(y, r); 597 | } 598 | // Check for the unbelievably stupid degenerate case of r == -0. 599 | if (r.digits[0] == 0 && biHighIndex(r) == 0) r.isNeg = false; 600 | 601 | return new Array(q, r); 602 | } 603 | 604 | function biDivide(x, y) 605 | { 606 | return biDivideModulo(x, y)[0]; 607 | } 608 | 609 | function biModulo(x, y) 610 | { 611 | return biDivideModulo(x, y)[1]; 612 | } 613 | 614 | function biMultiplyMod(x, y, m) 615 | { 616 | return biModulo(biMultiply(x, y), m); 617 | } 618 | 619 | function biPow(x, y) 620 | { 621 | var result = bigOne; 622 | var a = x; 623 | while (true) { 624 | if ((y & 1) != 0) result = biMultiply(result, a); 625 | y >>= 1; 626 | if (y == 0) break; 627 | a = biMultiply(a, a); 628 | } 629 | return result; 630 | } 631 | 632 | function biPowMod(x, y, m) 633 | { 634 | var result = bigOne; 635 | var a = x; 636 | var k = y; 637 | while (true) { 638 | if ((k.digits[0] & 1) != 0) result = biMultiplyMod(result, a, m); 639 | k = biShiftRight(k, 1); 640 | if (k.digits[0] == 0 && biHighIndex(k) == 0) break; 641 | a = biMultiplyMod(a, a, m); 642 | } 643 | return result; 644 | } 645 | 646 | // ---------- RSA.js copied ---------- 647 | 648 | /* 649 | * Copyright (c) 2015 Eric Wilde. 650 | * Copyright 1998-2015 David Shapiro. 651 | * 652 | * RSA.js is a suite of routines for performing RSA public-key computations 653 | * in JavaScript. The cryptographic functions herein are used for encoding 654 | * and decoding strings to be sent over unsecure channels. 655 | * 656 | * To use these routines, a pair of public/private keys is created through a 657 | * number of means (OpenSSL tools on Linux/Unix, Dave Shapiro's 658 | * RSAKeyGenerator program on Windows). These keys are passed to RSAKeyPair 659 | * as hexadecimal strings to create an encryption key object. This key object 660 | * is then used with encryptedString to encrypt blocks of plaintext using the 661 | * public key. The resulting cyphertext blocks can be decrypted with 662 | * decryptedString. 663 | * 664 | * Note that the cryptographic functions herein are complementary to those 665 | * found in CryptoFuncs.php and CryptoFuncs.pm. Hence, encrypted messages may 666 | * be sent between programs written in any of those languages. The most 667 | * useful, of course is to send messages encrypted by a Web page using RSA.js 668 | * to a PHP or Perl script running on a Web servitron. 669 | * 670 | * Also, the optional padding flag may be specified on the call to 671 | * encryptedString, in which case blocks of cyphertext that are compatible 672 | * with real crypto libraries such as OpenSSL or Microsoft will be created. 673 | * These blocks of cyphertext can then be sent to Web servitron that uses one 674 | * of these crypto libraries for decryption. This allows messages encrypted 675 | * with longer keys to be decrypted quickly on the Web server as well as 676 | * making for more secure communications when a padding algorithm such as 677 | * PKCS1v1.5 is used. 678 | * 679 | * These routines require BigInt.js and Barrett.js. 680 | */ 681 | 682 | /*****************************************************************************/ 683 | 684 | /* 685 | * Modifications 686 | * ------------- 687 | * 688 | * 2014 Jan 11 E. Wilde Add optional padding flag to encryptedString 689 | * for compatibility with real crypto libraries 690 | * such as OpenSSL or Microsoft. Add PKCS1v1.5 691 | * padding. 692 | * 693 | * 2015 Jan 5 D. Shapiro Add optional encoding flag for encryptedString 694 | * and encapsulate padding and encoding constants 695 | * in RSAAPP object. 696 | * 697 | * Original Code 698 | * ------------- 699 | * 700 | * Copyright 1998-2005 David Shapiro. 701 | * 702 | * You may use, re-use, abuse, copy, and modify this code to your liking, but 703 | * please keep this header. 704 | * 705 | * Thanks! 706 | * 707 | * Dave Shapiro 708 | * dave@ohdave.com 709 | */ 710 | 711 | /*****************************************************************************/ 712 | 713 | var RSAAPP = {}; 714 | 715 | RSAAPP.NoPadding = "NoPadding"; 716 | RSAAPP.PKCS1Padding = "PKCS1Padding"; 717 | RSAAPP.RawEncoding = "RawEncoding"; 718 | RSAAPP.NumericEncoding = "NumericEncoding" 719 | 720 | /*****************************************************************************/ 721 | 722 | function RSAKeyPair(encryptionExponent, decryptionExponent, modulus, keylen) 723 | /* 724 | * encryptionExponent The encryption exponent (i.e. public 725 | * encryption key) to be used for 726 | * encrypting messages. If you aren't 727 | * doing any encrypting, a dummy 728 | * exponent such as "10001" can be 729 | * passed. 730 | * 731 | * decryptionExponent The decryption exponent (i.e. private 732 | * decryption key) to be used for 733 | * decrypting messages. If you aren't 734 | * doing any decrypting, a dummy 735 | * exponent such as "10001" can be 736 | * passed. 737 | * 738 | * modulus The modulus to be used both for 739 | * encrypting and decrypting messages. 740 | * 741 | * keylen The optional length of the key, in 742 | * bits. If omitted, RSAKeyPair will 743 | * attempt to derive a key length (but, 744 | * see the notes below). 745 | * 746 | * returns The "new" object creator returns an 747 | * instance of a key object that can be 748 | * used to encrypt/decrypt messages. 749 | * 750 | * This routine is invoked as the first step in the encryption or decryption 751 | * process to take the three numbers (expressed as hexadecimal strings) that 752 | * are used for RSA asymmetric encryption/decryption and turn them into a key 753 | * object that can be used for encrypting and decrypting. 754 | * 755 | * The key object is created thusly: 756 | * 757 | * RSAKey = new RSAKeyPair("ABC12345", 10001, "987654FE"); 758 | * 759 | * or: 760 | * 761 | * RSAKey = new RSAKeyPair("ABC12345", 10001, "987654FE", 64); 762 | * 763 | * Note that RSAKeyPair will try to derive the length of the key that is being 764 | * used, from the key itself. The key length is especially useful when one of 765 | * the padding options is used and/or when the encrypted messages created by 766 | * the routine encryptedString are exchanged with a real crypto library such 767 | * as OpenSSL or Microsoft, as it determines how many padding characters are 768 | * appended. 769 | * 770 | * Usually, RSAKeyPair can determine the key length from the modulus of the 771 | * key but this doesn't always work properly, depending on the actual value of 772 | * the modulus. If you are exchanging messages with a real crypto library, 773 | * such as OpenSSL or Microsoft, that depends on the fact that the blocks 774 | * being passed to it are properly padded, you'll want the key length to be 775 | * set properly. If that's the case, of if you just want to be sure, you 776 | * should specify the key length that you used to generated the key, in bits 777 | * when this routine is invoked. 778 | */ 779 | { 780 | /* 781 | * Convert from hexadecimal and save the encryption/decryption exponents and 782 | * modulus as big integers in the key object. 783 | */ 784 | this.e = biFromHex(encryptionExponent); 785 | this.d = biFromHex(decryptionExponent); 786 | this.m = biFromHex(modulus); 787 | /* 788 | * Using big integers, we can represent two bytes per element in the big 789 | * integer array, so we calculate the chunk size as: 790 | * 791 | * chunkSize = 2 * (number of digits in modulus - 1) 792 | * 793 | * Since biHighIndex returns the high index, not the number of digits, the 794 | * number 1 has already been subtracted from its answer. 795 | * 796 | * However, having said all this, "User Knows Best". If our caller passes us 797 | * a key length (in bits), we'll treat it as gospel truth. 798 | */ 799 | if (typeof(keylen) != 'number') { this.chunkSize = 2 * biHighIndex(this.m); } 800 | else { this.chunkSize = keylen / 8; } 801 | 802 | this.radix = 16; 803 | /* 804 | * Precalculate the stuff used for Barrett modular reductions. 805 | */ 806 | this.barrett = new BarrettMu(this.m); 807 | } 808 | 809 | /*****************************************************************************/ 810 | 811 | function encryptedString(key, s, pad, encoding) 812 | /* 813 | * key The previously-built RSA key whose 814 | * public key component is to be used to 815 | * encrypt the plaintext string. 816 | * 817 | * s The plaintext string that is to be 818 | * encrypted, using the RSA assymmetric 819 | * encryption method. 820 | * 821 | * pad The optional padding method to use 822 | * when extending the plaintext to the 823 | * full chunk size required by the RSA 824 | * algorithm. To maintain compatibility 825 | * with other crypto libraries, the 826 | * padding method is described by a 827 | * string. The default, if not 828 | * specified is "OHDave". Here are the 829 | * choices: 830 | * 831 | * OHDave - this is the original 832 | * padding method employed by Dave 833 | * Shapiro and Rob Saunders. If 834 | * this method is chosen, the 835 | * plaintext can be of any length. 836 | * It will be padded to the correct 837 | * length with zeros and then broken 838 | * up into chunks of the correct 839 | * length before being encrypted. 840 | * The resultant cyphertext blocks 841 | * will be separated by blanks. 842 | * 843 | * Note that the original code by 844 | * Dave Shapiro reverses the byte 845 | * order to little-endian, as the 846 | * plaintext is encrypted. If 847 | * either these JavaScript routines 848 | * or one of the complementary 849 | * PHP/Perl routines derived from 850 | * this code is used for decryption, 851 | * the byte order will be reversed 852 | * again upon decryption so as to 853 | * come out correctly. 854 | * 855 | * Also note that this padding 856 | * method is claimed to be less 857 | * secure than PKCS1Padding. 858 | * 859 | * NoPadding - this method truncates 860 | * the plaintext to the length of 861 | * the RSA key, if it is longer. If 862 | * its length is shorter, it is 863 | * padded with zeros. In either 864 | * case, the plaintext string is 865 | * reversed to preserve big-endian 866 | * order before it is encrypted to 867 | * maintain compatibility with real 868 | * crypto libraries such as OpenSSL 869 | * or Microsoft. When the 870 | * cyphertext is to be decrypted 871 | * by a crypto library, the 872 | * library routine's RSAAPP.NoPadding 873 | * flag, or its equivalent, should 874 | * be used. 875 | * 876 | * Note that this padding method is 877 | * claimed to be less secure than 878 | * PKCS1Padding. 879 | * 880 | * PKCS1Padding - the PKCS1v1.5 881 | * padding method (as described in 882 | * RFC 2313) is employed to pad the 883 | * plaintext string. The plaintext 884 | * string must be no longer than the 885 | * length of the RSA key minus 11, 886 | * since PKCS1v1.5 requires 3 bytes 887 | * of overhead and specifies a 888 | * minimum pad of 8 bytes. The 889 | * plaintext string is padded with 890 | * randomly-generated bytes and then 891 | * its order is reversed to preserve 892 | * big-endian order before it is 893 | * encrypted to maintain 894 | * compatibility with real crypto 895 | * libraries such as OpenSSL or 896 | * Microsoft. When the cyphertext 897 | * is to be decrypted by a crypto 898 | * library, the library routine's 899 | * RSAAPP.PKCS1Padding flag, or its 900 | * equivalent, should be used. 901 | * 902 | * encoding The optional encoding scheme to use 903 | * for the return value. If ommitted, 904 | * numeric encoding will be used. 905 | * 906 | * RawEncoding - The return value 907 | * is given as its raw value. 908 | * This is the easiest method when 909 | * interoperating with server-side 910 | * OpenSSL, as no additional conversion 911 | * is required. Use the constant 912 | * RSAAPP.RawEncoding for this option. 913 | * 914 | * NumericEncoding - The return value 915 | * is given as a number in hexadecimal. 916 | * Perhaps useful for debugging, but 917 | * will need to be translated back to 918 | * its raw equivalent (e.g. using 919 | * PHP's hex2bin) before using with 920 | * OpenSSL. Use the constant 921 | * RSAAPP.NumericEncoding for this option. 922 | * 923 | * returns The cyphertext block that results 924 | * from encrypting the plaintext string 925 | * s with the RSA key. 926 | * 927 | * This routine accepts a plaintext string that is to be encrypted with the 928 | * public key component of the previously-built RSA key using the RSA 929 | * assymmetric encryption method. Before it is encrypted, the plaintext 930 | * string is padded to the same length as the encryption key for proper 931 | * encryption. 932 | * 933 | * Depending on the padding method chosen, an optional header with block type 934 | * is prepended, the plaintext is padded using zeros or randomly-generated 935 | * bytes, and then the plaintext is possibly broken up into chunks. 936 | * 937 | * Note that, for padding with zeros, this routine was altered by Rob Saunders 938 | * (rob@robsaunders.net). The new routine pads the string after it has been 939 | * converted to an array. This fixes an incompatibility with Flash MX's 940 | * ActionScript. 941 | * 942 | * The various padding schemes employed by this routine, and as presented to 943 | * RSA for encryption, are shown below. Note that the RSA encryption done 944 | * herein reverses the byte order as encryption is done: 945 | * 946 | * Plaintext In 947 | * ------------ 948 | * 949 | * d5 d4 d3 d2 d1 d0 950 | * 951 | * OHDave 952 | * ------ 953 | * 954 | * d5 d4 d3 d2 d1 d0 00 00 00 /.../ 00 00 00 00 00 00 00 00 955 | * 956 | * NoPadding 957 | * --------- 958 | * 959 | * 00 00 00 00 00 00 00 00 00 /.../ 00 00 d0 d1 d2 d3 d4 d5 960 | * 961 | * PKCS1Padding 962 | * ------------ 963 | * 964 | * d0 d1 d2 d3 d4 d5 00 p0 p1 /.../ p2 p3 p4 p5 p6 p7 02 00 965 | * \------------ ------------/ 966 | * \/ 967 | * Minimum 8 bytes pad length 968 | */ 969 | { 970 | var a = new Array(); // The usual Alice and Bob stuff 971 | var sl = s.length; // Plaintext string length 972 | var i, j, k; // The usual Fortran index stuff 973 | var padtype; // Type of padding to do 974 | var encodingtype; // Type of output encoding 975 | var rpad; // Random pad 976 | var al; // Array length 977 | var result = ""; // Cypthertext result 978 | var block; // Big integer block to encrypt 979 | var crypt; // Big integer result 980 | var text; // Text result 981 | /* 982 | * Figure out the padding type. 983 | */ 984 | if (typeof(pad) == 'string') { 985 | if (pad == RSAAPP.NoPadding) { padtype = 1; } 986 | else if (pad == RSAAPP.PKCS1Padding) { padtype = 2; } 987 | else { padtype = 0; } 988 | } 989 | else { padtype = 0; } 990 | /* 991 | * Determine encoding type. 992 | */ 993 | if (typeof(encoding) == 'string' && encoding == RSAAPP.RawEncoding) { 994 | encodingtype = 1; 995 | } 996 | else { encodingtype = 0; } 997 | 998 | /* 999 | * If we're not using Dave's padding method, we need to truncate long 1000 | * plaintext blocks to the correct length for the padding method used: 1001 | * 1002 | * NoPadding - key length 1003 | * PKCS1Padding - key length - 11 1004 | */ 1005 | if (padtype == 1) { 1006 | if (sl > key.chunkSize) { sl = key.chunkSize; } 1007 | } 1008 | else if (padtype == 2) { 1009 | if (sl > (key.chunkSize-11)) { sl = key.chunkSize - 11; } 1010 | } 1011 | /* 1012 | * Convert the plaintext string to an array of characters so that we can work 1013 | * with individual characters. 1014 | * 1015 | * Note that, if we're talking to a real crypto library at the other end, we 1016 | * reverse the plaintext order to preserve big-endian order. 1017 | */ 1018 | i = 0; 1019 | 1020 | if (padtype == 2) { j = sl - 1; } 1021 | else { j = key.chunkSize - 1; } 1022 | 1023 | while (i < sl) { 1024 | if (padtype) { a[j] = s.charCodeAt(i); } 1025 | else { a[i] = s.charCodeAt(i); } 1026 | 1027 | i++; j--; 1028 | } 1029 | /* 1030 | * Now is the time to add the padding. 1031 | * 1032 | * If we're doing PKCS1v1.5 padding, we pick up padding where we left off and 1033 | * pad the remainder of the block. Otherwise, we pad at the front of the 1034 | * block. This gives us the correct padding for big-endian blocks. 1035 | * 1036 | * The padding is either a zero byte or a randomly-generated non-zero byte. 1037 | */ 1038 | if (padtype == 1) { i = 0; } 1039 | 1040 | j = key.chunkSize - (sl % key.chunkSize); 1041 | 1042 | while (j > 0) { 1043 | if (padtype == 2) { 1044 | rpad = Math.floor(Math.random() * 256); 1045 | 1046 | while (!rpad) { rpad = Math.floor(Math.random() * 256); } 1047 | 1048 | a[i] = rpad; 1049 | } 1050 | else { a[i] = 0; } 1051 | 1052 | i++; j--; 1053 | } 1054 | /* 1055 | * For PKCS1v1.5 padding, we need to fill in the block header. 1056 | * 1057 | * According to RFC 2313, a block type, a padding string, and the data shall 1058 | * be formatted into the encryption block: 1059 | * 1060 | * EncrBlock = 00 || BlockType || PadString || 00 || Data 1061 | * 1062 | * The block type shall be a single octet indicating the structure of the 1063 | * encryption block. For this version of the document it shall have value 00, 1064 | * 01, or 02. For a private-key operation, the block type shall be 00 or 01. 1065 | * For a public-key operation, it shall be 02. 1066 | * 1067 | * The padding string shall consist of enough octets to pad the encryption 1068 | * block to the length of the encryption key. For block type 00, the octets 1069 | * shall have value 00; for block type 01, they shall have value FF; and for 1070 | * block type 02, they shall be pseudorandomly generated and nonzero. 1071 | * 1072 | * Note that in a previous step, we wrote padding bytes into the first three 1073 | * bytes of the encryption block because it was simpler to do so. We now 1074 | * overwrite them. 1075 | */ 1076 | if (padtype == 2) 1077 | { 1078 | a[sl] = 0; 1079 | a[key.chunkSize-2] = 2; 1080 | a[key.chunkSize-1] = 0; 1081 | } 1082 | /* 1083 | * Carve up the plaintext and encrypt each of the resultant blocks. 1084 | */ 1085 | al = a.length; 1086 | 1087 | for (i = 0; i < al; i += key.chunkSize) { 1088 | /* 1089 | * Get a block. 1090 | */ 1091 | block = new BigInt(); 1092 | 1093 | j = 0; 1094 | 1095 | for (k = i; k < (i+key.chunkSize); ++j) { 1096 | block.digits[j] = a[k++]; 1097 | block.digits[j] += a[k++] << 8; 1098 | } 1099 | /* 1100 | * Encrypt it, convert it to text, and append it to the result. 1101 | */ 1102 | crypt = key.barrett.powMod(block, key.e); 1103 | if (encodingtype == 1) { 1104 | text = biToBytes(crypt); 1105 | } 1106 | else { 1107 | text = (key.radix == 16) ? biToHex(crypt) : biToString(crypt, key.radix); 1108 | } 1109 | result += text; 1110 | } 1111 | /* 1112 | * Return the result, removing the last space. 1113 | */ 1114 | //result = (result.substring(0, result.length - 1)); 1115 | return result; 1116 | } 1117 | 1118 | /*****************************************************************************/ 1119 | 1120 | function decryptedString(key, c) 1121 | /* 1122 | * key The previously-built RSA key whose 1123 | * private key component is to be used 1124 | * to decrypt the cyphertext string. 1125 | * 1126 | * c The cyphertext string that is to be 1127 | * decrypted, using the RSA assymmetric 1128 | * encryption method. 1129 | * 1130 | * returns The plaintext block that results from 1131 | * decrypting the cyphertext string c 1132 | * with the RSA key. 1133 | * 1134 | * This routine is the complementary decryption routine that is meant to be 1135 | * used for JavaScript decryption of cyphertext blocks that were encrypted 1136 | * using the OHDave padding method of the encryptedString routine (in this 1137 | * module). It can also decrypt cyphertext blocks that were encrypted by 1138 | * RSAEncode (in CryptoFuncs.pm or CryptoFuncs.php) so that encrypted 1139 | * messages can be sent of insecure links (e.g. HTTP) to a Web page. 1140 | * 1141 | * It accepts a cyphertext string that is to be decrypted with the public key 1142 | * component of the previously-built RSA key using the RSA assymmetric 1143 | * encryption method. Multiple cyphertext blocks are broken apart, if they 1144 | * are found in c, and each block is decrypted. All of the decrypted blocks 1145 | * are concatenated back together to obtain the original plaintext string. 1146 | * 1147 | * This routine assumes that the plaintext was padded to the same length as 1148 | * the encryption key with zeros. Therefore, it removes any zero bytes that 1149 | * are found at the end of the last decrypted block, before it is appended to 1150 | * the decrypted plaintext string. 1151 | * 1152 | * Note that the encryptedString routine (in this module) works fairly quickly 1153 | * simply by virtue of the fact that the public key most often chosen is quite 1154 | * short (e.g. 0x10001). This routine does not have that luxury. The 1155 | * decryption key that it must employ is the full key length. For long keys, 1156 | * this can result in serious timing delays (e.g. 7-8 seconds to decrypt using 1157 | * 2048 bit keys on a reasonably fast machine, under the Firefox Web browser). 1158 | * 1159 | * If you intend to send encrypted messagess to a JavaScript program running 1160 | * under a Web browser, you might consider using shorter keys to keep the 1161 | * decryption times low. Alternately, a better scheme is to generate a random 1162 | * key for use by a symmetric encryption algorithm and transmit it to the 1163 | * other end, after encrypting it with encryptedString. The other end can use 1164 | * a real crypto library (e.g. OpenSSL or Microsoft) to decrypt the key and 1165 | * then use it to encrypt all of the messages (with a symmetric encryption 1166 | * algorithm such as Twofish or AES) bound for the JavaScript program. 1167 | * Symmetric decryption is orders of magnitude faster than asymmetric and 1168 | * should yield low decryption times, even when executed in JavaScript. 1169 | * 1170 | * Also note that only the OHDave padding method (e.g. zeros) is supported by 1171 | * this routine *AND* that this routine expects little-endian cyphertext, as 1172 | * created by the encryptedString routine (in this module) or the RSAEncode 1173 | * routine (in either CryptoFuncs.pm or CryptoFuncs.php). You can use one of 1174 | * the real crypto libraries to create cyphertext that can be decrypted by 1175 | * this routine, if you reverse the plaintext byte order first and then 1176 | * manually pad it with zero bytes. The plaintext should then be encrypted 1177 | * with the NoPadding flag or its equivalent in the crypto library of your 1178 | * choice. 1179 | */ 1180 | { 1181 | var blocks = c.split(" "); // Multiple blocks of cyphertext 1182 | var b; // The usual Alice and Bob stuff 1183 | var i, j; // The usual Fortran index stuff 1184 | var bi; // Cyphertext as a big integer 1185 | var result = ""; // Plaintext result 1186 | /* 1187 | * Carve up the cyphertext into blocks. 1188 | */ 1189 | for (i = 0; i < blocks.length; ++i) { 1190 | /* 1191 | * Depending on the radix being used for the key, convert this cyphertext 1192 | * block into a big integer. 1193 | */ 1194 | if (key.radix == 16) { bi = biFromHex(blocks[i]); } 1195 | else { bi = biFromString(blocks[i], key.radix); } 1196 | /* 1197 | * Decrypt the cyphertext. 1198 | */ 1199 | b = key.barrett.powMod(bi, key.d); 1200 | /* 1201 | * Convert the decrypted big integer back to the plaintext string. Since 1202 | * we are using big integers, each element thereof represents two bytes of 1203 | * plaintext. 1204 | */ 1205 | for (j = 0; j <= biHighIndex(b); ++j) { 1206 | result += String.fromCharCode(b.digits[j] & 255, b.digits[j] >> 8); 1207 | } 1208 | } 1209 | /* 1210 | * Remove trailing null, if any. 1211 | */ 1212 | if (result.charCodeAt(result.length - 1) == 0) { 1213 | result = result.substring(0, result.length - 1); 1214 | } 1215 | /* 1216 | * Return the plaintext. 1217 | */ 1218 | return (result); 1219 | } 1220 | 1221 | // ---------- Barrett.js copied ---------- 1222 | 1223 | // BarrettMu, a class for performing Barrett modular reduction computations in 1224 | // JavaScript. 1225 | // 1226 | // Requires BigInt.js. 1227 | // 1228 | // Copyright 2004-2005 David Shapiro. 1229 | // 1230 | // You may use, re-use, abuse, copy, and modify this code to your liking, but 1231 | // please keep this header. 1232 | // 1233 | // Thanks! 1234 | // 1235 | // Dave Shapiro 1236 | // dave@ohdave.com 1237 | 1238 | function BarrettMu(m) 1239 | { 1240 | this.modulus = biCopy(m); 1241 | this.k = biHighIndex(this.modulus) + 1; 1242 | var b2k = new BigInt(); 1243 | b2k.digits[2 * this.k] = 1; // b2k = b^(2k) 1244 | this.mu = biDivide(b2k, this.modulus); 1245 | this.bkplus1 = new BigInt(); 1246 | this.bkplus1.digits[this.k + 1] = 1; // bkplus1 = b^(k+1) 1247 | this.modulo = BarrettMu_modulo; 1248 | this.multiplyMod = BarrettMu_multiplyMod; 1249 | this.powMod = BarrettMu_powMod; 1250 | } 1251 | 1252 | function BarrettMu_modulo(x) 1253 | { 1254 | var q1 = biDivideByRadixPower(x, this.k - 1); 1255 | var q2 = biMultiply(q1, this.mu); 1256 | var q3 = biDivideByRadixPower(q2, this.k + 1); 1257 | var r1 = biModuloByRadixPower(x, this.k + 1); 1258 | var r2term = biMultiply(q3, this.modulus); 1259 | var r2 = biModuloByRadixPower(r2term, this.k + 1); 1260 | var r = biSubtract(r1, r2); 1261 | if (r.isNeg) { 1262 | r = biAdd(r, this.bkplus1); 1263 | } 1264 | var rgtem = biCompare(r, this.modulus) >= 0; 1265 | while (rgtem) { 1266 | r = biSubtract(r, this.modulus); 1267 | rgtem = biCompare(r, this.modulus) >= 0; 1268 | } 1269 | return r; 1270 | } 1271 | 1272 | function BarrettMu_multiplyMod(x, y) 1273 | { 1274 | /* 1275 | x = this.modulo(x); 1276 | y = this.modulo(y); 1277 | */ 1278 | var xy = biMultiply(x, y); 1279 | return this.modulo(xy); 1280 | } 1281 | 1282 | function BarrettMu_powMod(x, y) 1283 | { 1284 | var result = new BigInt(); 1285 | result.digits[0] = 1; 1286 | var a = x; 1287 | var k = y; 1288 | while (true) { 1289 | if ((k.digits[0] & 1) != 0) result = this.multiplyMod(result, a); 1290 | k = biShiftRight(k, 1); 1291 | if (k.digits[0] == 0 && biHighIndex(k) == 0) break; 1292 | a = this.multiplyMod(a, a); 1293 | } 1294 | return result; 1295 | } 1296 | 1297 | module.exports = { 1298 | BigInt: { 1299 | setMaxDigits: setMaxDigits, 1300 | }, 1301 | encryptedString: encryptedString, 1302 | RSAKeyPair: RSAKeyPair, 1303 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lyric-dl", 3 | "version": "0.2.0", 4 | "description": "Lyrics download tools", 5 | "main": "index.js", 6 | "dependencies": { 7 | "debug": "^2.6.8", 8 | "async": "^2.1.4", 9 | "commander": "^2.9.0", 10 | "crypto-js": "^3.1.9-1", 11 | "html-decoder": "^1.0.2", 12 | "cheerio": "1.0.0-rc.1" 13 | }, 14 | "directories": { 15 | "bin": "./bin", 16 | "lib": "./lib", 17 | "example": "./example" 18 | }, 19 | "devDependencies": {}, 20 | "scripts": { 21 | "test": "bash test/tests.sh" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://frimin@github.com/frimin/lyric-dl.git" 26 | }, 27 | "keywords": [ 28 | "lyric" 29 | ], 30 | "author": "frimin", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/frimin/lyric-dl/issues" 34 | }, 35 | "homepage": "https://github.com/frimin/lyric-dl#readme" 36 | } 37 | -------------------------------------------------------------------------------- /test/command_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | search_name="123" 4 | 5 | export DEBUG='*' 6 | 7 | test_case "search from ntes" 8 | lyric-dl search $search_name --output-url -f ntes > "$temp_dir/ntes_results" 9 | test_end 10 | 11 | test_case "check ntes result amount" 12 | ntes_results=($(cat "$temp_dir/ntes_results")) 13 | [[ ${#ntes_results[@]} -eq 30 ]] 14 | test_end 15 | 16 | test_case "search from qq" 17 | lyric-dl search $search_name --output-url -f qq > $temp_dir/qq_results 18 | test_end 19 | 20 | test_case "check qq result amount" 21 | qq_results=($(cat "$temp_dir/qq_results")) 22 | [[ ${#qq_results[@]} -eq 20 ]] 23 | test_end 24 | 25 | test_case "search from xiami" 26 | lyric-dl search $search_name --output-url -f xiami > $temp_dir/xiami_results 27 | test_end 28 | 29 | test_case "check xiai result amount" 30 | xiami_results=($(cat "$temp_dir/xiami_results")) 31 | [[ ${#xiami_results[@]} -eq 20 ]] 32 | test_end 33 | 34 | test_case "download from ntes" 35 | cat $temp_dir/ntes_results | sed 3q | lyric-dl download - -d $temp_dir --extract 36 | test_end 37 | 38 | test_case "download from qq" 39 | cat $temp_dir/qq_results | sed 3q | lyric-dl download - -d $temp_dir --extract 40 | test_end 41 | 42 | test_case "download qq from id" 43 | lyric-dl download "https://y.qq.com/n/yqq/song/201932121_num.html" -d $temp_dir --extract 44 | test_end 45 | 46 | test_case "download from xiami" 47 | cat $temp_dir/xiami_results | sed 3q | lyric-dl download - -d $temp_dir 48 | test_end -------------------------------------------------------------------------------- /test/tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH=$(pwd)/bin:$PATH 4 | 5 | succeed_count=0 6 | failed_count=0 7 | 8 | test_case () { 9 | echo "test case : '$1'" 10 | } 11 | 12 | test_end() { 13 | if [[ $? -ne 0 ]]; then 14 | ((failed_count++)) 15 | echo "failed" 16 | else 17 | ((succeed_count++)) 18 | if [[ -z "$1" ]]; then 19 | echo "ok" 20 | else 21 | echo "ok,$1" 22 | fi 23 | fi 24 | } 25 | 26 | temp_dir="test/.temp" 27 | 28 | [[ ! -d $temp_dir ]] && mkdir $temp_dir 29 | 30 | . test/command_tests.sh 31 | 32 | rm -rf $temp_dir 33 | 34 | echo "$succeed_count succeed, $failed_count failed" 35 | 36 | if [[ $failed_count -eq 0 ]]; then 37 | exit 0 38 | else 39 | exit 2 40 | fi --------------------------------------------------------------------------------