├── .gitignore ├── README.md ├── index.js ├── package.json ├── server.js ├── src ├── index.js ├── provider │ ├── netease.js │ ├── qq.js │ └── xiami.js └── vendor │ └── crypto.js ├── test ├── netease.html ├── netease.js ├── qq-time.js ├── server-test.json ├── test.js ├── xiami.html └── xiami.js ├── webpack.config.js └── zh-README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .vscode 4 | *.suo 5 | *.ntvs* 6 | *.njsproj 7 | *.sln 8 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # music-api-next 2 | 3 | > Music API for search results, songs, comments from QQ, Xiami and Netease. 4 | 5 | For more information on how to get started and how to use `music-api-next`, please see [author's blog](https://godbmw.com/) and comment there. For source files and issues, please visit [the github repo](https://github.com/dongyuanxin/music-api-next). 6 | 7 | [**DOCS**](https://godbmw.com/passage/62) 8 | 9 | [**中文文档**](https://godbmw.com/passage/63) 10 | 11 | ## Install 12 | 13 | ``` 14 | npm install music-api-next --save 15 | ``` 16 | 17 | If you are in China, please use: 18 | 19 | ``` 20 | cnpm install music-api-next --save 21 | ``` 22 | 23 | ## Quick Start 24 | 25 | ```javascript 26 | const musicAPI = require("music-api-next"); 27 | 28 | // Search API: search keywords on qq, xiami or netease 29 | musicAPI 30 | .searchSong({ 31 | key: "周杰伦", 32 | page: 1, 33 | limit: 10, 34 | vendor: "qq" 35 | }) 36 | .then(songs => console.log(songs)) 37 | .catch(error => console.log(error.message)); 38 | 39 | // Song API: get music meta including URL 40 | musicAPI 41 | .getSong({ 42 | id: "003OUlho2HcRHC", 43 | vendor: "qq" 44 | }) 45 | .then(meta => console.log(meta)) 46 | .catch(error => console.log(error.message)); 47 | 48 | // Comment API: get comments for the specified song 49 | musicAPI 50 | .getComment({ 51 | id: "003OUlho2HcRHC", 52 | page: 1, 53 | limit: 20, 54 | vendor: "qq" 55 | }) 56 | .then(comments => console.log(comments)) 57 | .catch(error => console.log(error.message)); 58 | ``` 59 | 60 | ## Run with a server 61 | 62 | ```shell 63 | git clone git@github.com:dongyuanxin/music-api-next.git 64 | cd music-api-next 65 | npm install 66 | // run server on port: 5050 67 | node server.js 68 | ``` 69 | 70 | You can see the results of music APIs by accessing the url. 71 | 72 | For example: 73 | 74 | - Search API: `http://localhost:5050/search/song?key=周杰伦&page=1&limit=10&vendor=qq` 75 | - Song API: `http://localhost:5050/get/song?id=003OUlho2HcRHC&vendor=qq` 76 | - Comment API: `http://localhost:5050/get/comment?id=003OUlho2HcRHC&page=1&limit=10&vendor=qq` 77 | 78 | ## Run with webpack 79 | 80 | First, package with webpack. 81 | 82 | ```shell 83 | git clone git@github.com:dongyuanxin/music-api-next.git 84 | cd music-api-next 85 | npm install 86 | // use webpack to package program 87 | // pacakged file named 'music-api-next.js' is placed in ./dist/ 88 | webpack 89 | ``` 90 | 91 | Then, you can move `music-api-next.js` very conveniently and use it in the following ways: 92 | 93 | ```javascript 94 | const musicAPI = require("./music-api-next"); 95 | 96 | // ... 97 | ``` 98 | 99 | ## API 100 | 101 | - `musicAPI.searchSong(query)`: 102 | 103 | ``` 104 | query: { 105 | key: String, 106 | page: Number, 107 | limit: Number, 108 | vendor: one of ['netease', 'xiami', 'qq'] 109 | } 110 | ``` 111 | 112 | - `musicAPI.getSong(query)`: 113 | 114 | ``` 115 | query: { 116 | id: String or Number, 117 | vendor: one of ['netease', 'xiami', 'qq'] 118 | } 119 | ``` 120 | 121 | - `musicAPI.getComment(query)`: 122 | 123 | ``` 124 | query: { 125 | id: String or Number, 126 | page: Number, 127 | limit: Number, 128 | vendor: one of ['netease', 'xiami', 'qq'] 129 | } 130 | ``` 131 | 132 | ## Warning 133 | 134 | 1. **It cannot be used for commercial purposes.** 135 | 2. **It runs only on NodeJS instead of browser.** 136 | 3. **Please use it politely and don't put too much pressure on music platforms.** 137 | 138 | ## Thanks 139 | 140 | The code for this project refers to the following open source projects and has been fixed, improved and added on NodeJS. 141 | 142 | 1. [listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension): Has received a lawyer's letter from Tencent. May stop maintenance at the end of the year 2018. 143 | 2. [musicAPI](https://github.com/LIU9293/musicAPI): It has stopped maintenance one year ago. So its APIs are invalid. 144 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./src/index"); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cheerio": "^1.0.0-rc.2", 4 | "crypto-js": "^3.1.9-1", 5 | "moment": "^2.22.2", 6 | "request": "^2.87.0" 7 | }, 8 | "devDependencies": { 9 | "koa": "^2.5.2", 10 | "koa-bodyparser": "^4.2.1", 11 | "koa-router": "^7.4.0", 12 | "koa-xml-body": "^2.0.0", 13 | "webpack": "^4.16.3" 14 | }, 15 | "scripts": { 16 | "start": "node server.js" 17 | }, 18 | "name": "music-api-next", 19 | "description": "Music API for search results, songs, comments from QQ, Xiami and Netease. ", 20 | "version": "1.0.0", 21 | "main": "index.js", 22 | "directories": { 23 | "test": "test" 24 | }, 25 | "keywords": [ 26 | "Music", 27 | "Xiami", 28 | "Netease", 29 | "QQ", 30 | "API", 31 | "Node" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/dongyuanxin/music-api-next.git" 36 | }, 37 | "author": "godbmw", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/dongyuanxin/music-api-next/issues" 41 | }, 42 | "homepage": "https://github.com/dongyuanxin/music-api-next#readme" 43 | } 44 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const PORT = 5050; 2 | 3 | const koa = require("koa"); 4 | const router = require("koa-router")(); 5 | const bodyParser = require("koa-bodyparser"); 6 | const xmlParser = require("koa-xml-body"); 7 | 8 | const path = require("path"); 9 | const http = require("http"); 10 | 11 | const musicAPI = require(path.resolve("src", "index.js")); 12 | 13 | let app = new koa(); 14 | app.use(xmlParser()); 15 | app.use(bodyParser()); 16 | 17 | router.get("/search/song", async (ctx, next) => { 18 | let response = await musicAPI.searchSong(ctx.request.query); 19 | ctx.response.body = response; 20 | return; 21 | }); 22 | router.post("/search/song", async (ctx, next) => { 23 | let response = await musicAPI.searchSong(ctx.request.body); 24 | ctx.response.body = response; 25 | return; 26 | }); 27 | 28 | router.get("/get/song", async (ctx, next) => { 29 | let response = await musicAPI.getSong(ctx.request.query); 30 | ctx.response.body = response; 31 | return; 32 | }); 33 | router.post("/get/song", async (ctx, next) => { 34 | let response = await musicAPI.getSong(ctx.request.body); 35 | ctx.response.body = response; 36 | return; 37 | }); 38 | 39 | router.get("/get/comment", async (ctx, next) => { 40 | let response = await musicAPI.getComment(ctx.request.query); 41 | ctx.response.body = response; 42 | return; 43 | }); 44 | router.post("/get/comment", async (ctx, next) => { 45 | let response = await musicAPI.getComment(ctx.request.body); 46 | ctx.response.body = response; 47 | return; 48 | }); 49 | 50 | app.use(router.routes()); 51 | 52 | app.use(async (ctx, next) => { 53 | ctx.status = 404; 54 | }); 55 | 56 | http.createServer(app.callback()).listen(PORT); 57 | 58 | console.log("music-api started at port " + PORT); 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Netease = require("./provider/netease"); 2 | const QQ = require("./provider/qq"); 3 | const Xiami = require("./provider/xiami"); 4 | 5 | const api = { 6 | netease: new Netease(), 7 | qq: new QQ(), 8 | xiami: new Xiami() 9 | }; 10 | 11 | const isEmptyStr = str => str === "" || str === undefined || str === null; 12 | const isUnvalidVendor = vendor => 13 | isEmptyStr(vendor) || Object.keys(api).indexOf(vendor) < 0; 14 | 15 | const searchSong = async options => { 16 | let { key, page, limit, vendor } = options; 17 | if (isEmptyStr(key) || isUnvalidVendor(vendor)) { 18 | return { 19 | success: false, 20 | msg: "Missing parameter" 21 | }; 22 | } 23 | page = page === undefined ? 1 : page; 24 | limit = limit === undefined ? 20 : limit; 25 | return await api[vendor].searchSong(key, page, limit); 26 | }; 27 | 28 | const getSong = async options => { 29 | let { id, vendor } = options; 30 | if (isUnvalidVendor(vendor) || id === undefined) { 31 | return { success: false, msg: "Missing parameter" }; 32 | } 33 | return await api[vendor].getSong(id); 34 | }; 35 | 36 | const getComment = async options => { 37 | let { vendor, id, page, limit } = options; 38 | if (isUnvalidVendor(vendor) || id === undefined) { 39 | return { success: false, msg: "Missing parameter" }; 40 | } 41 | page = page === undefined ? 1 : page; 42 | limit = limit === undefined ? 20 : limit; 43 | return await api[vendor].getComment(id, page, limit); 44 | }; 45 | 46 | module.exports = { 47 | searchSong, 48 | getSong, 49 | getComment 50 | }; 51 | -------------------------------------------------------------------------------- /src/provider/netease.js: -------------------------------------------------------------------------------- 1 | const request = require("request"); 2 | const moment = require("moment"); 3 | 4 | const querystring = require("querystring"); 5 | const { asrsea } = require("./../vendor/crypto"); 6 | 7 | class Music { 8 | constructor() { 9 | this.e = "010001"; 10 | this.f = 11 | "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"; 12 | this.g = "0CoJUm6Qyw8W8jud"; 13 | } 14 | searchSong(key, page, limit) { 15 | let url = "http://music.163.com/weapi/cloudsearch/get/web?csrf_token="; 16 | let form = { 17 | s: key, 18 | type: 1, 19 | limit, 20 | offset: limit * (page - 1) 21 | }; 22 | let { encText, encSecKey } = asrsea( 23 | JSON.stringify(form), 24 | this.e, 25 | this.f, 26 | this.g 27 | ); 28 | let options = { 29 | url, 30 | method: "POST", 31 | body: querystring.stringify({ 32 | params: encText, 33 | encSecKey: encSecKey 34 | }), 35 | headers: { "Content-Type": "application/x-www-form-urlencoded" } 36 | }; 37 | let promise = new Promise(resolve => { 38 | request(options, (err, res, body) => { 39 | if (err) { 40 | return resolve({ 41 | success: false, 42 | msg: err.message 43 | }); 44 | } 45 | try { 46 | let data = JSON.parse(body); 47 | return resolve({ 48 | success: true, 49 | results: data.result.songs.map(item => { 50 | return { 51 | id: item.id, 52 | name: item.name, 53 | artist: item.ar[0].name, 54 | album: item.al.name, 55 | cover: item.al.picUrl, 56 | needPay: item.privilege.st < 0 57 | }; 58 | }) 59 | }); 60 | } catch (error) { 61 | return resolve({ success: false, msg: error.message }); 62 | } 63 | }); 64 | }); 65 | return promise; 66 | } 67 | getSong(id) { 68 | return new Promise(resolve => { 69 | resolve({ 70 | success: true, 71 | results: { 72 | url: "http://music.163.com/song/media/outer/url?id=" + id + ".mp3" 73 | } 74 | }); 75 | }); 76 | } 77 | filterComment(comment) { 78 | let rule = /(\[.*?\])|\n|\\n/gm; 79 | return comment.replace(rule, ""); 80 | } 81 | getComment(id, page, limit) { 82 | let url = "https://music.163.com/weapi/v1/resource/comments/R_SO_4_" + id; 83 | let form = { 84 | rid: "R_SO_4_" + id, 85 | offset: limit * (page - 1), 86 | total: true, 87 | limit, 88 | csrf_token: "" 89 | }; 90 | let { encText, encSecKey } = asrsea( 91 | JSON.stringify(form), 92 | this.e, 93 | this.f, 94 | this.g 95 | ); 96 | let options = { 97 | url, 98 | method: "POST", 99 | body: querystring.stringify({ params: encText, encSecKey: encSecKey }), 100 | headers: { "Content-Type": "application/x-www-form-urlencoded" } 101 | }; 102 | let promise = new Promise(resolve => { 103 | request(options, (err, res, body) => { 104 | if (err) { 105 | return resolve({ success: false, msg: err.message }); 106 | } 107 | try { 108 | let data = JSON.parse(body); 109 | resolve({ 110 | success: true, 111 | results: data.comments.map(item => { 112 | return { 113 | time: moment(item.time).format("YYYY-MM-DD H:mm:ss"), 114 | content: this.filterComment(item.content), 115 | user: { 116 | headImgUrl: item.user.avatarUrl, 117 | nickname: item.user.nickname 118 | } 119 | }; 120 | }) 121 | }); 122 | } catch (error) { 123 | resolve({ success: false, msg: error.message }); 124 | } 125 | }); 126 | }); 127 | return promise; 128 | } 129 | } 130 | 131 | module.exports = Music; 132 | -------------------------------------------------------------------------------- /src/provider/qq.js: -------------------------------------------------------------------------------- 1 | const request = require("request"); 2 | const moment = require("moment"); 3 | 4 | const querystring = require("querystring"); 5 | 6 | class Music { 7 | constructor() {} 8 | searchSong(key, page, limit) { 9 | let jsonpCallback = "jsonp4"; 10 | let url = 11 | "http://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp?" + 12 | querystring.stringify({ 13 | jsonpCallback, 14 | loginUin: 0, 15 | hostUin: 0, 16 | format: "jsonp", 17 | inCharset: "utf-8", 18 | outCharset: "utf-8", 19 | notice: 0, 20 | platform: "qq", 21 | needNewCode: 0, 22 | p: page, 23 | n: limit, 24 | w: key 25 | }); 26 | let options = { 27 | url, 28 | method: "GET", 29 | headers: { 30 | referer: "https://y.qq.com/portal/search.html", 31 | "user-agent": 32 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" 33 | } 34 | }; 35 | let promise = new Promise(resolve => { 36 | request(options, (err, res, body) => { 37 | if (err) 38 | return resolve({ 39 | success: false 40 | }); 41 | try { 42 | let data = body.substr(jsonpCallback.length + 1); 43 | data = data.substr(0, data.length - 1); 44 | data = JSON.parse(data); 45 | return resolve({ 46 | success: true, 47 | results: data.data.song.list.map(item => { 48 | return { 49 | id: item.songmid, 50 | name: item.songname, 51 | artist: item.singer[0].name, 52 | album: item.albumname, 53 | cover: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${ 54 | item.albummid 55 | }.jpg`, 56 | needPay: item.pay.payplay > 0 57 | }; 58 | }) 59 | }); 60 | } catch (error) { 61 | return resolve({ 62 | success: false, 63 | msg: error.message 64 | }); 65 | } 66 | }); 67 | }); 68 | return promise; 69 | } 70 | getSong(id) { 71 | let jsonpCallback = "MusicJsonCallback7156632135681187"; 72 | let url = 73 | "https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?" + 74 | querystring.stringify({ 75 | g_tk: 1959393642, 76 | jsonpCallback, 77 | loginUin: 2181111110, 78 | hostUin: 0, 79 | format: "json", 80 | inCharset: "utf8", 81 | outCharset: "utf-8", 82 | notice: 0, 83 | platform: "yqq", 84 | needNewCode: 0, 85 | cid: 205361747, 86 | callback: jsonpCallback, 87 | uin: 2181111110, 88 | songmid: id, 89 | filename: `C400${id}.m4a`, 90 | guid: 9870159400 91 | }); 92 | let options = { 93 | url, 94 | method: "GET", 95 | headers: { 96 | referer: "https://y.qq.com/portal/player.html", 97 | "user-agent": 98 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" 99 | } 100 | }; 101 | let promise = new Promise(resolve => { 102 | request(options, (err, res, body) => { 103 | if (err) { 104 | return resolve({ success: false, msg: err.message }); 105 | } 106 | 107 | try { 108 | let data = body.substr(jsonpCallback.length + 1); 109 | data = data.substr(0, data.length - 1); 110 | data = JSON.parse(data); 111 | data = data.data.items[0]; 112 | return resolve({ 113 | success: true, 114 | results: { 115 | url: 116 | `http://dl.stream.qqmusic.qq.com/C400${id}.m4a?` + 117 | querystring.stringify({ 118 | vkey: data.vkey, 119 | guid: 9870159400, // 和上方一定要一样 120 | uin: 2181111110, 121 | fromtag: 66 122 | }) 123 | } 124 | }); 125 | } catch (error) { 126 | return resolve({ success: false, msg: error.message }); 127 | } 128 | }); 129 | }); 130 | return promise; 131 | } 132 | __getTopId(id) { 133 | let jsonpCallback = "getOneSongInfoCallback"; 134 | let url = 135 | "https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg?" + 136 | querystring.stringify({ 137 | songmid: id, 138 | tpl: "yqq_song_detail", 139 | loginUin: 0, 140 | hostUin: 0, 141 | format: "jsonp", 142 | callback: jsonpCallback, 143 | jsonpCallback, 144 | inCharset: "utf8", 145 | outCharset: "utf-8", 146 | notice: 0, 147 | platform: "yqq", 148 | needNewCode: 0 149 | }); 150 | let options = { 151 | url, 152 | method: "GET", 153 | headers: { 154 | referer: "https://y.qq.com/n/yqq/song/" + id + ".html", 155 | "user-agent": 156 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" 157 | } 158 | }; 159 | let promise = new Promise(resolve => { 160 | request(options, (err, res, body) => { 161 | if (err) { 162 | return resolve({ success: false, msg: err.message }); 163 | } 164 | try { 165 | let data = body.substr(jsonpCallback.length + 1); 166 | data = data.substr(0, data.length - 1); 167 | data = JSON.parse(data); 168 | let topId = data.data[0].id; 169 | if (topId === null || topId === undefined) { 170 | return resolve({ success: false, msg: "Not found" }); 171 | } else { 172 | return resolve({ success: true, results: topId }); 173 | } 174 | } catch (error) { 175 | return resolve({ success: false, msg: error.message }); 176 | } 177 | }); 178 | }); 179 | return promise; 180 | } 181 | filterComment(comment) { 182 | let rule = /(\[em\].*?\[\/em\])|\n|\\n/gm; 183 | return comment.replace(rule, ""); 184 | } 185 | async getComment(id, page, limit) { 186 | let results = await this.__getTopId(id); 187 | let promise = new Promise(resolve => { 188 | if (results.success === false) { 189 | return resolve(results); 190 | } 191 | let topId = results.results; 192 | let jsonpCallback = "jsoncallback21880487934016424"; 193 | let url = 194 | "https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg?" + 195 | querystring.stringify({ 196 | g_tk: 5381, 197 | jsonpCallback, 198 | loginUin: 0, 199 | hostUin: 0, 200 | format: "jsonp", 201 | inCharset: "utf8", 202 | outCharset: "GB2312", 203 | notice: 0, 204 | platform: "yqq", 205 | needNewCode: 0, 206 | cid: 205360772, 207 | reqtype: 2, 208 | biztype: 1, 209 | topid: topId, 210 | cmd: 8, 211 | needmusiccrit: 0, 212 | pagenum: page - 1, // 注意这里page从0开始计算 213 | pagesize: limit, 214 | lasthotcommentid: "", 215 | callback: jsonpCallback, 216 | domain: "qq.com", 217 | ct: 24, 218 | cv: 101010 219 | }); 220 | let options = { 221 | url, 222 | method: "GET", 223 | headers: { 224 | referer: "https://y.qq.com/n/yqq/song/" + id + ".html", 225 | "user-agent": 226 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" 227 | } 228 | }; 229 | request(options, (err, res, body) => { 230 | if (err) { 231 | return resolve({ success: false, msg: err.message }); 232 | } 233 | try { 234 | let data = body.substr(jsonpCallback.length + 1); 235 | data = data.substr(0, data.length - 3); // 最后有2个换行符和1个")" 236 | data = JSON.parse(data); 237 | return resolve({ 238 | success: true, 239 | results: data.comment.commentlist.map(item => { 240 | return { 241 | time: moment(1e3 * item.time).format("YYYY-MM-DD H:mm:ss"), 242 | content: this.filterComment(item.rootcommentcontent), 243 | user: { headImgUrl: item.avatarurl, nickname: item.nick } 244 | }; 245 | }) 246 | }); 247 | } catch (error) { 248 | return resolve({ success: false, msg: error.message }); 249 | } 250 | }); 251 | }); 252 | return promise; 253 | } 254 | } 255 | 256 | module.exports = Music; 257 | -------------------------------------------------------------------------------- /src/provider/xiami.js: -------------------------------------------------------------------------------- 1 | const request = require("request"); 2 | const cheerio = require("cheerio"); 3 | 4 | const querystring = require("querystring"); 5 | 6 | class Music { 7 | constructor() {} 8 | _caesar(location) { 9 | var num = location[0]; 10 | var avg_len = Math.floor(location.slice(1).length / num); 11 | var remainder = location.slice(1).length % num; 12 | 13 | var result = []; 14 | for (var i = 0; i < remainder; i++) { 15 | var line = location.slice( 16 | i * (avg_len + 1) + 1, 17 | (i + 1) * (avg_len + 1) + 1 18 | ); 19 | result.push(line); 20 | } 21 | 22 | for (var i = 0; i < num - remainder; i++) { 23 | var line = location 24 | .slice((avg_len + 1) * remainder) 25 | .slice(i * avg_len + 1, (i + 1) * avg_len + 1); 26 | result.push(line); 27 | } 28 | 29 | var s = []; 30 | for (var i = 0; i < avg_len; i++) { 31 | for (var j = 0; j < num; j++) { 32 | s.push(result[j][i]); 33 | } 34 | } 35 | 36 | for (var i = 0; i < remainder; i++) { 37 | s.push(result[i].slice(-1)); 38 | } 39 | 40 | return unescape(s.join("")).replace(/\^/g, "0"); 41 | } 42 | _handleProtocolRelativeUrl(url) { 43 | let regex = /^.*?\/\//; 44 | let result = url.replace(regex, "http://"); 45 | return result; 46 | } 47 | _xmRetinaUrl(s) { 48 | if (s.slice(-6, -4) == "_1") { 49 | return s.slice(0, -6) + s.slice(-4); 50 | } 51 | return s; 52 | } 53 | searchSong(key, page, limit) { 54 | let url = 55 | "http://api.xiami.com/web?" + 56 | querystring.stringify({ 57 | v: "2.0", 58 | key, 59 | limit, 60 | page, 61 | r: "search/songs", 62 | app_key: 1 63 | }); 64 | let options = { 65 | url, 66 | method: "POST", 67 | headers: { 68 | referer: "http://h.xiami.com/", // must options 69 | user_agent: 70 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36" 71 | } 72 | }; 73 | let promise = new Promise(resolve => { 74 | request(options, (err, res, body) => { 75 | if (err) 76 | return resolve({ 77 | success: false 78 | }); 79 | try { 80 | let data = JSON.parse(body); 81 | return resolve({ 82 | success: true, 83 | results: data.data.songs.map(item => { 84 | return { 85 | id: item.song_id, 86 | name: item.song_name, 87 | artist: item.artist_name, 88 | album: item.album_name, 89 | cover: item.album_pic, 90 | needPay: item.need_pay_flag === 1, 91 | plus: { file: item.listen_file } 92 | }; 93 | }) 94 | }); 95 | } catch (error) { 96 | return resolve({ 97 | success: false, 98 | msg: error.message 99 | }); 100 | } 101 | }); 102 | }); 103 | return promise; 104 | } 105 | getSong(id) { 106 | let url = `http://www.xiami.com/song/playlist/id/${id}/object_name/default/object_id/0/cat/json`; 107 | let options = { 108 | url, 109 | method: "GET", 110 | headers: { 111 | user_agent: 112 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36" 113 | } 114 | }; 115 | let promise = new Promise(resolve => { 116 | request(options, (err, res, body) => { 117 | if (err) 118 | return resolve({ 119 | success: false 120 | }); 121 | try { 122 | let data = JSON.parse(body); 123 | let location = data.data.trackList[0].location; 124 | return resolve({ 125 | success: true, 126 | results: { 127 | url: this._handleProtocolRelativeUrl(this._caesar(location)), 128 | lyric: this._handleProtocolRelativeUrl( 129 | data.data.trackList[0].lyric_url 130 | ), 131 | name: data.data.trackList[0].name, 132 | album: data.data.trackList[0].album_name, 133 | artist: data.data.trackList[0].artist_name, 134 | cover: data.data.trackList[0].album_pic 135 | } 136 | }); 137 | } catch (error) { 138 | return resolve({ 139 | success: false, 140 | msg: error.message 141 | }); 142 | } 143 | }); 144 | }); 145 | return promise; 146 | } 147 | __getPageComment(id, page) { 148 | let url = `https://www.xiami.com/commentlist/turnpage/id/${id}/page/${page}/ajax/1`; 149 | let options = { 150 | url, 151 | method: "POST", 152 | body: querystring.stringify({ type: "4" }), 153 | headers: { 154 | "Content-Type": "application/x-www-form-urlencoded", 155 | origin: "https://www.xiami.com", 156 | referer: "https://www.xiami.com/song/" + id, 157 | user_agent: 158 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36" 159 | } 160 | }; 161 | 162 | let promise = new Promise(resolve => { 163 | request(options, (err, res, body) => { 164 | if (err) return resolve({ success: false, msg: err.message }); 165 | try { 166 | let $ = cheerio.load(body), 167 | liArr = $("ul li"), 168 | results = []; 169 | for (let i = 0; i < liArr.length; ++i) { 170 | let li = $(liArr[i]), 171 | id = li.attr("id"); 172 | results.push({ 173 | time: li.find(".info span.time").text() + ":00", 174 | content: li 175 | .find("#brief_" + id) 176 | .clone() 177 | .children() 178 | .remove() 179 | .end() 180 | .text() 181 | .replace(/(\s*$)/g, ""), 182 | user: { 183 | headImgUrl: li.find("img").attr("src"), 184 | nickname: li.find("img").attr("alt") 185 | } 186 | }); 187 | } 188 | return resolve({ success: true, results }); 189 | } catch (error) { 190 | return resolve({ success: false, msg: error.message }); 191 | } 192 | }); 193 | }); 194 | return promise; 195 | } 196 | async getComment(id, page, limit) { 197 | const pageSize = 10; 198 | 199 | let startPage = parseInt(page * limit, 10) / pageSize, 200 | endPage = parseInt((page + 1) * limit, 10) / pageSize, 201 | offset = startPage * pageSize; 202 | 203 | let left = page * limit - offset, 204 | right = (page + 1) * limit - offset, 205 | results = []; 206 | 207 | let promise = new Promise(async resolve => { 208 | for (let i = startPage; i <= endPage; i++) { 209 | if (i === 0) { 210 | continue; 211 | } 212 | let res = await this.__getPageComment(id, i); 213 | if (res.success === false) { 214 | return resolve(res); 215 | } 216 | results = results.concat(res.results); 217 | } 218 | return resolve({ 219 | success: true, 220 | results: results.slice(left, right) 221 | }); 222 | }); 223 | 224 | return promise; 225 | } 226 | } 227 | 228 | module.exports = Music; 229 | -------------------------------------------------------------------------------- /src/vendor/crypto.js: -------------------------------------------------------------------------------- 1 | const CryptoJS = require("crypto-js"); 2 | 3 | (function() { 4 | function b(a, b) { 5 | var c = CryptoJS.enc.Utf8.parse(b), 6 | d = CryptoJS.enc.Utf8.parse("0102030405060708"), 7 | e = CryptoJS.enc.Utf8.parse(a), 8 | f = CryptoJS.AES.encrypt(e, c, { 9 | iv: d, 10 | mode: CryptoJS.mode.CBC 11 | }); 12 | return f.toString(); 13 | } 14 | function d(d, e, f, g) { 15 | var h = {}, 16 | i = "u3wFl5eFwTWI7dHF", 17 | encSecKey = 18 | "1eb2800c1605520f6c62e45a3e7eb9a3d331a4f1491618e4c52c029fd29a2b8535dc58708ce099817dd52b4bb1c9b5243f734dd0236849fd0b2c912aa49fab35659cd72d6633850d121b824237b18b3485e2c36cef52a270fb177aa17b2c7a865a836263a6db440eb1e6cd4a6066a0e379715d78b4b1caacaec76f45ce8a4e28"; 19 | return ( 20 | (h.encText = b(d, g)), 21 | (h.encText = b(h.encText, i)), 22 | (h.encSecKey = encSecKey), 23 | h 24 | ); 25 | } 26 | module.exports = { 27 | asrsea: d 28 | }; 29 | })(); 30 | -------------------------------------------------------------------------------- /test/netease.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |\r\n ';
424 | for (
425 | var l = s.middlecommentcontent.length, p = 0;
426 | l > p;
427 | p++
428 | ) {
429 | var d = s.middlecommentcontent[p];
430 | d.replynick || (d.replynick = d.replyuin),
431 | d.replyednick ||
432 | (d.replyednick = d.replyeduin);
433 | var u = "",
434 | h = "";
435 | l > 1 && (u = " // "),
436 | (h = l > 2 && p != l - 1 ? " // " : ""),
437 | (c += "\r\n "),
438 | (c +=
439 | 0 == p
440 | ? '回复 ' +
443 | (null == (n = d.replyednick)
444 | ? ""
445 | : _.escape(n)) +
446 | ': ' +
447 | (null ==
448 | (n = d.subcommentcontent
449 | .replace(/\n/gi, "
")
450 | .replace(/\\n/gi, "
"))
451 | ? ""
452 | : n) +
453 | "" +
454 | (null == (n = u) ? "" : n) +
455 | " "
456 | : '\r\n ' +
459 | (null == (n = d.replynick)
460 | ? ""
461 | : _.escape(n)) +
462 | ' 回复 ' +
465 | (null == (n = d.replyednick)
466 | ? ""
467 | : _.escape(n)) +
468 | ' : ' +
469 | (null ==
470 | (n = d.subcommentcontent
471 | .replace(/\n/gi, "
")
472 | .replace(/\\n/gi, "
"))
473 | ? ""
474 | : n) +
475 | "" +
476 | (null == (n = h) ? "" : n) +
477 | "\r\n ");
478 | }
479 | c += "\r\n
'),
483 | s.rootcommentcontent &&
484 | (c +=
485 | "" +
486 | (null ==
487 | (n = s.rootcommentcontent
488 | .replace(/\n/gi, "
")
489 | .replace(/\\n/gi, "
"))
490 | ? ""
491 | : n)),
492 | (c += "
'),
497 | s.rootcommentcontent &&
498 | (c +=
499 | "" +
500 | (null ==
501 | (n = s.rootcommentcontent
502 | .replace(/\n/gi, "
")
503 | .replace(/\\n/gi, "
"))
504 | ? ""
505 | : n)),
506 | (c += "
' + 872 | (null == (n = a.muscritcontent) ? "" : n) + 873 | '
\r\n查看全文\r\n
\r\n\r\n ';
1020 | for (var l = s.middlecommentcontent.length, p = 0; l > p; p++) {
1021 | var d = s.middlecommentcontent[p];
1022 | d.replynick || (d.replynick = d.replyuin),
1023 | d.replyednick || (d.replyednick = d.replyeduin);
1024 | var u = "",
1025 | h = "";
1026 | l > 1 && (u = " // "),
1027 | (h = l > 2 && p != l - 1 ? " // " : ""),
1028 | (c += "\r\n "),
1029 | (c +=
1030 | 0 == p
1031 | ? '回复 ' +
1034 | (null == (n = d.replyednick) ? "" : _.escape(n)) +
1035 | ': ' +
1036 | (null ==
1037 | (n = d.subcommentcontent
1038 | .replace(/\n/gi, "
")
1039 | .replace(/\\n/gi, "
"))
1040 | ? ""
1041 | : n) +
1042 | "" +
1043 | (null == (n = u) ? "" : n) +
1044 | " "
1045 | : '\r\n ' +
1048 | (null == (n = d.replynick) ? "" : _.escape(n)) +
1049 | ' 回复 ' +
1052 | (null == (n = d.replyednick) ? "" : _.escape(n)) +
1053 | ' : ' +
1054 | (null ==
1055 | (n = d.subcommentcontent
1056 | .replace(/\n/gi, "
")
1057 | .replace(/\\n/gi, "
"))
1058 | ? ""
1059 | : n) +
1060 | "" +
1061 | (null == (n = h) ? "" : n) +
1062 | "\r\n ");
1063 | }
1064 | c += "\r\n
'),
1067 | s.rootcommentcontent &&
1068 | (c +=
1069 | "" +
1070 | (null ==
1071 | (n = s.rootcommentcontent
1072 | .replace(/\n/gi, "
")
1073 | .replace(/\\n/gi, "
"))
1074 | ? ""
1075 | : n)),
1076 | (c += "
'),
1081 | s.rootcommentcontent &&
1082 | (c +=
1083 | "" +
1084 | (null ==
1085 | (n = s.rootcommentcontent
1086 | .replace(/\n/gi, "
")
1087 | .replace(/\\n/gi, "
"))
1088 | ? ""
1089 | : n)),
1090 | (c += "
\r\n ';
1277 | for (var l = s.middlecommentcontent.length, p = 0; l > p; p++) {
1278 | var d = s.middlecommentcontent[p];
1279 | d.replynick || (d.replynick = d.replyuin),
1280 | d.replyednick || (d.replyednick = d.replyeduin);
1281 | var u = "",
1282 | h = "";
1283 | l > 1 && (u = " // "),
1284 | (h = l > 2 && p != l - 1 ? " // " : ""),
1285 | (c += "\r\n "),
1286 | (c +=
1287 | 0 == p
1288 | ? '回复 ' +
1291 | (null == (n = d.replyednick) ? "" : _.escape(n)) +
1292 | ': ' +
1293 | (null ==
1294 | (n = d.subcommentcontent
1295 | .replace(/\n/gi, "
")
1296 | .replace(/\\n/gi, "
"))
1297 | ? ""
1298 | : n) +
1299 | "" +
1300 | (null == (n = u) ? "" : n) +
1301 | " "
1302 | : '\r\n ' +
1305 | (null == (n = d.replynick) ? "" : _.escape(n)) +
1306 | ' 回复 ' +
1309 | (null == (n = d.replyednick) ? "" : _.escape(n)) +
1310 | ' : ' +
1311 | (null ==
1312 | (n = d.subcommentcontent
1313 | .replace(/\n/gi, "
")
1314 | .replace(/\\n/gi, "
"))
1315 | ? ""
1316 | : n) +
1317 | "" +
1318 | (null == (n = h) ? "" : n) +
1319 | "\r\n ");
1320 | }
1321 | c += "\r\n
'),
1324 | s.rootcommentcontent &&
1325 | (c +=
1326 | "" +
1327 | (null ==
1328 | (n = s.rootcommentcontent
1329 | .replace(/\n/gi, "
")
1330 | .replace(/\\n/gi, "
"))
1331 | ? ""
1332 | : n)),
1333 | (c += "
'),
1338 | s.rootcommentcontent &&
1339 | (c +=
1340 | "" +
1341 | (null ==
1342 | (n = s.rootcommentcontent
1343 | .replace(/\n/gi, "
")
1344 | .replace(/\\n/gi, "
"))
1345 | ? ""
1346 | : n)),
1347 | (c += "
5 |
6 |
8 |
34 |
35 |
37 |
63 |
64 |
66 |
93 |
94 |
96 |
123 |
124 |
126 |
153 |
154 |
156 |
182 |
183 |
185 |
212 |
213 |
215 |
244 |
245 |
247 |
275 |
276 |
278 |